diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/AzureAgentProvider.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/AzureAgentProvider.cs index a86e2a2f4a..c45e601990 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/AzureAgentProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/AzureAgentProvider.cs @@ -123,7 +123,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj IAsyncEnumerable agentResponse = messages is not null ? agent.RunStreamingAsync([.. messages], null, runOptions, cancellationToken) : - agent.RunStreamingAsync([new ChatMessage(ChatRole.User, string.Empty)], null, runOptions, cancellationToken); + agent.RunStreamingAsync([], null, runOptions, cancellationToken); await foreach (AgentResponseUpdate update in agentResponse.ConfigureAwait(false)) { diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/AddConversationMessageTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/AddConversationMessageTemplate.cs index 52cb6eb2e2..34e1cb0020 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/AddConversationMessageTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/AddConversationMessageTemplate.cs @@ -1,7 +1,7 @@ // ------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version: 17.0.0.0 +// Runtime Version: 18.0.0.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -10,16 +10,13 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// /// Class to produce the template output /// - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")] internal partial class AddConversationMessageTemplate : ActionTemplate { /// @@ -35,17 +32,8 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen this.Write("\n"); this.Write("\n"); this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n\n/// \n/// Adds a new message to the specified agent conversation\n/// \ninternal sealed class "); + this.Write("\n/// \n/// Adds a new message to the specified agent conversation\n/// \ninternal sealed class "); this.Write(this.ToStringHelper.ToStringWithCulture(this.Name)); this.Write("Executor(FormulaSession session, WorkflowAgentProvider agentProvider) : ActionExe" + "cutor(id: \""); @@ -134,446 +122,6 @@ this.Write("\n "); } -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) { string resultTypeName = $"Dictionary()}?>?"; @@ -803,116 +351,6 @@ this.Write(").ConfigureAwait(false);"); } -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) { if (templateLine is not null) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/AddConversationMessageTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/AddConversationMessageTemplate.tt index 439f62f8db..d1e7db928c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/AddConversationMessageTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/AddConversationMessageTemplate.tt @@ -1,8 +1,12 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> - +<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.Extensions" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateRecordExpressionTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateStringExpressionTemplate.tt" once="true" #> +<#@ include file="Snippets/FormatMessageTemplate.tt" once="true" #> /// /// Adds a new message to the specified agent conversation /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ClearAllVariablesTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ClearAllVariablesTemplate.cs index ea534767bc..0d87f1c739 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ClearAllVariablesTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ClearAllVariablesTemplate.cs @@ -9,11 +9,8 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; - using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; using System.Collections.Generic; + using Microsoft.Agents.ObjectModel; using System; /// @@ -27,17 +24,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -87,85 +73,6 @@ this.Write("\n "); } -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateEnumExpression( EnumExpression expression, string targetVariable, @@ -310,601 +217,5 @@ this.Write(").ConfigureAwait(false);"); } } - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ClearAllVariablesTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ClearAllVariablesTemplate.tt index fbac67edf2..b86d189ddb 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ClearAllVariablesTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ClearAllVariablesTemplate.tt @@ -1,7 +1,10 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateEnumExpressionTemplate.tt" once="true" #> /// /// Reset all the state for the targeted variable scope. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ConditionGroupTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ConditionGroupTemplate.cs index 8f330d9afd..eabaff77d8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ConditionGroupTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ConditionGroupTemplate.cs @@ -9,11 +9,8 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,17 +24,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -182,746 +168,5 @@ this.Write(").ConfigureAwait(false);"); } } - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ConditionGroupTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ConditionGroupTemplate.tt index d91cadc4d2..37ec8863f1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ConditionGroupTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ConditionGroupTemplate.tt @@ -1,7 +1,10 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.ObjectModel" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateBoolExpressionTemplate.tt" once="true" #> /// /// Conditional branching similar to an if / elseif / elseif / else chain. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CopyConversationMessagesTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CopyConversationMessagesTemplate.cs index 9826408215..c29b7ed714 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CopyConversationMessagesTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CopyConversationMessagesTemplate.cs @@ -1,7 +1,7 @@ // ------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version: 17.0.0.0 +// Runtime Version: 18.0.0.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -9,17 +9,14 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// /// Class to produce the template output /// - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")] internal partial class CopyConversationMessagesTemplate : ActionTemplate { /// @@ -27,16 +24,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -103,554 +90,6 @@ this.Write("\n "); } -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) { string typeName = isNullable ? "string?" : "string"; @@ -881,46 +320,5 @@ this.Write(").ConfigureAwait(false);"); } } - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CopyConversationMessagesTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CopyConversationMessagesTemplate.tt index 8014af3f8b..4a27eb40cb 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CopyConversationMessagesTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CopyConversationMessagesTemplate.tt @@ -1,7 +1,11 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ import namespace="Microsoft.Extensions.AI" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateStringExpressionTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateValueExpressionTemplate.tt" once="true" #> /// /// Copies one or more messages into the specified agent conversation. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CreateConversationTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CreateConversationTemplate.cs index f9f0c52939..1a27946e42 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CreateConversationTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CreateConversationTemplate.cs @@ -1,7 +1,7 @@ // ------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version: 17.0.0.0 +// Runtime Version: 18.0.0.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -9,17 +9,13 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// /// Class to produce the template output /// - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")] internal partial class CreateConversationTemplate : ActionTemplate { /// @@ -27,19 +23,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -91,825 +74,5 @@ this.Write("\n "); } } - -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CreateConversationTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CreateConversationTemplate.tt index 859f25a56a..902bcc2ff4 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CreateConversationTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/CreateConversationTemplate.tt @@ -1,7 +1,8 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> /// /// Creates a new conversation and stores the identifier value to the "<#= this.Model.ConversationId #>" variable. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/EditTableV2Template.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/EditTableV2Template.cs index 56a1048946..a05ddba863 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/EditTableV2Template.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/EditTableV2Template.cs @@ -9,11 +9,6 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; - using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,21 +22,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n/// \n/// Modify items in a list\n/// \ninternal sealed class "); @@ -53,853 +33,5 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen " {\n return default;\n }\n}"); return this.GenerationEnvironment.ToString(); } - -void AssignVariable(PropertyPath targetVariable, string valueVariable, bool tightFormat = false) -{ - if (targetVariable is not null) - { -this.Write("\n await context.QueueStateUpdateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(VariableName(targetVariable))); - -this.Write("\", value: "); - -this.Write(this.ToStringHelper.ToStringWithCulture(valueVariable)); - -this.Write(", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(VariableScope(targetVariable))); - -this.Write("\").ConfigureAwait(false);"); - - - if (!tightFormat) - { -this.Write("\n "); - -} - } -} - - -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/EditTableV2Template.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/EditTableV2Template.tt index a39630ac5b..e783089c51 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/EditTableV2Template.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/EditTableV2Template.tt @@ -1,7 +1,6 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> /// /// Modify items in a list /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ForeachTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ForeachTemplate.cs index 2815bb4bdc..ba15f9d0c7 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ForeachTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ForeachTemplate.cs @@ -9,11 +9,7 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,18 +23,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -143,675 +127,6 @@ this.Write("\n "); } -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateValueExpression(ValueExpression expression, string targetVariable) => EvaluateValueExpression(expression, targetVariable); @@ -921,46 +236,5 @@ this.Write(").ConfigureAwait(false);"); } } - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ForeachTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ForeachTemplate.tt index 5e77d26b05..77f13c5184 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ForeachTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ForeachTemplate.tt @@ -1,7 +1,9 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateValueExpressionTemplate.tt" once="true" #> /// /// Loops over a list assignign the loop variable to "<#= this.Model.Value #>" variable. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.cs index 2f4bbf63d5..3cb2642f79 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.cs @@ -1,7 +1,7 @@ // ------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version: 17.0.0.0 +// Runtime Version: 18.0.0.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -9,17 +9,16 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { + using System.Collections.Generic; using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// /// Class to produce the template output /// - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")] internal partial class InvokeAzureAgentTemplate : ActionTemplate { /// @@ -37,13 +36,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen this.Write("\n"); this.Write("\n"); this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n/// \n/// Invokes an agent to process messages and return a response wit" + "hin a conversation context.\n/// \ninternal sealed class "); this.Write(this.ToStringHelper.ToStringWithCulture(this.Name)); @@ -191,259 +183,6 @@ this.Write(").ConfigureAwait(false);"); } -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateListExpression(ValueExpression expression, string targetVariable) { string typeName = GetTypeAlias(); @@ -552,114 +291,6 @@ this.Write(").ConfigureAwait(false);"); } -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) { string typeName = isNullable ? "string?" : "string"; @@ -780,156 +411,5 @@ this.Write(").ConfigureAwait(false);"); } } - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.tt index b4ca34174d..e38068260a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/InvokeAzureAgentTemplate.tt @@ -1,7 +1,14 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.Extensions" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ import namespace="Microsoft.Extensions.AI" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateBoolExpressionTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateListExpressionTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateStringExpressionTemplate.tt" once="true" #> /// /// Invokes an agent to process messages and return a response within a conversation context. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ParseValueTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ParseValueTemplate.cs index a2415bea8f..4572f39973 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ParseValueTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ParseValueTemplate.cs @@ -9,11 +9,7 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,19 +23,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -112,825 +95,5 @@ this.Write("\n "); } } - -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ParseValueTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ParseValueTemplate.tt index 5a7c073293..246e1d331b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ParseValueTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ParseValueTemplate.tt @@ -1,7 +1,8 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> /// /// Parses a string or untyped value to the provided data type. When the input is a string, it will be treated as JSON. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/QuestionTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/QuestionTemplate.cs index cb3cfa89ad..61afa0309a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/QuestionTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/QuestionTemplate.cs @@ -9,11 +9,6 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; - using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,21 +22,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n/// \n/// Request input.\n/// \ninternal sealed class "); @@ -53,853 +33,5 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen " {\n return default;\n }\n}"); return this.GenerationEnvironment.ToString(); } - -void AssignVariable(PropertyPath targetVariable, string valueVariable, bool tightFormat = false) -{ - if (targetVariable is not null) - { -this.Write("\n await context.QueueStateUpdateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(VariableName(targetVariable))); - -this.Write("\", value: "); - -this.Write(this.ToStringHelper.ToStringWithCulture(valueVariable)); - -this.Write(", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(VariableScope(targetVariable))); - -this.Write("\").ConfigureAwait(false);"); - - - if (!tightFormat) - { -this.Write("\n "); - -} - } -} - - -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/QuestionTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/QuestionTemplate.tt index 5b98d30c9a..7d3a15af6b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/QuestionTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/QuestionTemplate.tt @@ -1,7 +1,6 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> /// /// Request input. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ResetVariableTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ResetVariableTemplate.cs index 597e567d5c..bd56580a7b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ResetVariableTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ResetVariableTemplate.cs @@ -9,11 +9,7 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,19 +23,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -87,825 +70,5 @@ this.Write("\n "); } } - -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ResetVariableTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ResetVariableTemplate.tt index 80eb9b4323..0565a2328b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ResetVariableTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/ResetVariableTemplate.tt @@ -1,7 +1,8 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> /// /// Resets the value of the "<#= this.Model.Variable #>" variable, potentially causing re-evaluation /// of the default value, question or action that provides the value to this variable. diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessageTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessageTemplate.cs index a2a679e688..8a3a860914 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessageTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessageTemplate.cs @@ -9,11 +9,7 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,17 +23,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -93,446 +78,6 @@ this.Write("\n "); } -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) { string resultTypeName = $"Dictionary()}?>?"; @@ -761,156 +306,5 @@ this.Write(").ConfigureAwait(false);"); } } - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessageTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessageTemplate.tt index e2e3754bda..42ec61ad27 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessageTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessageTemplate.tt @@ -1,7 +1,10 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateRecordExpressionTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateStringExpressionTemplate.tt" once="true" #> /// /// Retrieves a list of messages from an agent conversation. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessagesTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessagesTemplate.cs index 08fb0a75ec..1a740ed76c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessagesTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessagesTemplate.cs @@ -1,7 +1,7 @@ // ------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version: 17.0.0.0 +// Runtime Version: 18.0.0.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -9,17 +9,15 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; - using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; using System.Collections.Generic; + using Microsoft.Agents.AI.Workflows.Declarative.Extensions; + using Microsoft.Agents.ObjectModel; using System; /// /// Class to produce the template output /// - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")] internal partial class RetrieveConversationMessagesTemplate : ActionTemplate { /// @@ -37,13 +35,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen this.Write("\n"); this.Write("\n"); this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n/// \n/// Retrieves a specific message from an agent conversation.\n/// <" + "/summary>\ninternal sealed class "); this.Write(this.ToStringHelper.ToStringWithCulture(this.Name)); @@ -108,85 +99,6 @@ this.Write("\n "); } -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateEnumExpression( EnumExpression expression, string targetVariable, @@ -440,114 +352,6 @@ this.Write(").ConfigureAwait(false);"); } -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) { string resultTypeName = $"Dictionary()}?>?"; @@ -776,156 +580,5 @@ this.Write(").ConfigureAwait(false);"); } } - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessagesTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessagesTemplate.tt index 96aec79282..1b035d674b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessagesTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/RetrieveConversationMessagesTemplate.tt @@ -1,7 +1,14 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.Extensions" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateEnumExpressionTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateIntExpressionTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateRecordExpressionTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateStringExpressionTemplate.tt" once="true" #> /// /// Retrieves a specific message from an agent conversation. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SendActivityTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SendActivityTemplate.cs index 509127bdf4..cce0854bff 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SendActivityTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SendActivityTemplate.cs @@ -10,10 +10,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,18 +24,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -71,822 +56,15 @@ if (this.Model.Activity is MessageActivityTemplate messageActivity) } - this.Write("\n );\n AgentResponse response = new([new ChatMessage(ChatRole" + - ".Assistant, activityText)]);\n await context.AddEventAsync(new AgentRes" + - "ponseEvent(this.Id, response)).ConfigureAwait(false);"); + this.Write("\n );\n AgentResponse response = new([new ChatMessage(ChatRole.As" + + "sistant, activityText)]);\n await context.AddEventAsync(new AgentResponseE" + + "vent(this.Id, response)).ConfigureAwait(false);"); } this.Write("\n\n return default;\n }\n}"); return this.GenerationEnvironment.ToString(); } -void AssignVariable(PropertyPath targetVariable, string valueVariable, bool tightFormat = false) -{ - if (targetVariable is not null) - { -this.Write("\n await context.QueueStateUpdateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(VariableName(targetVariable))); - -this.Write("\", value: "); - -this.Write(this.ToStringHelper.ToStringWithCulture(valueVariable)); - -this.Write(", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(VariableScope(targetVariable))); - -this.Write("\").ConfigureAwait(false);"); - - - if (!tightFormat) - { -this.Write("\n "); - -} - } -} - - -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) { if (templateLine is not null) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SendActivityTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SendActivityTemplate.tt index f11d2181b4..283ac57e1b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SendActivityTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SendActivityTemplate.tt @@ -1,7 +1,9 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.Extensions" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/FormatMessageTemplate.tt" once="true" #> /// /// Formats a message template and sends an activity event. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetMultipleVariablesTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetMultipleVariablesTemplate.cs index 65e0c71e99..52e86a1f65 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetMultipleVariablesTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetMultipleVariablesTemplate.cs @@ -9,11 +9,7 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,18 +23,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -98,675 +82,6 @@ this.Write("\n "); } -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateValueExpression(ValueExpression expression, string targetVariable) => EvaluateValueExpression(expression, targetVariable); @@ -876,46 +191,5 @@ this.Write(").ConfigureAwait(false);"); } } - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetMultipleVariablesTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetMultipleVariablesTemplate.tt index 3746488c27..44b697eb2f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetMultipleVariablesTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetMultipleVariablesTemplate.tt @@ -1,7 +1,9 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateValueExpressionTemplate.tt" once="true" #> /// /// Assigns an evaluated expression, other variable, or literal value to one or more variables. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetTextVariableTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetTextVariableTemplate.cs index a8ac685649..81a9ee28bf 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetTextVariableTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetTextVariableTemplate.cs @@ -10,10 +10,7 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,17 +24,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -87,785 +73,6 @@ this.Write("\n "); } -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) => - EvaluateValueExpression(expression, targetVariable); - -void EvaluateValueExpression(ValueExpression expression, string targetVariable) -{ - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) { if (templateLine is not null) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetTextVariableTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetTextVariableTemplate.tt index fc5996e20a..1d16ee92ea 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetTextVariableTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetTextVariableTemplate.tt @@ -1,7 +1,10 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.Extensions" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/FormatMessageTemplate.tt" once="true" #> /// /// Assigns an evaluated message template to the "<#= this.Model.Variable #>" variable. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetVariableTemplate.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetVariableTemplate.cs index 5d2e08b3e4..972fc1e5c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetVariableTemplate.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetVariableTemplate.cs @@ -9,11 +9,7 @@ // ------------------------------------------------------------------------------ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen { - using Microsoft.Agents.AI.Workflows.Declarative.Extensions; - using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; - using Microsoft.Extensions.AI; - using System.Collections.Generic; using System; /// @@ -27,18 +23,6 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.CodeGen /// public override string TransformText() { - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); - this.Write("\n"); this.Write("\n"); this.Write("\n"); this.Write("\n"); @@ -89,675 +73,6 @@ this.Write("\n "); } -void EvaluateBoolExpression(BoolExpression expression, string targetVariable, bool defaultValue = false) -{ - if (expression is null) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatBoolValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync>("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n bool "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateEnumExpression( - EnumExpression expression, - string targetVariable, - IDictionary resultMap, - string defaultValue = null, - bool qualifyResult = false, - bool isNullable = false) - where TWrapper : EnumWrapper -{ - string resultType = $"{GetTypeAlias()}{(isNullable ? "?" : "")}"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(defaultValue))); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - resultMap.TryGetValue(expression.LiteralValue, out string resultValue); - if (qualifyResult) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write("."); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultValue)); - -this.Write(";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatValue(resultValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultType)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateIntExpression(IntExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "int?" : "int"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "0")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateListExpression(ValueExpression expression, string targetVariable) -{ - string typeName = GetTypeAlias(); - if (expression is null) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(GetTypeAlias())); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write("> = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n IList<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateListAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateRecordExpression(ObjectExpression expression, string targetVariable) -{ - string resultTypeName = $"Dictionary()}?>?"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = null;"); - - - } - else if (expression.IsLiteral) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" =\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatDataValue(expression.LiteralValue))); - -this.Write(";"); - - - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write("? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateExpressionAsync<"); - -this.Write(this.ToStringHelper.ToStringWithCulture(resultTypeName)); - -this.Write(">("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - -void EvaluateStringExpression(StringExpression expression, string targetVariable, bool isNullable = false) -{ - string typeName = isNullable ? "string?" : "string"; - if (expression is null) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(isNullable ? "null" : "string.Empty")); - -this.Write(";"); - - - } - else if (expression.IsLiteral) - { - if (expression.LiteralValue.Contains("\n")) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = \n \"\"\"\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.LiteralValue)); - -this.Write("\n \"\"\";"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = "); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.LiteralValue))); - -this.Write(";"); - - - } - } - else if (expression.IsVariableReference && expression.VariableReference.SegmentCount == 2) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.ReadStateAsync(key: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.VariableName)); - -this.Write("\", scopeName: \""); - -this.Write(this.ToStringHelper.ToStringWithCulture(expression.VariableReference.NamespaceAlias)); - -this.Write("\").ConfigureAwait(false);"); - - - } - else if (expression.IsVariableReference) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.VariableReference.ToString()))); - -this.Write(").ConfigureAwait(false);"); - - - } - else - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(typeName)); - -this.Write(" "); - -this.Write(this.ToStringHelper.ToStringWithCulture(targetVariable)); - -this.Write(" = await context.EvaluateValueAsync("); - -this.Write(this.ToStringHelper.ToStringWithCulture(FormatStringValue(expression.ExpressionText))); - -this.Write(").ConfigureAwait(false);"); - - - } -} - - void EvaluateValueExpression(ValueExpression expression, string targetVariable) => EvaluateValueExpression(expression, targetVariable); @@ -867,46 +182,5 @@ this.Write(").ConfigureAwait(false);"); } } - -void EvaluateMessageTemplate(TemplateLine templateLine, string variableName) -{ - if (templateLine is not null) - { -this.Write("\n string "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" =\n await context.FormatTemplateAsync(\n \"\"\""); - - - FormatMessageTemplate(templateLine); -this.Write("\n \"\"\");"); - - - } - else - { -this.Write("\n string? "); - -this.Write(this.ToStringHelper.ToStringWithCulture(variableName)); - -this.Write(" = null;"); - - - } -} - -void FormatMessageTemplate(TemplateLine line) -{ - foreach (string text in line.ToTemplateString().ByLine()) - { -this.Write("\n "); - -this.Write(this.ToStringHelper.ToStringWithCulture(text)); - - - } -} - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetVariableTemplate.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetVariableTemplate.tt index 42e12c1f99..2adbb1922c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetVariableTemplate.tt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/SetVariableTemplate.tt @@ -1,7 +1,9 @@ <#@ template language="C#" inherits="ActionTemplate" visibility="internal" linePragmas="false" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #> -<#@ include file="Snippets/Index.tt" once="true" #> +<#@ import namespace="Microsoft.Agents.ObjectModel" #> +<#@ include file="Snippets/AssignVariableTemplate.tt" once="true" #> +<#@ include file="Snippets/EvaluateValueExpressionTemplate.tt" once="true" #> /// /// Assigns an evaluated expression, other variable, or literal value to the "<#= this.Model.Variable #>" variable. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/Snippets/Index.tt b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/Snippets/Index.tt deleted file mode 100644 index 7b4e68deb3..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/CodeGen/Snippets/Index.tt +++ /dev/null @@ -1,14 +0,0 @@ -<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.Extensions" #> -<#@ import namespace="Microsoft.Agents.AI.Workflows.Declarative.ObjectModel" #> -<#@ import namespace="Microsoft.Agents.ObjectModel" #> -<#@ import namespace="Microsoft.Extensions.AI" #> -<#@ import namespace="System.Collections.Generic" #> -<#@ include file="AssignVariableTemplate.tt" once="true" #> -<#@ include file="EvaluateBoolExpressionTemplate.tt" once="true" #> -<#@ include file="EvaluateEnumExpressionTemplate.tt" once="true" #> -<#@ include file="EvaluateIntExpressionTemplate.tt" once="true" #> -<#@ include file="EvaluateListExpressionTemplate.tt" once="true" #> -<#@ include file="EvaluateRecordExpressionTemplate.tt" once="true" #> -<#@ include file="EvaluateStringExpressionTemplate.tt" once="true" #> -<#@ include file="EvaluateValueExpressionTemplate.tt" once="true" #> -<#@ include file="FormatMessageTemplate.tt" once="true" #> diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs index e1514f2240..30c887ffe2 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs @@ -17,7 +17,9 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { + Throw.IfNull(this.Model.Message); Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); + string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value; bool isWorkflowConversation = context.IsWorkflowConversation(conversationId, out string? _); @@ -26,7 +28,7 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode // Capture the created message, which includes the assigned ID. newMessage = await agentProvider.CreateMessageAsync(conversationId, newMessage, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(this.Model.Message?.Path, newMessage.ToRecord(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Message.Path, newMessage.ToRecord(), context).ConfigureAwait(false); if (isWorkflowConversation) { diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs index 834497bbce..a0171488e0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs @@ -23,7 +23,7 @@ internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, Workflo VariablesToClear.ConversationScopedVariables => WorkflowFormulaState.DefaultScopeName, VariablesToClear.ConversationHistory => null, VariablesToClear.UserScopedVariables => null, - _ => null + _ => null, }; if (scope is not null) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs index 5b1459d323..7bfaed1ab0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs @@ -7,6 +7,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; @@ -15,8 +16,10 @@ internal sealed class CreateConversationExecutor(CreateConversation model, Workf { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { + Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); + string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false); - await this.AssignAsync(this.Model.ConversationId?.Path, FormulaValue.New(conversationId), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ConversationId.Path, FormulaValue.New(conversationId), context).ConfigureAwait(false); await context.QueueConversationUpdateAsync(conversationId, cancellationToken).ConfigureAwait(false); return default; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs index fff160c97b..b06a5ebd36 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs @@ -18,12 +18,12 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { - PropertyPath variablePath = Throw.IfNull(this.Model.ItemsVariable?.Path, $"{nameof(this.Model)}.{nameof(this.Model.ItemsVariable)}"); + Throw.IfNull(this.Model.ItemsVariable, $"{nameof(this.Model)}.{nameof(this.Model.ItemsVariable)}"); - FormulaValue table = context.ReadState(variablePath); + FormulaValue table = context.ReadState(this.Model.ItemsVariable); if (table is not TableValue tableValue) { - throw this.Exception($"Require '{variablePath}' to be a table, not: '{table.GetType().Name}'."); + throw this.Exception($"Require '{this.Model.ItemsVariable.Path}' to be a table, not: '{table.GetType().Name}'."); } EditTableOperation? changeType = this.Model.ChangeType; @@ -33,12 +33,12 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat EvaluationResult expressionResult = this.Evaluator.GetValue(addItemValue); RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), expressionResult.Value.ToFormula()); await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, newRecord, context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, newRecord, context).ConfigureAwait(false); } else if (changeType is ClearItemsOperation) { await tableValue.ClearAsync(cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, FormulaValue.NewBlank(), context).ConfigureAwait(false); } else if (changeType is RemoveItemOperation removeItemOperation) { @@ -46,8 +46,8 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat EvaluationResult expressionResult = this.Evaluator.GetValue(removeItemValue); if (expressionResult.Value.ToFormula() is TableValue removeItemTable) { - await tableValue.RemoveAsync(removeItemTable?.Rows.Select(row => row.Value), all: true, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false); + await tableValue.RemoveAsync(removeItemTable.Rows.Select(row => row.Value), all: true, cancellationToken).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, FormulaValue.NewBlank(), context).ConfigureAwait(false); } } else if (changeType is TakeLastItemOperation) @@ -56,7 +56,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat if (lastRow is not null) { await tableValue.RemoveAsync([lastRow], all: true, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, lastRow, context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, lastRow, context).ConfigureAwait(false); } } else if (changeType is TakeFirstItemOperation) @@ -65,7 +65,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat if (firstRow is not null) { await tableValue.RemoveAsync([firstRow], all: true, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, firstRow, context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, firstRow, context).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs index fa201527eb..57fe319aaf 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs @@ -19,24 +19,18 @@ internal sealed class ParseValueExecutor(ParseValue model, WorkflowFormulaState { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { - PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}"); + Throw.IfNull(this.Model.ValueType, $"{nameof(this.Model)}.{nameof(model.ValueType)}"); + Throw.IfNull(this.Model.Variable, $"{nameof(this.Model)}.{nameof(model.Variable)}"); ValueExpression valueExpression = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}"); EvaluationResult expressionResult = this.Evaluator.GetValue(valueExpression); FormulaValue parsedValue; - if (this.Model.ValueType is not null) - { - VariableType targetType = new(this.Model.ValueType); - object? parsedResult = expressionResult.Value.ToObject().ConvertType(targetType); - parsedValue = parsedResult.ToFormula(); - } - else - { - parsedValue = expressionResult.Value.ToFormula(); - } + VariableType targetType = new(this.Model.ValueType); + object? parsedResult = expressionResult.Value.ToObject().ConvertType(targetType); + parsedValue = parsedResult.ToFormula(); - await this.AssignAsync(variablePath, parsedValue, context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Variable.Path, parsedValue, context).ConfigureAwait(false); return default; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs index a2b9ee22af..4c6b4e340b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs @@ -17,6 +17,7 @@ internal sealed class ResetVariableExecutor(ResetVariable model, WorkflowFormula protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { Throw.IfNull(this.Model.Variable, $"{nameof(this.Model)}.{nameof(model.Variable)}"); + await context.QueueStateResetAsync(this.Model.Variable, cancellationToken).ConfigureAwait(false); Debug.WriteLine( $""" diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs index 466e3f2ff4..5ce3869ed1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs @@ -16,13 +16,15 @@ internal sealed class RetrieveConversationMessageExecutor(RetrieveConversationMe { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { + Throw.IfNull(this.Model.Message); Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); + string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value; string messageId = this.Evaluator.GetValue(Throw.IfNull(this.Model.MessageId, $"{nameof(this.Model)}.{nameof(this.Model.MessageId)}")).Value; ChatMessage message = await agentProvider.GetMessageAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(this.Model.Message?.Path, message.ToRecord(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Message.Path, message.ToRecord(), context).ConfigureAwait(false); return default; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs index 8b41fd451b..96c3ceee1a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs @@ -18,11 +18,13 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { + Throw.IfNull(this.Model.Messages); Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); + string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value; List messages = []; - await foreach (var m in agentProvider.GetMessagesAsync( + await foreach (ChatMessage message in agentProvider.GetMessagesAsync( conversationId, limit: this.GetLimit(), after: this.GetMessage(this.Model.MessageAfter), @@ -30,21 +32,16 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM newestFirst: this.IsDescending(), cancellationToken).ConfigureAwait(false)) { - messages.Add(m); + messages.Add(message); } - await this.AssignAsync(this.Model.Messages?.Path, messages.ToTable(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Messages.Path, messages.ToTable(), context).ConfigureAwait(false); return default; } private int? GetLimit() { - if (this.Model.Limit is null) - { - return null; - } - long limit = this.Evaluator.GetValue(this.Model.Limit).Value; return Convert.ToInt32(Math.Min(limit, 100)); } @@ -61,11 +58,6 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM private bool IsDescending() { - if (this.Model.SortOrder is null) - { - return false; - } - AgentMessageSortOrderWrapper sortOrderWrapper = this.Evaluator.GetValue(this.Model.SortOrder).Value; return sortOrderWrapper.Value == AgentMessageSortOrder.NewestFirst; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs index 44cd9baf16..37b8d43e8a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs @@ -7,6 +7,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; @@ -15,16 +16,12 @@ internal sealed class SetTextVariableExecutor(SetTextVariable model, WorkflowFor { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { - if (this.Model.Value is null) - { - await this.AssignAsync(this.Model.Variable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false); - } - else - { - FormulaValue expressionResult = FormulaValue.New(this.Engine.Format(this.Model.Value)); + Throw.IfNull(this.Model.Variable); + Throw.IfNull(this.Model.Value); - await this.AssignAsync(this.Model.Variable?.Path, expressionResult, context).ConfigureAwait(false); - } + FormulaValue expressionResult = FormulaValue.New(this.Engine.Format(this.Model.Value)); + + await this.AssignAsync(this.Model.Variable.Path, expressionResult, context).ConfigureAwait(false); return default; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs index 64d01de0eb..6fd4002df5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs @@ -7,7 +7,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Agents.ObjectModel.Abstractions; -using Microsoft.PowerFx.Types; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; @@ -16,16 +16,12 @@ internal sealed class SetVariableExecutor(SetVariable model, WorkflowFormulaStat { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { - if (this.Model.Value is null) - { - await this.AssignAsync(this.Model.Variable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false); - } - else - { - EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Value); + Throw.IfNull(this.Model.Variable); + Throw.IfNull(this.Model.Value); - await this.AssignAsync(this.Model.Variable?.Path, expressionResult.Value.ToFormula(), context).ConfigureAwait(false); - } + EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Value); + + await this.AssignAsync(this.Model.Variable.Path, expressionResult.Value.ToFormula(), context).ConfigureAwait(false); return default; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs index ff61f1191b..a208e10b2d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs @@ -21,42 +21,51 @@ public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(o { private const string WorkflowWithConversationFileName = "MediaInputConversation.yaml"; private const string WorkflowWithAutoSendFileName = "MediaInputAutoSend.yaml"; - private const string PdfReference = "https://sample-files.com/downloads/documents/pdf/basic-text.pdf"; private const string ImageReference = "https://sample-files.com/downloads/images/jpg/web_optimized_1200x800_97kb.jpg"; + private const string PdfReference = "https://sample-files.com/downloads/documents/pdf/basic-text.pdf"; [Theory] - [InlineData(ImageReference, "image/jpeg", true, Skip = "Failing due to agent service bug.")] - [InlineData(ImageReference, "image/jpeg", false, Skip = "Failing due to agent service bug.")] + [InlineData(ImageReference, "image/jpeg", true)] + [InlineData(ImageReference, "image/jpeg", false)] public async Task ValidateFileUrlAsync(string fileSource, string mediaType, bool useConversation) { - this.Output.WriteLine($"File: {ImageReference}"); + // Arrange + this.Output.WriteLine($"File: {fileSource}"); + + // Act & Assert await this.ValidateFileAsync(new UriContent(fileSource, mediaType), useConversation); } [Theory] [InlineData(ImageReference, "image/jpeg", true)] - [InlineData(ImageReference, "image/jpeg", false, Skip = "Failing due to agent service bug.")] + [InlineData(ImageReference, "image/jpeg", false)] [InlineData(PdfReference, "application/pdf", true)] [InlineData(PdfReference, "application/pdf", false)] public async Task ValidateFileDataAsync(string fileSource, string mediaType, bool useConversation) { + // Arrange byte[] fileData = await DownloadFileAsync(fileSource); string encodedData = Convert.ToBase64String(fileData); string fileUrl = $"data:{mediaType};base64,{encodedData}"; - this.Output.WriteLine($"Content: {fileUrl.Substring(0, 112)}..."); + this.Output.WriteLine($"Content: {fileUrl.Substring(0, Math.Min(112, fileUrl.Length))}..."); + + // Act & Assert await this.ValidateFileAsync(new DataContent(fileUrl), useConversation); } [Theory] - [InlineData(PdfReference, "doc.pdf", true, Skip = "Failing due to agent service bug.")] - [InlineData(PdfReference, "doc.pdf", false, Skip = "Failing due to agent service bug.")] + [InlineData(PdfReference, "doc.pdf", true)] + [InlineData(PdfReference, "doc.pdf", false)] public async Task ValidateFileUploadAsync(string fileSource, string documentName, bool useConversation) { + // Arrange byte[] fileData = await DownloadFileAsync(fileSource); AIProjectClient client = new(this.TestEndpoint, new AzureCliCredential()); using MemoryStream contentStream = new(fileData); OpenAIFileClient fileClient = client.GetProjectOpenAIClient().GetOpenAIFileClient(); OpenAIFile fileInfo = await fileClient.UploadFileAsync(contentStream, documentName, FileUploadPurpose.Assistants); + + // Act & Assert try { this.Output.WriteLine($"File: {fileInfo.Id}"); @@ -77,6 +86,7 @@ public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(o private async Task ValidateFileAsync(AIContent fileContent, bool useConversation) { + // Act AgentProvider agentProvider = AgentProvider.Create(this.Configuration, AgentProvider.Names.Vision); await agentProvider.CreateAgentsAsync().ConfigureAwait(false); @@ -93,6 +103,8 @@ public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(o WorkflowHarness harness = new(workflow, runId: Path.GetFileNameWithoutExtension(workflowFileName)); WorkflowEvents workflowEvents = await harness.RunWorkflowAsync(inputMessage).ConfigureAwait(false); + + // Assert Assert.Equal(useConversation ? 1 : 2, workflowEvents.ConversationEvents.Count); this.Output.WriteLine("CONVERSATION: " + workflowEvents.ConversationEvents[0].ConversationId); AgentResponseEvent agentResponseEvent = Assert.Single(workflowEvents.AgentResponseEvents); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InputArguments.json b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InputArguments.json index f4962e1bc6..02289ddfa7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InputArguments.json +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InputArguments.json @@ -10,7 +10,6 @@ "conversation_count": 1, "min_action_count": 1, "min_response_count": 1, - "min_message_count": 2, "actions": { "start": [ "invoke_poem" diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json index 6af29b49c5..68c40219d0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/Marketing.json @@ -10,7 +10,6 @@ "conversation_count": 1, "min_action_count": 3, "min_response_count": 3, - "min_message_count": 6, "actions": { "start": [ "invoke_analyst", diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/MockAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/MockAgentProvider.cs index 5a55dd297a..1c496c3936 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/MockAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/MockAgentProvider.cs @@ -17,7 +17,7 @@ internal sealed class MockAgentProvider : Mock { public IList ExistingConversationIds { get; } = []; - public List? TestMessages { get; set; } + public List TestMessages { get; set; } = []; public MockAgentProvider() { @@ -45,7 +45,7 @@ internal sealed class MockAgentProvider : Mock It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(testMessages.First())); + .Returns((conversationId, message, cancellationToken) => Task.FromResult(this.CaptureChatMessage(message))); } private string CreateConversationId() @@ -56,6 +56,13 @@ internal sealed class MockAgentProvider : Mock return newConversationId; } + private ChatMessage CaptureChatMessage(ChatMessage message) + { + this.TestMessages.Add(message); + + return message; + } + private List CreateMessages() { // Create test messages diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs index b5cdeadaeb..a7f2ba48f6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; +using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; +using Microsoft.PowerFx.Types; using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; @@ -28,20 +31,64 @@ public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) messageText: $"Hello from {role}"); } + [Theory] + [InlineData(AgentMessageRole.User)] + [InlineData(AgentMessageRole.Agent)] + public async Task AddMessageToWorkflowAsync(AgentMessageRole role) + { + // Arrange + this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("WorkflowConversationId"), VariableScopeNames.System); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(AddMessageToWorkflowAsync), + variableName: "TestMessage", + role: AgentMessageRoleWrapper.Get(role), + conversationId: "WorkflowConversationId", + messageText: $"Hello from {role}"); + } + + [Theory] + [InlineData(AgentMessageRole.User)] + [InlineData(AgentMessageRole.Agent)] + public async Task AddMessageWithMetadataAsync(AgentMessageRole role) + { + // Arrange + Dictionary metadataValues = + new() + { + ["Key1"] = "Value1", + ["Key2"] = "Value2", + }; + RecordDataValue metadataRecord = metadataValues.ToRecordValue(); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(AddMessageWithMetadataAsync), + variableName: "TestMessage", + role: AgentMessageRoleWrapper.Get(role), + messageText: $"Hello from {role}", + metadata: metadataRecord); + } + private async Task ExecuteTestAsync( string displayName, string variableName, AgentMessageRoleWrapper role, - string messageText) + string messageText, + string? conversationId = null, + RecordDataValue? metadata = null) { // Arrange MockAgentProvider mockAgentProvider = new(); - AddConversationMessage model = this.CreateModel( - this.FormatDisplayName(displayName), - FormatVariablePath(variableName), - "TestConversationId", - role, - messageText); + AddConversationMessage model = + this.CreateModel( + this.FormatDisplayName(displayName), + FormatVariablePath(variableName), + conversationId ?? "TestConversationId", + role, + messageText, + metadata); AddConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State); @@ -49,10 +96,15 @@ public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) await this.ExecuteAsync(action); // Assert - ChatMessage? testMessage = mockAgentProvider.TestMessages?.FirstOrDefault(); + ChatMessage? testMessage = mockAgentProvider.TestMessages?.LastOrDefault(); Assert.NotNull(testMessage); VerifyModel(model, action); this.VerifyState(variableName, testMessage.ToRecord()); + if (metadata is not null) + { + Assert.NotNull(testMessage.AdditionalProperties); + Assert.NotEmpty(testMessage.AdditionalProperties); + } } private AddConversationMessage CreateModel( @@ -60,8 +112,15 @@ public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) string messageVariable, string conversationId, AgentMessageRoleWrapper role, - string messageText) + string messageText, + RecordDataValue? metadata) { + ObjectExpression.Builder? metadataExpression = null; + if (metadata is not null) + { + metadataExpression = ObjectExpression.Literal(metadata).ToBuilder(); + } + AddConversationMessage.Builder actionBuilder = new() { @@ -70,6 +129,7 @@ public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) Message = PropertyPath.Create(messageVariable), ConversationId = StringExpression.Literal(conversationId), Role = role, + Metadata = metadataExpression, }; actionBuilder.Content.Add(new AddConversationMessageContent.Builder diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs index 5ccea000e1..70e4ac0a02 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs @@ -13,47 +13,91 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; /// public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output) { + [Fact] + public async Task ClearGlobalScopeAsync() + { + // Arrange + this.State.Set("GlobalVar", FormulaValue.New("Old value"), VariableScopeNames.Global); + + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ClearGlobalScopeAsync)), + VariablesToClear.AllGlobalVariables, + "GlobalVar", + VariableScopeNames.Global); + } + [Fact] public async Task ClearWorkflowScopeAsync() { // Arrange - this.State.Set("NoVar", FormulaValue.New("Old value")); + this.State.Set("LocalVar", FormulaValue.New("Old value")); + + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ClearWorkflowScopeAsync)), + VariablesToClear.ConversationScopedVariables, + "LocalVar"); + } + + [Fact] + public async Task ClearUserScopeAsync() + { + // Arrange + this.State.Set("LocalVar", FormulaValue.New("Old value")); + + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ClearUserScopeAsync)), + VariablesToClear.UserScopedVariables, + "LocalVar", + expectedValue: FormulaValue.New("Old value")); + } + + [Fact] + public async Task ClearWorkflowHistoryAsync() + { + // Arrange + this.State.Set("LocalVar", FormulaValue.New("Old value")); + + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ClearWorkflowHistoryAsync)), + VariablesToClear.ConversationHistory, + "LocalVar", + expectedValue: FormulaValue.New("Old value")); + } + + private async Task ExecuteTestAsync( + string displayName, + VariablesToClear scope, + string variableName, + string variableScope = VariableScopeNames.Local, + FormulaValue? expectedValue = null) + { + // Arrange + ClearAllVariables model = this.CreateModel( + this.FormatDisplayName(displayName), + scope); + + ClearAllVariablesExecutor action = new(model, this.State); + this.State.Bind(); - ClearAllVariables model = - this.CreateModel( - this.FormatDisplayName(nameof(ClearWorkflowScopeAsync)), - VariablesToClear.ConversationScopedVariables); - // Act - ClearAllVariablesExecutor action = new(model, this.State); await this.ExecuteAsync(action); // Assert VerifyModel(model, action); this.VerifyUndefined("NoVar"); - } - - [Fact] - public async Task ClearUndefinedScopeAsync() - { - // Arrange - this.State.Set("NoVar", FormulaValue.New("Old value")); - this.State.Bind(); - - // Arrange - ClearAllVariables model = - this.CreateModel( - this.FormatDisplayName(nameof(ClearUndefinedScopeAsync)), - VariablesToClear.UserScopedVariables); - - // Act - ClearAllVariablesExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("NoVar", FormulaValue.New("Old value")); + if (expectedValue is null) + { + this.VerifyUndefined(variableName, variableScope); + } + else + { + this.VerifyState(variableName, variableScope, expectedValue); + } } private ClearAllVariables CreateModel(string displayName, VariablesToClear variableTarget) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs new file mode 100644 index 0000000000..5eb723ae0e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs @@ -0,0 +1,345 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; +using Microsoft.Agents.ObjectModel; +using Microsoft.PowerFx.Types; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; + +/// +/// Tests for . +/// +public sealed class EditTableV2ExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output) +{ + [Fact] + public void InvalidModelNullItemsVariable() + { + // Arrange + EditTableV2 model = new EditTableV2.Builder + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(nameof(InvalidModelNullItemsVariable)), + ItemsVariable = null, + ChangeType = new AddItemOperation.Builder + { + Value = new ValueExpression.Builder(ValueExpression.Literal(new StringDataValue("test"))) + }.Build() + }.Build(); + + // Act, Assert + DeclarativeModelException exception = Assert.Throws(() => new EditTableV2Executor(model, this.State)); + Assert.Contains("required", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task InvalidModelVariableNotTableAsync() + { + // Arrange + this.State.Set("NotATable", FormulaValue.New("I am a string")); + + EditTableV2 model = this.CreateModel( + nameof(InvalidModelVariableNotTableAsync), + "NotATable", + new AddItemOperation.Builder + { + Value = new ValueExpression.Builder(ValueExpression.Literal(new StringDataValue("test"))) + }.Build()); + + EditTableV2Executor action = new(model, this.State); + + // Act & Assert + await Assert.ThrowsAsync(async () => await this.ExecuteAsync(action)); + } + + [Fact] + public async Task InvalidModelAddItemOperationNullValueAsync() + { + // Arrange + EditTableV2 model = new EditTableV2.Builder + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(nameof(InvalidModelAddItemOperationNullValueAsync)), + ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")), + ChangeType = new AddItemOperation.Builder + { + Value = null + }.Build() + }.Build(); + + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Act, Assert + EditTableV2Executor action = new(model, this.State); + await Assert.ThrowsAsync(async () => await this.ExecuteAsync(action)); + } + + [Fact] + public async Task InvalidModelRemoveItemOperationNullValueAsync() + { + // Arrange + EditTableV2 model = new EditTableV2.Builder + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(nameof(InvalidModelRemoveItemOperationNullValueAsync)), + ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")), + ChangeType = new RemoveItemOperation.Builder + { + Value = null + }.Build() + }.Build(); + + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Act, Assert + EditTableV2Executor action = new(model, this.State); + await Assert.ThrowsAsync(async () => await this.ExecuteAsync(action)); + } + + [Fact] + public async Task RemoveItemOperationNonTableValueAsync() + { + // Arrange + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1"))); + TableValue tableValue = FormulaValue.NewTable(recordType, record1); + this.State.Set("TestTable", tableValue); + + // Set a string value instead of a table for removal + this.State.Set("RemoveItems", FormulaValue.New("NotATable")); + + EditTableV2 model = new EditTableV2.Builder + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(nameof(RemoveItemOperationNonTableValueAsync)), + ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")), + ChangeType = new RemoveItemOperation.Builder + { + Value = new ValueExpression.Builder(ValueExpression.Variable(PropertyPath.TopicVariable("RemoveItems"))) + }.Build() + }.Build(); + + // Act + EditTableV2Executor action = new(model, this.State); + await this.ExecuteAsync(action); + + // Assert: When the remove value is not a table, no removal occurs, so the table should be unchanged + FormulaValue value = this.State.Get("TestTable"); + Assert.IsAssignableFrom(value); + TableValue resultTable = (TableValue)value; + Assert.Single(resultTable.Rows); + } + + [Fact] + public async Task AddItemOperationWithSingleFieldRecordAsync() + { + // Arrange: Create an empty table with single field + RecordType recordType = RecordType.Empty().Add("Name", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Arrange, Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(AddItemOperationWithSingleFieldRecordAsync), + variableName: "TestTable", + changeType: this.CreateAddItemOperation(new RecordDataValue.Builder + { + Properties = + { + ["Name"] = new StringDataValue("John") + } + }.Build()), + verifyAction: (variableName, recordValue) => + Assert.Equal("John", recordValue.GetField("Name").ToObject()) + ); + } + + [Fact] + public async Task AddItemOperationWithScalarValueAsync() + { + // Arrange: Create an empty table with single field + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(AddItemOperationWithScalarValueAsync), + variableName: "TestTable", + changeType: this.CreateAddItemOperation(new StringDataValue("TestValue")), + verifyAction: (variableName, recordValue) => + Assert.Equal("TestValue", recordValue.GetField("Value").ToObject()) + ); + } + + [Fact] + public async Task ClearItemsOperationAsync() + { + // Arrange: Create a table with some items + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1"))); + RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2"))); + TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2); + this.State.Set("TestTable", tableValue); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(ClearItemsOperationAsync), + variableName: "TestTable", + changeType: new ClearItemsOperation.Builder().Build()); + } + + [Fact] + public async Task RemoveItemOperationAsync() + { + // Arrange: Create a table with some items + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1"))); + RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2"))); + TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2); + this.State.Set("TestTable", tableValue); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(RemoveItemOperationAsync), + variableName: "TestTable", + changeType: this.CreateRemoveItemOperation("Item1")); + } + + [Fact] + public async Task TakeLastItemOperationWithItemsAsync() + { + // Arrange: Create a table with some items + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1"))); + RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2"))); + RecordValue record3 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item3"))); + TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2, record3); + this.State.Set("TestTable", tableValue); + + // Arrange, Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(TakeLastItemOperationWithItemsAsync), + variableName: "TestTable", + changeType: new TakeLastItemOperation.Builder().Build(), + verifyAction: (variableName, recordValue) => + Assert.Equal("Item3", recordValue.GetField("Value").ToObject()) + ); + } + + [Fact] + public async Task TakeLastItemOperationEmptyTableAsync() + { + // Arrange: Create an empty table + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Arrange, Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(TakeLastItemOperationEmptyTableAsync), + variableName: "TestTable", + changeType: new TakeLastItemOperation.Builder().Build()); + } + + [Fact] + public async Task TakeFirstItemOperationWithItemsAsync() + { + // Arrange: Create a table with some items + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1"))); + RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2"))); + RecordValue record3 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item3"))); + TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2, record3); + this.State.Set("TestTable", tableValue); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(TakeFirstItemOperationWithItemsAsync), + variableName: "TestTable", + changeType: new TakeFirstItemOperation.Builder().Build(), + verifyAction: (variableName, recordValue) => + Assert.Equal("Item1", recordValue.GetField("Value").ToObject()) + ); + } + + [Fact] + public async Task TakeFirstItemOperationEmptyTableAsync() + { + // Arrange: Create an empty table + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(TakeFirstItemOperationEmptyTableAsync), + variableName: "TestTable", + changeType: new TakeFirstItemOperation.Builder().Build()); + } + + private async Task ExecuteTestAsync( + string displayName, + string variableName, + EditTableOperation changeType, + Action? verifyAction = null) where TValue : FormulaValue + { + // Arrange + EditTableV2 model = this.CreateModel(displayName, variableName, changeType); + + EditTableV2Executor action = new(model, this.State); + + // Act + await this.ExecuteAsync(action); + + // Assert + VerifyModel(model, action); + FormulaValue value = this.State.Get(variableName); + TValue typedValue = Assert.IsAssignableFrom(value); + verifyAction?.Invoke(variableName, typedValue); + } + + private EditTableV2 CreateModel(string displayName, string variableName, EditTableOperation changeType) + { + EditTableV2.Builder actionBuilder = new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + ItemsVariable = PropertyPath.Create(FormatVariablePath(variableName)), + ChangeType = changeType + }; + + return AssignParent(actionBuilder); + } + + private AddItemOperation CreateAddItemOperation(DataValue value) + { + return new AddItemOperation.Builder + { + Value = new ValueExpression.Builder(ValueExpression.Literal(value)) + }.Build(); + } + + private RemoveItemOperation CreateRemoveItemOperation(string itemValue) + { + // Create a table with the item to remove + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue recordToRemove = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New(itemValue))); + TableValue tableToRemove = FormulaValue.NewTable(recordType, recordToRemove); + + // Store in state for expression evaluation + this.State.Set("RemoveItems", tableToRemove); + this.State.Bind(); + + return new RemoveItemOperation.Builder + { + Value = new ValueExpression.Builder(ValueExpression.Variable(PropertyPath.TopicVariable("RemoveItems"))) + }.Build(); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs index 0591066471..22854c90e8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs @@ -25,95 +25,71 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA {"key1", new PropertyInfo.Builder() { Type = DataType.String } }, } }; - ParseValue model = - this.CreateModel( - this.FormatDisplayName(nameof(ParseRecordAsync)), - recordBuilder, - @"{ ""key1"": ""val1"" }"); - // Act - ParseValueExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("Target", FormulaValue.NewRecordFromFields(new NamedValue("key1", FormulaValue.New("val1")))); + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ParseRecordAsync)), + recordBuilder, + @"{ ""key1"": ""val1"" }", + FormulaValue.NewRecordFromFields(new NamedValue("key1", FormulaValue.New("val1")))); } [Fact] public async Task ParseTableAsync() { - // Arrange - RecordDataType.Builder recordBuilder = - new() - { - Properties = - { - {"key1", new PropertyInfo.Builder() { Type = DataType.String } }, - } - }; - ParseValue model = - this.CreateModel( - this.FormatDisplayName(nameof(ParseTableAsync)), - DataType.EmptyTable, - @"[""apple"",""banana"",""cat""]"); - - // Act - ParseValueExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("Target", FormulaValue.NewSingleColumnTable(FormulaValue.New("apple"), FormulaValue.New("banana"), FormulaValue.New("cat"))); + // Arrange, Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ParseTableAsync)), + DataType.EmptyTable, + @"[""apple"",""banana"",""cat""]", + FormulaValue.NewSingleColumnTable(FormulaValue.New("apple"), FormulaValue.New("banana"), FormulaValue.New("cat"))); } [Fact] public async Task ParseBooleanAsync() { - // Arrange - ParseValue model = - this.CreateModel( - this.FormatDisplayName(nameof(ParseTableAsync)), - new BooleanDataType.Builder(), - "True"); - - // Act - ParseValueExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("Target", FormulaValue.New(true)); + // Arrange, Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ParseBooleanAsync)), + new BooleanDataType.Builder(), + "True", + FormulaValue.New(true)); } [Fact] public async Task ParseNumberAsync() { - // Arrange - ParseValue model = - this.CreateModel( - this.FormatDisplayName(nameof(ParseNumberAsync)), - new NumberDataType.Builder(), - "42"); - - // Act - ParseValueExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("Target", FormulaValue.New(42)); + // Arrange, Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ParseNumberAsync)), + new NumberDataType.Builder(), + "42", + FormulaValue.New(42)); } [Fact] public async Task ParseStringAsync() { - // Arrange + // Arrange, Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ParseStringAsync)), + new StringDataType.Builder(), + "Hello, World!", + FormulaValue.New("Hello, World!")); + } + + private async Task ExecuteTestAsync( + string displayName, + DataType.Builder dataBuilder, + string sourceText, + FormulaValue expectedValue) + { ParseValue model = this.CreateModel( - this.FormatDisplayName(nameof(ParseStringAsync)), - new StringDataType.Builder(), - "Hello, World!"); + displayName, + "Target", + dataBuilder, + sourceText); // Act ParseValueExecutor action = new(model, this.State); @@ -121,10 +97,14 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA // Assert VerifyModel(model, action); - this.VerifyState("Target", FormulaValue.New("Hello, World!")); + this.VerifyState("Target", expectedValue); } - private ParseValue CreateModel(string displayName, DataType.Builder typeBuilder, string sourceText) + private ParseValue CreateModel( + string displayName, + string variableName, + DataType.Builder typeBuilder, + string sourceText) { ParseValue.Builder actionBuilder = new() @@ -132,7 +112,7 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA Id = this.CreateActionId(), DisplayName = this.FormatDisplayName(displayName), ValueType = typeBuilder, - Variable = PropertyPath.TopicVariable("Target"), + Variable = PropertyPath.TopicVariable(variableName), Value = new ValueExpression.Builder(ValueExpression.Literal(StringDataValue.Create(sourceText))), }; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs index ad77cb602e..9059780751 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs @@ -63,7 +63,7 @@ public sealed class ResetVariableExecutorTest(ITestOutputHelper output) : Workfl { Id = this.CreateActionId(), DisplayName = this.FormatDisplayName(displayName), - Variable = InitializablePropertyPath.Create(variablePath), + Variable = PropertyPath.Create(variablePath), }; return AssignParent(actionBuilder); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs index b25fff59b8..037ee5b94a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; @@ -32,7 +33,6 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : // Arrange this.State.Set("SourceNumber", FormulaValue.New(10)); this.State.Set("SourceText", FormulaValue.New("Hello")); - this.State.Bind(); // Act, Assert await this.ExecuteTestAsync( @@ -50,7 +50,6 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : // Arrange this.State.Set("Source1", FormulaValue.New(123)); this.State.Set("Source2", FormulaValue.New("Reference")); - this.State.Bind(); // Act, Assert await this.ExecuteTestAsync( @@ -74,6 +73,19 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : ]); } + [Fact] + public async Task SetMultipleVariablesWithNullVariableAsync() + { + // Arrange, Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(SetMultipleVariablesWithNullVariableAsync), + assignments: [ + new AssignmentCase("NullVar1", null, FormulaValue.NewBlank()), + new AssignmentCase(null, new StringDataValue("NotNull"), FormulaValue.New("NotNull")), + new AssignmentCase("NullVar2", null, FormulaValue.NewBlank()) + ]); + } + [Fact] public async Task SetMultipleVariablesUpdateExistingAsync() { @@ -116,9 +128,9 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : // Assert VerifyModel(model, action); - foreach (AssignmentCase assignment in assignments) + foreach (AssignmentCase assignment in assignments.Where(a => a.VariableName != null)) { - this.VerifyState(assignment.VariableName, assignment.ExpectedValue); + this.VerifyState(assignment.VariableName!, assignment.ExpectedValue); } } @@ -140,9 +152,15 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : _ => throw new System.ArgumentException($"Unsupported value type: {assignment.ValueExpression?.GetType().Name}") }; + InitializablePropertyPath? variablePath = null; + if (assignment.VariableName != null) + { + variablePath = PropertyPath.Create(FormatVariablePath(assignment.VariableName)); + } + actionBuilder.Assignments.Add(new VariableAssignment.Builder() { - Variable = PropertyPath.Create(FormatVariablePath(assignment.VariableName)), + Variable = variablePath, Value = valueExpressionBuilder, }); } @@ -150,5 +168,5 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : return AssignParent(actionBuilder); } - private sealed record AssignmentCase(string VariableName, object? ValueExpression, FormulaValue ExpectedValue); + private sealed record AssignmentCase(string? VariableName, object? ValueExpression, FormulaValue ExpectedValue); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs index 930e5cb570..0bc850e9ce 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs @@ -16,20 +16,11 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work [Fact] public async Task SetLiteralValueAsync() { - // Arrange - SetTextVariable model = - this.CreateModel( + // Arrange, Act & Assert + await this.ExecuteTestAsync( this.FormatDisplayName(nameof(SetLiteralValueAsync)), - FormatVariablePath("TextVar"), - "Text variable value"); - - // Act - SetTextVariableExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("TextVar", FormulaValue.New("Text variable value")); + "TextVar", + "New value"); } [Fact] @@ -38,11 +29,24 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work // Arrange this.State.Set("TextVar", FormulaValue.New("Old value")); + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(UpdateExistingValueAsync)), + "TextVar", + "New value"); + } + + private async Task ExecuteTestAsync( + string displayName, + string variableName, + string textValue) + { + // Arrange SetTextVariable model = this.CreateModel( - this.FormatDisplayName(nameof(UpdateExistingValueAsync)), - FormatVariablePath("TextVar"), - "New value"); + displayName, + variableName, + textValue); // Act SetTextVariableExecutor action = new(model, this.State); @@ -50,7 +54,7 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work // Assert VerifyModel(model, action); - this.VerifyState("TextVar", FormulaValue.New("New value")); + this.VerifyState(variableName, FormulaValue.New(textValue)); } private SetTextVariable CreateModel(string displayName, string variablePath, string textValue) @@ -60,7 +64,7 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work { Id = this.CreateActionId(), DisplayName = this.FormatDisplayName(displayName), - Variable = InitializablePropertyPath.Create(variablePath), + Variable = PropertyPath.Create(FormatVariablePath(variablePath)), Value = TemplateLine.Parse(textValue), }; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs index f45ccffa78..dddfab6365 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs @@ -92,7 +92,6 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow { // Arrange this.State.Set("Source", FormulaValue.New(true)); - this.State.Bind(); ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); @@ -109,7 +108,6 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow { // Arrange this.State.Set("Source", FormulaValue.New(321)); - this.State.Bind(); ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); @@ -126,7 +124,6 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow { // Arrange this.State.Set("Source", FormulaValue.New("Test")); - this.State.Bind(); ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); @@ -196,7 +193,7 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow { Id = this.CreateActionId(), DisplayName = this.FormatDisplayName(displayName), - Variable = InitializablePropertyPath.Create(variablePath), + Variable = PropertyPath.Create(variablePath), Value = valueExpression, }; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs index 19758aa372..f7c074f1f5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs @@ -27,6 +27,8 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor internal async Task ExecuteAsync(DeclarativeActionExecutor executor) { + this.State.Bind(); + TestWorkflowExecutor workflowExecutor = new(); WorkflowBuilder workflowBuilder = new(workflowExecutor); workflowBuilder.AddEdge(workflowExecutor, executor); diff --git a/python/.github/skills/python-development/SKILL.md b/python/.github/skills/python-development/SKILL.md index 7b119d13d8..c19f273588 100644 --- a/python/.github/skills/python-development/SKILL.md +++ b/python/.github/skills/python-development/SKILL.md @@ -69,7 +69,7 @@ def equal(arg1: str, arg2: str) -> bool: ```python # Core -from agent_framework import ChatAgent, ChatMessage, tool +from agent_framework import ChatAgent, Message, tool # Components from agent_framework.observability import enable_instrumentation @@ -84,10 +84,10 @@ from agent_framework.azure import AzureOpenAIChatClient Define `__all__` in each module. Avoid `from module import *` in `__init__.py` files: ```python -__all__ = ["ChatAgent", "ChatMessage", "ChatResponse"] +__all__ = ["ChatAgent", "Message", "ChatResponse"] from ._agents import ChatAgent -from ._types import ChatMessage, ChatResponse +from ._types import Message, ChatResponse ``` ## Performance Guidelines diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index af64583090..745aabf87b 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -7,6 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0b260210] - 2026-02-10 + +### Added + +- **agent-framework-core**: Add long-running agents and background responses support with `ContinuationToken` TypedDict, `background` option in `OpenAIResponsesOptions`, and continuation token propagation through response types ([#3808](https://github.com/microsoft/agent-framework/pull/3808)) +- **agent-framework-core**: Add streaming support for code interpreter deltas ([#3775](https://github.com/microsoft/agent-framework/pull/3775)) +- **agent-framework-core**: Add explicit input, output, and workflow_output parameters to `@handler`, `@executor` and `request_info` ([#3472](https://github.com/microsoft/agent-framework/pull/3472)) +- **agent-framework-core**: Add explicit schema handling to `@tool` decorator ([#3734](https://github.com/microsoft/agent-framework/pull/3734)) +- **agent-framework-core**: New session and context provider types ([#3763](https://github.com/microsoft/agent-framework/pull/3763)) +- **agent-framework-purview**: Add tests to Purview package ([#3513](https://github.com/microsoft/agent-framework/pull/3513)) + +### Changed + +- **agent-framework-core**: [BREAKING] Renamed core types for simpler API: `ChatAgent` → `Agent`, `RawChatAgent` → `RawAgent`, `ChatMessage` → `Message`, `ChatClientProtocol` → `SupportsChatGetResponse` ([#3747](https://github.com/microsoft/agent-framework/pull/3747)) +- **agent-framework-core**: [BREAKING] Moved to a single `get_response` and `run` API ([#3379](https://github.com/microsoft/agent-framework/pull/3379)) +- **agent-framework-core**: [BREAKING] Merge `send_responses` into `run` method ([#3720](https://github.com/microsoft/agent-framework/pull/3720)) +- **agent-framework-core**: [BREAKING] Renamed `AgentRunContext` to `AgentContext` ([#3714](https://github.com/microsoft/agent-framework/pull/3714)) +- **agent-framework-core**: [BREAKING] Renamed `AgentProtocol` to `SupportsAgentRun` ([#3717](https://github.com/microsoft/agent-framework/pull/3717)) +- **agent-framework-core**: [BREAKING] Renamed next middleware parameter to `call_next` ([#3735](https://github.com/microsoft/agent-framework/pull/3735)) +- **agent-framework-core**: [BREAKING] Standardize TypeVar naming convention (`TName` → `NameT`) ([#3770](https://github.com/microsoft/agent-framework/pull/3770)) +- **agent-framework-core**: [BREAKING] Refactor workflow events to unified discriminated union pattern ([#3690](https://github.com/microsoft/agent-framework/pull/3690)) +- **agent-framework-core**: [BREAKING] Refactor `SharedState` to `State` with sync methods and superstep caching ([#3667](https://github.com/microsoft/agent-framework/pull/3667)) +- **agent-framework-core**: [BREAKING] Move single-config fluent methods to constructor parameters ([#3693](https://github.com/microsoft/agent-framework/pull/3693)) +- **agent-framework-core**: [BREAKING] Types API Review improvements ([#3647](https://github.com/microsoft/agent-framework/pull/3647)) +- **agent-framework-core**: [BREAKING] Fix workflow as agent streaming output ([#3649](https://github.com/microsoft/agent-framework/pull/3649)) +- **agent-framework-orchestrations**: [BREAKING] Move orchestrations to dedicated package ([#3685](https://github.com/microsoft/agent-framework/pull/3685)) +- **agent-framework-core**: [BREAKING] Remove workflow register factory methods; update tests and samples ([#3781](https://github.com/microsoft/agent-framework/pull/3781)) +- **agent-framework-core**: Include sub-workflow structure in graph signature for checkpoint validation ([#3783](https://github.com/microsoft/agent-framework/pull/3783)) +- **agent-framework-core**: Adjust workflows TypeVars from prefix to suffix naming convention ([#3661](https://github.com/microsoft/agent-framework/pull/3661)) +- **agent-framework-purview**: Update CorrelationId ([#3745](https://github.com/microsoft/agent-framework/pull/3745)) +- **agent-framework-anthropic**: Added internal kwargs filtering for Anthropic client ([#3544](https://github.com/microsoft/agent-framework/pull/3544)) +- **agent-framework-github-copilot**: Updated instructions/system_message logic in GitHub Copilot agent ([#3625](https://github.com/microsoft/agent-framework/pull/3625)) +- **agent-framework-mem0**: Disable mem0 telemetry by default ([#3506](https://github.com/microsoft/agent-framework/pull/3506)) + +### Fixed + +- **agent-framework-core**: Fix workflow not pausing when agent calls declaration-only tool ([#3757](https://github.com/microsoft/agent-framework/pull/3757)) +- **agent-framework-core**: Fix GroupChat orchestrator message cleanup issue ([#3712](https://github.com/microsoft/agent-framework/pull/3712)) +- **agent-framework-core**: Fix HandoffBuilder silently dropping `context_provider` during agent cloning ([#3721](https://github.com/microsoft/agent-framework/pull/3721)) +- **agent-framework-core**: Fix subworkflow duplicate request info events ([#3689](https://github.com/microsoft/agent-framework/pull/3689)) +- **agent-framework-core**: Fix workflow cancellation not propagating to active executors ([#3663](https://github.com/microsoft/agent-framework/pull/3663)) +- **agent-framework-core**: Filter `response_format` from MCP tool call kwargs ([#3494](https://github.com/microsoft/agent-framework/pull/3494)) +- **agent-framework-core**: Fix broken Content API imports in Python samples ([#3639](https://github.com/microsoft/agent-framework/pull/3639)) +- **agent-framework-core**: Potential fix for clear-text logging of sensitive information ([#3573](https://github.com/microsoft/agent-framework/pull/3573)) +- **agent-framework-core**: Skip `model_deployment_name` validation for application endpoints ([#3621](https://github.com/microsoft/agent-framework/pull/3621)) +- **agent-framework-azure-ai**: Fix AzureAIClient dropping agent instructions (Responses API) ([#3636](https://github.com/microsoft/agent-framework/pull/3636)) +- **agent-framework-azure-ai**: Fix AzureAIAgentClient dropping agent instructions in sequential workflows ([#3563](https://github.com/microsoft/agent-framework/pull/3563)) +- **agent-framework-ag-ui**: Fix AG-UI message handling and MCP tool double-call bug ([#3635](https://github.com/microsoft/agent-framework/pull/3635)) +- **agent-framework-claude**: Handle API errors in `run_stream()` method ([#3653](https://github.com/microsoft/agent-framework/pull/3653)) +- **agent-framework-claude**: Preserve `$defs` in JSON schema for nested Pydantic models ([#3655](https://github.com/microsoft/agent-framework/pull/3655)) + ## [1.0.0b260130] - 2026-01-30 ### Added @@ -268,7 +319,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **agent-framework-core**: [BREAKING] Support Magentic agent tool call approvals and plan stalling HITL behavior (#2569) -- **agent-framework-core**: [BREAKING] Standardize orchestration outputs as list of `ChatMessage`; allow agent as group chat manager (#2291) +- **agent-framework-core**: [BREAKING] Standardize orchestration outputs as list of `Message`; allow agent as group chat manager (#2291) - **agent-framework-core**: [BREAKING] Respond with `AgentRunResponse` including serialized structured output (#2285) - **observability**: Use `executor_id` and `edge_group_id` as span names for clearer traces (#2538) - **agent-framework-devui**: Add multimodal input support for workflows and refactor chat input (#2593) @@ -314,7 +365,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **agent-framework-core**: Fix tool execution bleed-over in aiohttp/Bot Framework scenarios ([#2314](https://github.com/microsoft/agent-framework/pull/2314)) - **agent-framework-core**: `@ai_function` now correctly handles `self` parameter ([#2266](https://github.com/microsoft/agent-framework/pull/2266)) - **agent-framework-core**: Resolve string annotations in `FunctionExecutor` ([#2308](https://github.com/microsoft/agent-framework/pull/2308)) -- **agent-framework-core**: Langfuse observability captures ChatAgent system instructions ([#2316](https://github.com/microsoft/agent-framework/pull/2316)) +- **agent-framework-core**: Langfuse observability captures Agent system instructions ([#2316](https://github.com/microsoft/agent-framework/pull/2316)) - **agent-framework-core**: Incomplete URL substring sanitization fix ([#2274](https://github.com/microsoft/agent-framework/pull/2274)) - **observability**: Handle datetime serialization in tool results ([#2248](https://github.com/microsoft/agent-framework/pull/2248)) @@ -571,7 +622,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/). -[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260130...HEAD +[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260210...HEAD +[1.0.0b260210]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260130...python-1.0.0b260210 [1.0.0b260130]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260128...python-1.0.0b260130 [1.0.0b260128]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260127...python-1.0.0b260128 [1.0.0b260127]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260123...python-1.0.0b260127 diff --git a/python/CODING_STANDARD.md b/python/CODING_STANDARD.md index 16f34be54c..e052b60669 100644 --- a/python/CODING_STANDARD.md +++ b/python/CODING_STANDARD.md @@ -118,10 +118,10 @@ Prefer attributes over inheritance when parameters are mostly the same: ```python # ✅ Preferred - using attributes -from agent_framework import ChatMessage +from agent_framework import Message -user_msg = ChatMessage("user", ["Hello, world!"]) -asst_msg = ChatMessage("assistant", ["Hello, world!"]) +user_msg = Message("user", ["Hello, world!"]) +asst_msg = Message("assistant", ["Hello, world!"]) # ❌ Not preferred - unnecessary inheritance from agent_framework import UserMessage, AssistantMessage @@ -157,7 +157,7 @@ The package follows a flat import structure: - **Core**: Import directly from `agent_framework` ```python - from agent_framework import ChatAgent, tool + from agent_framework import Agent, tool ``` - **Components**: Import from `agent_framework.` @@ -381,12 +381,12 @@ def create_client( Use Google-style docstrings for all public APIs: ```python -def create_agent(name: str, chat_client: ChatClientProtocol) -> Agent: +def create_agent(name: str, client: SupportsChatGetResponse) -> Agent: """Create a new agent with the specified configuration. Args: name: The name of the agent. - chat_client: The chat client to use for communication. + client: The chat client to use for communication. Returns: True if the strings are the same, False otherwise. @@ -409,10 +409,10 @@ Define `__all__` in each module to explicitly declare the public API. Avoid usin ```python # ✅ Preferred - explicit __all__ and imports -__all__ = ["ChatAgent", "ChatMessage", "ChatResponse"] +__all__ = ["Agent", "Message", "ChatResponse"] -from ._agents import ChatAgent -from ._types import ChatMessage, ChatResponse +from ._agents import Agent +from ._types import Message, ChatResponse # ❌ Avoid - star imports from ._agents import * diff --git a/python/DEV_SETUP.md b/python/DEV_SETUP.md index f189031468..025e3ce36e 100644 --- a/python/DEV_SETUP.md +++ b/python/DEV_SETUP.md @@ -116,7 +116,7 @@ You will then configure the ChatClient class with the keyword argument `env_file ```python from agent_framework.openai import OpenAIChatClient -chat_client = OpenAIChatClient(env_file_path="openai.env") +client = OpenAIChatClient(env_file_path="openai.env") ``` ## Tests diff --git a/python/README.md b/python/README.md index 80cb85e4f4..160a7affb0 100644 --- a/python/README.md +++ b/python/README.md @@ -62,7 +62,7 @@ You can also override environment variables by explicitly passing configuration ```python from agent_framework.azure import AzureOpenAIChatClient -chat_client = AzureOpenAIChatClient( +client = AzureOpenAIChatClient( api_key='', endpoint='', deployment_name='', @@ -78,12 +78,12 @@ Create agents and invoke them directly: ```python import asyncio -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient async def main(): - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), instructions=""" 1) A robot may not injure a human being... 2) A robot must obey orders given it by human beings... @@ -106,15 +106,15 @@ You can use the chat client classes directly for advanced workflows: ```python import asyncio -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework.openai import OpenAIChatClient async def main(): client = OpenAIChatClient() messages = [ - ChatMessage("system", ["You are a helpful assistant."]), - ChatMessage("user", ["Write a haiku about Agent Framework."]) + Message("system", ["You are a helpful assistant."]), + Message("user", ["Write a haiku about Agent Framework."]) ] response = await client.get_response(messages) @@ -140,7 +140,7 @@ import asyncio from typing import Annotated from random import randint from pydantic import Field -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient @@ -162,8 +162,8 @@ def get_menu_specials() -> str: async def main(): - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), instructions="You are a helpful assistant that can provide weather and restaurant information.", tools=[get_weather, get_menu_specials] ) @@ -189,20 +189,20 @@ Coordinate multiple agents to collaborate on complex tasks using orchestration p ```python import asyncio -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient async def main(): # Create specialized agents - writer = ChatAgent( - chat_client=OpenAIChatClient(), + writer = Agent( + client=OpenAIChatClient(), name="Writer", instructions="You are a creative content writer. Generate and refine slogans based on feedback." ) - reviewer = ChatAgent( - chat_client=OpenAIChatClient(), + reviewer = Agent( + client=OpenAIChatClient(), name="Reviewer", instructions="You are a critical reviewer. Provide detailed feedback on proposed slogans." ) diff --git a/python/packages/a2a/agent_framework_a2a/__init__.py b/python/packages/a2a/agent_framework_a2a/__init__.py index ab4deb98ce..4b4d54ecc3 100644 --- a/python/packages/a2a/agent_framework_a2a/__init__.py +++ b/python/packages/a2a/agent_framework_a2a/__init__.py @@ -2,7 +2,7 @@ import importlib.metadata -from ._agent import A2AAgent +from ._agent import A2AAgent, A2AContinuationToken try: __version__ = importlib.metadata.version(__name__) @@ -11,5 +11,6 @@ except importlib.metadata.PackageNotFoundError: __all__ = [ "A2AAgent", + "A2AContinuationToken", "__version__", ] diff --git a/python/packages/a2a/agent_framework_a2a/_agent.py b/python/packages/a2a/agent_framework_a2a/_agent.py index 335e4c1f68..a938128f10 100644 --- a/python/packages/a2a/agent_framework_a2a/_agent.py +++ b/python/packages/a2a/agent_framework_a2a/_agent.py @@ -7,7 +7,7 @@ import json import re import uuid from collections.abc import AsyncIterable, Awaitable, Sequence -from typing import Any, Final, Literal, cast, overload +from typing import Any, Final, Literal, overload import httpx from a2a.client import Client, ClientConfig, ClientFactory, minimal_agent_card @@ -18,8 +18,9 @@ from a2a.types import ( FilePart, FileWithBytes, FileWithUri, - Message, Task, + TaskIdParams, + TaskQueryParams, TaskState, TextPart, TransportProtocol, @@ -32,23 +33,41 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, + ContinuationToken, + Message, ResponseStream, normalize_messages, prepend_agent_framework_to_user_agent, ) from agent_framework.observability import AgentTelemetryLayer -__all__ = ["A2AAgent"] +__all__ = ["A2AAgent", "A2AContinuationToken"] URI_PATTERN = re.compile(r"^data:(?P[^;]+);base64,(?P[A-Za-z0-9+/=]+)$") + + +class A2AContinuationToken(ContinuationToken): + """Continuation token for A2A protocol long-running tasks.""" + + task_id: str + """A2A protocol task ID.""" + context_id: str + """A2A protocol context ID.""" + + TERMINAL_TASK_STATES = [ TaskState.completed, TaskState.failed, TaskState.canceled, TaskState.rejected, ] +IN_PROGRESS_TASK_STATES = [ + TaskState.submitted, + TaskState.working, + TaskState.input_required, + TaskState.auth_required, +] def _get_uri_data(uri: str) -> str: @@ -63,7 +82,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): """Agent2Agent (A2A) protocol implementation. Wraps an A2A Client to connect the Agent Framework with external A2A-compliant agents - via HTTP/JSON-RPC. Converts framework ChatMessages to A2A Messages on send, and converts + via HTTP/JSON-RPC. Converts framework Messages to A2A Messages on send, and converts A2A responses (Messages/Tasks) back to framework types. Inherits BaseAgent capabilities while managing the underlying A2A protocol communication. @@ -189,107 +208,89 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[False] = ..., thread: AgentThread | None = None, + continuation_token: A2AContinuationToken | None = None, + background: bool = False, **kwargs: Any, ) -> Awaitable[AgentResponse[Any]]: ... @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[True], thread: AgentThread | None = None, + continuation_token: A2AContinuationToken | None = None, + background: bool = False, **kwargs: Any, ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, + continuation_token: A2AContinuationToken | None = None, + background: bool = False, **kwargs: Any, ) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: """Get a response from the agent. - This method returns the final result of the agent's execution - as a single AgentResponse object when stream=False. When stream=True, - it returns a ResponseStream that yields AgentResponseUpdate objects. - Args: messages: The message(s) to send to the agent. Keyword Args: stream: Whether to stream the response. Defaults to False. thread: The conversation thread associated with the message(s). + continuation_token: Optional token to resume a long-running task + instead of starting a new one. + background: When True, in-progress task updates surface continuation + tokens so the caller can poll or resubscribe later. When False + (default), the agent internally waits for the task to complete. kwargs: Additional keyword arguments. Returns: When stream=False: An Awaitable[AgentResponse]. When stream=True: A ResponseStream of AgentResponseUpdate items. """ + if continuation_token is not None: + a2a_stream: AsyncIterable[Any] = self.client.resubscribe(TaskIdParams(id=continuation_token["task_id"])) + else: + normalized_messages = normalize_messages(messages) + a2a_message = self._prepare_message_for_a2a(normalized_messages[-1]) + a2a_stream = self.client.send_message(a2a_message) + + response = ResponseStream( + self._map_a2a_stream(a2a_stream, background=background), + finalizer=AgentResponse.from_updates, + ) if stream: - return self._run_stream_impl(messages=messages, thread=thread, **kwargs) - return self._run_impl(messages=messages, thread=thread, **kwargs) + return response + return response.get_final_response() - async def _run_impl( + async def _map_a2a_stream( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + a2a_stream: AsyncIterable[Any], *, - thread: AgentThread | None = None, - **kwargs: Any, - ) -> AgentResponse[Any]: - """Non-streaming implementation of run.""" - # Collect all updates and use framework to consolidate updates into response - updates: list[AgentResponseUpdate] = [] - async for update in self._stream_updates(messages, thread=thread, **kwargs): - updates.append(update) - return AgentResponse.from_updates(updates) - - def _run_stream_impl( - self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, - *, - thread: AgentThread | None = None, - **kwargs: Any, - ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: - """Streaming implementation of run.""" - - def _finalize(updates: Sequence[AgentResponseUpdate]) -> AgentResponse[Any]: - return AgentResponse.from_updates(list(updates)) - - return ResponseStream(self._stream_updates(messages, thread=thread, **kwargs), finalizer=_finalize) - - async def _stream_updates( - self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, - *, - thread: AgentThread | None = None, - **kwargs: Any, + background: bool = False, ) -> AsyncIterable[AgentResponseUpdate]: - """Internal method to stream updates from the A2A agent. + """Map raw A2A protocol items to AgentResponseUpdates. Args: - messages: The message(s) to send to the agent. + a2a_stream: The raw A2A event stream. Keyword Args: - thread: The conversation thread associated with the message(s). - kwargs: Additional keyword arguments. - - Yields: - AgentResponseUpdate items from the A2A agent. + background: When False, in-progress task updates are silently + consumed (the stream keeps iterating until a terminal state). + When True, they are yielded with a continuation token. """ - normalized_messages = normalize_messages(messages) - a2a_message = self._prepare_message_for_a2a(normalized_messages[-1]) - - response_stream = self.client.send_message(a2a_message) - - async for item in response_stream: - if isinstance(item, Message): + async for item in a2a_stream: + if isinstance(item, A2AMessage): # Process A2A Message contents = self._parse_contents_from_a2a(item.parts) yield AgentResponseUpdate( @@ -300,37 +301,86 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): ) elif isinstance(item, tuple) and len(item) == 2: # ClientEvent = (Task, UpdateEvent) task, _update_event = item - if isinstance(task, Task) and task.status.state in TERMINAL_TASK_STATES: - # Convert Task artifacts to ChatMessages and yield as separate updates - task_messages = self._parse_messages_from_task(task) - if task_messages: - for message in task_messages: - # Use the artifact's ID from raw_representation as message_id for unique identification - artifact_id = getattr(message.raw_representation, "artifact_id", None) - yield AgentResponseUpdate( - contents=message.contents, - role=message.role, - response_id=task.id, - message_id=artifact_id, - raw_representation=task, - ) - else: - # Empty task - yield AgentResponseUpdate( - contents=[], - role="assistant", - response_id=task.id, - raw_representation=task, - ) + if isinstance(task, Task): + for update in self._updates_from_task(task, background=background): + yield update else: - # Unknown response type msg = f"Only Message and Task responses are supported from A2A agents. Received: {type(item)}" raise NotImplementedError(msg) - def _prepare_message_for_a2a(self, message: ChatMessage) -> A2AMessage: - """Prepare a ChatMessage for the A2A protocol. + # ------------------------------------------------------------------ + # Task helpers + # ------------------------------------------------------------------ - Transforms Agent Framework ChatMessage objects into A2A protocol Messages by: + def _updates_from_task(self, task: Task, *, background: bool = False) -> list[AgentResponseUpdate]: + """Convert an A2A Task into AgentResponseUpdate(s). + + Terminal tasks produce updates from their artifacts/history. + In-progress tasks produce a continuation token update only when + ``background=True``; otherwise they are silently skipped so the + caller keeps consuming the stream until completion. + """ + if task.status.state in TERMINAL_TASK_STATES: + task_messages = self._parse_messages_from_task(task) + if task_messages: + return [ + AgentResponseUpdate( + contents=message.contents, + role=message.role, + response_id=task.id, + message_id=getattr(message.raw_representation, "artifact_id", None), + raw_representation=task, + ) + for message in task_messages + ] + return [AgentResponseUpdate(contents=[], role="assistant", response_id=task.id, raw_representation=task)] + + if background and task.status.state in IN_PROGRESS_TASK_STATES: + token = self._build_continuation_token(task) + return [ + AgentResponseUpdate( + contents=[], + role="assistant", + response_id=task.id, + continuation_token=token, + raw_representation=task, + ) + ] + + return [] + + @staticmethod + def _build_continuation_token(task: Task) -> A2AContinuationToken | None: + """Build an A2AContinuationToken from an A2A Task if it is still in progress.""" + if task.status.state in IN_PROGRESS_TASK_STATES: + return A2AContinuationToken(task_id=task.id, context_id=task.context_id) + return None + + async def poll_task(self, continuation_token: A2AContinuationToken) -> AgentResponse[Any]: + """Poll for the current state of a long-running A2A task. + + Unlike ``run(continuation_token=...)``, which resubscribes to the SSE + stream, this performs a single request to retrieve the task state. + + Args: + continuation_token: A token previously obtained from a response's + ``continuation_token`` field. + + Returns: + An AgentResponse whose ``continuation_token`` is set when the task + is still in progress, or ``None`` when it has reached a terminal state. + """ + task_id = continuation_token["task_id"] + task = await self.client.get_task(TaskQueryParams(id=task_id)) + updates = self._updates_from_task(task, background=True) + if updates: + return AgentResponse.from_updates(updates) + return AgentResponse(messages=[], response_id=task.id, raw_representation=task) + + def _prepare_message_for_a2a(self, message: Message) -> A2AMessage: + """Prepare a Message for the A2A protocol. + + Transforms Agent Framework Message objects into A2A protocol Messages by: - Converting all message contents to appropriate A2A Part types - Mapping text content to TextPart objects - Converting file references (URI/data/hosted_file) to FilePart objects @@ -339,7 +389,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): """ parts: list[A2APart] = [] if not message.contents: - raise ValueError("ChatMessage.contents is empty; cannot convert to A2AMessage.") + raise ValueError("Message.contents is empty; cannot convert to A2AMessage.") # Process ALL contents for content in message.contents: @@ -401,11 +451,15 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): case _: raise ValueError(f"Unknown content type: {content.type}") + # Exclude framework-internal keys (e.g. attribution) from wire metadata + internal_keys = {"_attribution"} + metadata = {k: v for k, v in message.additional_properties.items() if k not in internal_keys} or None + return A2AMessage( role=A2ARole("user"), parts=parts, message_id=message.message_id or uuid.uuid4().hex, - metadata=cast(dict[str, Any], message.additional_properties), + metadata=metadata, ) def _parse_contents_from_a2a(self, parts: Sequence[A2APart]) -> list[Content]: @@ -457,9 +511,9 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): raise ValueError(f"Unknown Part kind: {inner_part.kind}") return contents - def _parse_messages_from_task(self, task: Task) -> list[ChatMessage]: - """Parse A2A Task artifacts into ChatMessages with ASSISTANT role.""" - messages: list[ChatMessage] = [] + def _parse_messages_from_task(self, task: Task) -> list[Message]: + """Parse A2A Task artifacts into Messages with ASSISTANT role.""" + messages: list[Message] = [] if task.artifacts is not None: for artifact in task.artifacts: @@ -469,7 +523,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): history_item = task.history[-1] contents = self._parse_contents_from_a2a(history_item.parts) messages.append( - ChatMessage( + Message( role="assistant" if history_item.role == A2ARole.agent else "user", contents=contents, raw_representation=history_item, @@ -478,10 +532,10 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): return messages - def _parse_message_from_artifact(self, artifact: Artifact) -> ChatMessage: - """Parse A2A Artifact into ChatMessage using part contents.""" + def _parse_message_from_artifact(self, artifact: Artifact) -> Message: + """Parse A2A Artifact into Message using part contents.""" contents = self._parse_contents_from_a2a(artifact.parts) - return ChatMessage( + return Message( role="assistant", contents=contents, raw_representation=artifact, diff --git a/python/packages/a2a/pyproject.toml b/python/packages/a2a/pyproject.toml index dbf59aea85..8e91a8de70 100644 --- a/python/packages/a2a/pyproject.toml +++ b/python/packages/a2a/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "a2a-sdk>=0.3.5", ] diff --git a/python/packages/a2a/tests/test_a2a_agent.py b/python/packages/a2a/tests/test_a2a_agent.py index 10e2e9c956..61123df5ab 100644 --- a/python/packages/a2a/tests/test_a2a_agent.py +++ b/python/packages/a2a/tests/test_a2a_agent.py @@ -12,23 +12,24 @@ from a2a.types import ( DataPart, FilePart, FileWithUri, - Message, Part, Task, TaskState, TaskStatus, TextPart, ) +from a2a.types import Message as A2AMessage from a2a.types import Role as A2ARole from agent_framework import ( AgentResponse, AgentResponseUpdate, - ChatMessage, Content, + Message, ) from agent_framework.a2a import A2AAgent from pytest import fixture, raises +from agent_framework_a2a import A2AContinuationToken from agent_framework_a2a._agent import _get_uri_data # type: ignore @@ -38,6 +39,8 @@ class MockA2AClient: def __init__(self) -> None: self.call_count: int = 0 self.responses: list[Any] = [] + self.resubscribe_responses: list[Any] = [] + self.get_task_response: Task | None = None def add_message_response(self, message_id: str, text: str, role: str = "agent") -> None: """Add a mock Message response.""" @@ -46,7 +49,7 @@ class MockA2AClient: text_part = Part(root=TextPart(text=text)) # Create actual Message instance - message = Message( + message = A2AMessage( message_id=message_id, role=A2ARole.agent if role == "agent" else A2ARole.user, parts=[text_part] ) self.responses.append(message) @@ -80,6 +83,18 @@ class MockA2AClient: client_event = (task, update_event) self.responses.append(client_event) + def add_in_progress_task_response( + self, + task_id: str, + context_id: str = "test-context", + state: TaskState = TaskState.working, + ) -> None: + """Add a mock in-progress Task response (non-terminal).""" + status = TaskStatus(state=state, message=None) + task = Task(id=task_id, context_id=context_id, status=status) + client_event = (task, None) + self.responses.append(client_event) + async def send_message(self, message: Any) -> AsyncIterator[Any]: """Mock send_message method that yields responses.""" self.call_count += 1 @@ -88,6 +103,22 @@ class MockA2AClient: response = self.responses.pop(0) yield response + async def resubscribe(self, request: Any) -> AsyncIterator[Any]: + """Mock resubscribe method that yields responses.""" + self.call_count += 1 + + for response in self.resubscribe_responses: + yield response + self.resubscribe_responses.clear() + + async def get_task(self, request: Any) -> Task: + """Mock get_task method that returns a task.""" + self.call_count += 1 + if self.get_task_response is not None: + return self.get_task_response + msg = "No get_task response configured" + raise ValueError(msg) + @fixture def mock_a2a_client() -> MockA2AClient: @@ -250,7 +281,7 @@ def test_parse_message_from_artifact(a2a_agent: A2AAgent) -> None: result = a2a_agent._parse_message_from_artifact(artifact) - assert isinstance(result, ChatMessage) + assert isinstance(result, Message) assert result.role == "assistant" assert result.text == "Artifact content" assert result.raw_representation == artifact @@ -293,9 +324,9 @@ def test_parse_contents_from_a2a_conversion(a2a_agent: A2AAgent) -> None: def test_prepare_message_for_a2a_with_error_content(a2a_agent: A2AAgent) -> None: """Test _prepare_message_for_a2a with ErrorContent.""" - # Create ChatMessage with ErrorContent + # Create Message with ErrorContent error_content = Content.from_error(message="Test error message") - message = ChatMessage(role="user", contents=[error_content]) + message = Message(role="user", contents=[error_content]) # Convert to A2A message a2a_message = a2a_agent._prepare_message_for_a2a(message) @@ -308,9 +339,9 @@ def test_prepare_message_for_a2a_with_error_content(a2a_agent: A2AAgent) -> None def test_prepare_message_for_a2a_with_uri_content(a2a_agent: A2AAgent) -> None: """Test _prepare_message_for_a2a with UriContent.""" - # Create ChatMessage with UriContent + # Create Message with UriContent uri_content = Content.from_uri(uri="http://example.com/file.pdf", media_type="application/pdf") - message = ChatMessage(role="user", contents=[uri_content]) + message = Message(role="user", contents=[uri_content]) # Convert to A2A message a2a_message = a2a_agent._prepare_message_for_a2a(message) @@ -324,9 +355,9 @@ def test_prepare_message_for_a2a_with_uri_content(a2a_agent: A2AAgent) -> None: def test_prepare_message_for_a2a_with_data_content(a2a_agent: A2AAgent) -> None: """Test _prepare_message_for_a2a with DataContent.""" - # Create ChatMessage with DataContent (base64 data URI) + # Create Message with DataContent (base64 data URI) data_content = Content.from_uri(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain") - message = ChatMessage(role="user", contents=[data_content]) + message = Message(role="user", contents=[data_content]) # Convert to A2A message a2a_message = a2a_agent._prepare_message_for_a2a(message) @@ -339,11 +370,11 @@ def test_prepare_message_for_a2a_with_data_content(a2a_agent: A2AAgent) -> None: def test_prepare_message_for_a2a_empty_contents_raises_error(a2a_agent: A2AAgent) -> None: """Test _prepare_message_for_a2a with empty contents raises ValueError.""" - # Create ChatMessage with no contents - message = ChatMessage(role="user", contents=[]) + # Create Message with no contents + message = Message(role="user", contents=[]) # Should raise ValueError for empty contents - with raises(ValueError, match="ChatMessage.contents is empty"): + with raises(ValueError, match="Message.contents is empty"): a2a_agent._prepare_message_for_a2a(message) @@ -401,12 +432,12 @@ async def test_context_manager_no_cleanup_when_no_http_client() -> None: def test_prepare_message_for_a2a_with_multiple_contents() -> None: - """Test conversion of ChatMessage with multiple contents.""" + """Test conversion of Message with multiple contents.""" agent = A2AAgent(client=MagicMock(), _http_client=None) # Create message with multiple content types - message = ChatMessage( + message = Message( role="user", contents=[ Content.from_text(text="Here's the analysis:"), @@ -458,12 +489,12 @@ def test_parse_contents_from_a2a_unknown_part_kind() -> None: def test_prepare_message_for_a2a_with_hosted_file() -> None: - """Test conversion of ChatMessage with HostedFileContent to A2A message.""" + """Test conversion of Message with HostedFileContent to A2A message.""" agent = A2AAgent(client=MagicMock(), _http_client=None) # Create message with hosted file content - message = ChatMessage( + message = Message( role="user", contents=[Content.from_hosted_file(file_id="hosted://storage/document.pdf")], ) @@ -598,3 +629,158 @@ def test_a2a_agent_initialization_with_timeout_parameter() -> None: # Verify it's an httpx.Timeout object with our custom timeout applied to all components assert isinstance(timeout_arg, httpx.Timeout) + + +# region Continuation Token Tests + + +async def test_working_task_emits_continuation_token(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test that a working (non-terminal) task yields an update with a continuation token when background=True.""" + mock_a2a_client.add_in_progress_task_response("task-wip", context_id="ctx-1", state=TaskState.working) + + response = await a2a_agent.run("Start long task", background=True) + + assert isinstance(response, AgentResponse) + assert response.continuation_token is not None + assert response.continuation_token["task_id"] == "task-wip" + assert response.continuation_token["context_id"] == "ctx-1" + + +async def test_submitted_task_emits_continuation_token(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test that a submitted task yields a continuation token when background=True.""" + mock_a2a_client.add_in_progress_task_response("task-sub", state=TaskState.submitted) + + response = await a2a_agent.run("Submit task", background=True) + + assert response.continuation_token is not None + assert response.continuation_token["task_id"] == "task-sub" + + +async def test_input_required_task_emits_continuation_token( + a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient +) -> None: + """Test that an input_required task yields a continuation token when background=True.""" + mock_a2a_client.add_in_progress_task_response("task-input", state=TaskState.input_required) + + response = await a2a_agent.run("Need input", background=True) + + assert response.continuation_token is not None + assert response.continuation_token["task_id"] == "task-input" + + +async def test_working_task_no_token_without_background(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test that background=False (default) does not emit continuation tokens for in-progress tasks.""" + mock_a2a_client.add_in_progress_task_response("task-fg", context_id="ctx-fg", state=TaskState.working) + + response = await a2a_agent.run("Foreground task") + + assert response.continuation_token is None + + +async def test_completed_task_has_no_continuation_token(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test that a completed task does not set a continuation token.""" + mock_a2a_client.add_task_response("task-done", [{"id": "art-1", "content": "Result"}]) + + response = await a2a_agent.run("Quick task") + + assert response.continuation_token is None + assert len(response.messages) == 1 + assert response.messages[0].text == "Result" + + +async def test_streaming_emits_continuation_token(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test that streaming with background=True yields updates with continuation tokens.""" + mock_a2a_client.add_in_progress_task_response("task-stream", context_id="ctx-s", state=TaskState.working) + + updates: list[AgentResponseUpdate] = [] + async for update in a2a_agent.run("Stream task", stream=True, background=True): + updates.append(update) + + assert len(updates) == 1 + assert updates[0].continuation_token is not None + assert updates[0].continuation_token["task_id"] == "task-stream" + assert updates[0].continuation_token["context_id"] == "ctx-s" + + +async def test_resume_via_continuation_token(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test that run() with continuation_token uses resubscribe instead of send_message.""" + # Set up the resubscribe response (completed task) + status = TaskStatus(state=TaskState.completed, message=None) + artifact = Artifact( + artifact_id="art-resume", + name="result", + parts=[Part(root=TextPart(text="Resumed result"))], + ) + task = Task(id="task-resume", context_id="ctx-r", status=status, artifacts=[artifact]) + mock_a2a_client.resubscribe_responses.append((task, None)) + + token = A2AContinuationToken(task_id="task-resume", context_id="ctx-r") + response = await a2a_agent.run(continuation_token=token) + + assert isinstance(response, AgentResponse) + assert len(response.messages) == 1 + assert response.messages[0].text == "Resumed result" + assert response.continuation_token is None + + +async def test_resume_streaming_via_continuation_token(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test that streaming run() with continuation_token and background=True uses resubscribe.""" + # Still working + status_wip = TaskStatus(state=TaskState.working, message=None) + task_wip = Task(id="task-rs", context_id="ctx-rs", status=status_wip) + # Then completed + status_done = TaskStatus(state=TaskState.completed, message=None) + artifact = Artifact( + artifact_id="art-rs", + name="result", + parts=[Part(root=TextPart(text="Stream resumed"))], + ) + task_done = Task(id="task-rs", context_id="ctx-rs", status=status_done, artifacts=[artifact]) + mock_a2a_client.resubscribe_responses.extend([(task_wip, None), (task_done, None)]) + + token = A2AContinuationToken(task_id="task-rs", context_id="ctx-rs") + updates: list[AgentResponseUpdate] = [] + async for update in a2a_agent.run(stream=True, continuation_token=token, background=True): + updates.append(update) + + # First update: in-progress with token, second: completed with content + assert len(updates) == 2 + assert updates[0].continuation_token is not None + assert updates[0].continuation_token["task_id"] == "task-rs" + assert updates[1].continuation_token is None + assert updates[1].contents[0].text == "Stream resumed" + + +async def test_poll_task_in_progress(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test poll_task returns continuation token when task is still in progress.""" + status = TaskStatus(state=TaskState.working, message=None) + mock_a2a_client.get_task_response = Task(id="task-poll", context_id="ctx-p", status=status) + + token = A2AContinuationToken(task_id="task-poll", context_id="ctx-p") + response = await a2a_agent.poll_task(token) + + assert response.continuation_token is not None + assert response.continuation_token["task_id"] == "task-poll" + + +async def test_poll_task_completed(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: + """Test poll_task returns result with no continuation token when task is complete.""" + status = TaskStatus(state=TaskState.completed, message=None) + artifact = Artifact( + artifact_id="art-poll", + name="result", + parts=[Part(root=TextPart(text="Poll result"))], + ) + mock_a2a_client.get_task_response = Task( + id="task-poll-done", context_id="ctx-pd", status=status, artifacts=[artifact] + ) + + token = A2AContinuationToken(task_id="task-poll-done", context_id="ctx-pd") + response = await a2a_agent.poll_task(token) + + assert response.continuation_token is None + assert len(response.messages) == 1 + assert response.messages[0].text == "Poll result" + + +# endregion diff --git a/python/packages/ag-ui/README.md b/python/packages/ag-ui/README.md index d2ff4c3d10..3488d9c8bf 100644 --- a/python/packages/ag-ui/README.md +++ b/python/packages/ag-ui/README.md @@ -14,15 +14,15 @@ pip install agent-framework-ag-ui ```python from fastapi import FastAPI -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureOpenAIChatClient from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint # Create your agent -agent = ChatAgent( +agent = Agent( name="my_agent", instructions="You are a helpful assistant.", - chat_client=AzureOpenAIChatClient( + client=AzureOpenAIChatClient( endpoint="https://your-resource.openai.azure.com/", deployment_name="gpt-4o-mini", api_key="your-api-key", @@ -58,7 +58,7 @@ The `AGUIChatClient` supports: - Streaming and non-streaming responses - Hybrid tool execution (client-side + server-side tools) - Automatic thread management for conversation continuity -- Integration with `ChatAgent` for client-side history management +- Integration with `Agent` for client-side history management ## Documentation @@ -91,7 +91,7 @@ The AG-UI endpoint does not enforce authentication by default. **For production import os from fastapi import Depends, FastAPI, HTTPException, Security from fastapi.security import APIKeyHeader -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint # Configure API key authentication @@ -104,7 +104,7 @@ async def verify_api_key(api_key: str | None = Security(API_KEY_HEADER)) -> None raise HTTPException(status_code=401, detail="Invalid or missing API key") # Create agent and app -agent = ChatAgent(name="my_agent", instructions="...", chat_client=...) +agent = Agent(name="my_agent", instructions="...", client=...) app = FastAPI() # Register endpoint WITH authentication diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_client.py b/python/packages/ag-ui/agent_framework_ag_ui/_client.py index c30e4e5926..1df1ba84e2 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_client.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_client.py @@ -15,11 +15,11 @@ from typing import TYPE_CHECKING, Any, Generic, TypedDict, cast import httpx from agent_framework import ( BaseChatClient, - ChatMessage, ChatResponse, ChatResponseUpdate, Content, FunctionTool, + Message, ResponseStream, ) from agent_framework._middleware import ChatMiddlewareLayer @@ -69,10 +69,10 @@ AGUIChatOptionsT = TypeVar( ) -def _apply_server_function_call_unwrap(chat_client: BaseChatClientT) -> BaseChatClientT: +def _apply_server_function_call_unwrap(client: BaseChatClientT) -> BaseChatClientT: """Class decorator that unwraps server-side function calls after tool handling.""" - original_get_response = chat_client.get_response + original_get_response = client.get_response @wraps(original_get_response) def response_wrapper( @@ -105,8 +105,8 @@ def _apply_server_function_call_unwrap(chat_client: BaseChatClientT) -> BaseChat _unwrap_server_function_call_contents(cast(MutableSequence[Content | dict[str, Any]], update.contents)) return update - chat_client.get_response = response_wrapper # type: ignore[assignment] - return chat_client + client.get_response = response_wrapper # type: ignore[assignment] + return client @_apply_server_function_call_unwrap @@ -130,8 +130,8 @@ class AGUIChatClient( This client sends exactly the messages it receives to the server. It does NOT automatically maintain conversation history. The server must handle history via thread_id. - For stateless servers: Use ChatAgent wrapper which will send full message history on each - request. However, even with ChatAgent, the server must echo back all context for the + For stateless servers: Use Agent wrapper which will send full message history on each + request. However, even with Agent, the server must echo back all context for the agent to maintain history across turns. Important: Tool Handling (Hybrid Execution - matches .NET) @@ -140,7 +140,7 @@ class AGUIChatClient( 3. When LLM calls a client tool, function invocation executes it locally 4. Both client and server tools work together (hybrid pattern) - The wrapping ChatAgent's function invocation handles client tool execution + The wrapping Agent's function invocation handles client tool execution automatically when the server's LLM decides to call them. Examples: @@ -162,18 +162,18 @@ class AGUIChatClient( metadata={"thread_id": thread_id} ) - Recommended usage with ChatAgent (client manages history): + Recommended usage with Agent (client manages history): .. code-block:: python - from agent_framework import ChatAgent + from agent_framework import Agent from agent_framework.ag_ui import AGUIChatClient client = AGUIChatClient(endpoint="http://localhost:8888/") - agent = ChatAgent(name="assistant", client=client) + agent = Agent(name="assistant", client=client) thread = await agent.get_new_thread() - # ChatAgent automatically maintains history and sends full context + # Agent automatically maintains history and sends full context response = await agent.run("Hello!", thread=thread) response2 = await agent.run("How are you?", thread=thread) @@ -282,9 +282,7 @@ class AGUIChatClient( logger = get_logger() logger.debug(f"[AGUIChatClient] Registered server placeholder: {tool_name}") - def _extract_state_from_messages( - self, messages: Sequence[ChatMessage] - ) -> tuple[list[ChatMessage], dict[str, Any] | None]: + def _extract_state_from_messages(self, messages: Sequence[Message]) -> tuple[list[Message], dict[str, Any] | None]: """Extract state from last message if present. Args: @@ -319,11 +317,11 @@ class AGUIChatClient( return list(messages), None - def _convert_messages_to_agui_format(self, messages: list[ChatMessage]) -> list[dict[str, Any]]: + def _convert_messages_to_agui_format(self, messages: list[Message]) -> list[dict[str, Any]]: """Convert Agent Framework messages to AG-UI format. Args: - messages: List of ChatMessage objects + messages: List of Message objects Returns: List of AG-UI formatted message dictionaries @@ -353,7 +351,7 @@ class AGUIChatClient( def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], stream: bool = False, options: Mapping[str, Any], **kwargs: Any, @@ -393,7 +391,7 @@ class AGUIChatClient( async def _streaming_impl( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], **kwargs: Any, ) -> AsyncIterable[ChatResponseUpdate]: @@ -415,7 +413,7 @@ class AGUIChatClient( agui_messages = self._convert_messages_to_agui_format(messages_to_send) # Send client tools to server so LLM knows about them - # Client tools execute via ChatAgent's function invocation wrapper + # Client tools execute via Agent's function invocation wrapper agui_tools = convert_tools_to_agui_format(options.get("tools")) # Build set of client tool names (matches .NET clientToolSet) diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py b/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py index 3f35572f78..709d8f4887 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py @@ -9,8 +9,8 @@ import logging from typing import Any, cast from agent_framework import ( - ChatMessage, Content, + Message, prepare_function_call_results, ) @@ -25,9 +25,9 @@ from ._utils import ( logger = logging.getLogger(__name__) -def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]: +def _sanitize_tool_history(messages: list[Message]) -> list[Message]: """Normalize tool ordering and inject synthetic results for AG-UI edge cases.""" - sanitized: list[ChatMessage] = [] + sanitized: list[Message] = [] pending_tool_call_ids: set[str] | None = None pending_confirm_changes_id: str | None = None @@ -60,7 +60,7 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]: ] if filtered_contents: # Create a new message without confirm_changes to avoid mutating the input - filtered_msg = ChatMessage(role=msg.role, contents=filtered_contents) + filtered_msg = Message(role=msg.role, contents=filtered_contents) sanitized.append(filtered_msg) # If no contents left after filtering, don't append anything @@ -99,7 +99,7 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]: if pending_confirm_changes_id and approval_accepted is not None: logger.info(f"Injecting synthetic tool result for confirm_changes call_id={pending_confirm_changes_id}") - synthetic_result = ChatMessage( + synthetic_result = Message( role="tool", contents=[ Content.from_function_result( @@ -128,7 +128,7 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]: logger.info( f"Injecting synthetic tool result for confirm_changes call_id={pending_confirm_changes_id}" ) - synthetic_result = ChatMessage( + synthetic_result = Message( role="tool", contents=[ Content.from_function_result( @@ -152,7 +152,7 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]: ) for pending_call_id in pending_tool_call_ids: logger.info(f"Injecting synthetic tool result for pending call_id={pending_call_id}") - synthetic_result = ChatMessage( + synthetic_result = Message( role="tool", contents=[ Content.from_function_result( @@ -196,10 +196,10 @@ def _sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]: return sanitized -def _deduplicate_messages(messages: list[ChatMessage]) -> list[ChatMessage]: +def _deduplicate_messages(messages: list[Message]) -> list[Message]: """Remove duplicate messages while preserving order.""" seen_keys: dict[Any, int] = {} - unique_messages: list[ChatMessage] = [] + unique_messages: list[Message] = [] for idx, msg in enumerate(messages): role_value = get_role_value(msg) @@ -256,7 +256,7 @@ def _deduplicate_messages(messages: list[ChatMessage]) -> list[ChatMessage]: def normalize_agui_input_messages( messages: list[dict[str, Any]], -) -> tuple[list[ChatMessage], list[dict[str, Any]]]: +) -> tuple[list[Message], list[dict[str, Any]]]: """Normalize raw AG-UI messages into provider and snapshot formats.""" provider_messages = agui_messages_to_agent_framework(messages) provider_messages = _sanitize_tool_history(provider_messages) @@ -265,14 +265,14 @@ def normalize_agui_input_messages( return provider_messages, snapshot_messages -def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[ChatMessage]: +def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Message]: """Convert AG-UI messages to Agent Framework format. Args: messages: List of AG-UI messages Returns: - List of Agent Framework ChatMessage objects + List of Agent Framework Message objects """ def _update_tool_call_arguments( @@ -367,7 +367,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha allowed_keys = set(original_args.keys()) return {key: value for key, value in modified_args.items() if key in allowed_keys} - result: list[ChatMessage] = [] + result: list[Message] = [] for msg in messages: # Handle standard tool result messages early (role="tool") to preserve provider invariants # This path maps AG‑UI tool messages to function_result content with the correct tool_call_id @@ -480,7 +480,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha merged_args["steps"] = merged_steps state_args = merged_args - # Update the ChatMessage tool call with only enabled steps (for LLM context). + # Update the Message tool call with only enabled steps (for LLM context). # The LLM should only see the steps that were actually approved/executed. updated_args_for_llm = ( json.dumps(filtered_args) @@ -510,14 +510,14 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha function_call=func_call_for_approval, additional_properties={"ag_ui_state_args": state_args} if state_args else None, ) - chat_msg = ChatMessage( + chat_msg = Message( role="user", contents=[approval_response], ) else: # No matching function call found - this is likely a confirm_changes approval # Keep the old behavior for backwards compatibility - chat_msg = ChatMessage( + chat_msg = Message( role="user", contents=[Content.from_text(text=approval_payload_text)], additional_properties={"is_tool_result": True, "tool_call_id": str(tool_call_id or "")}, @@ -537,7 +537,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha func_result = result_content else: func_result = str(result_content) - chat_msg = ChatMessage( + chat_msg = Message( role="tool", contents=[Content.from_function_result(call_id=str(tool_call_id), result=func_result)], ) @@ -553,7 +553,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha tool_call_id = msg.get("toolCallId") or msg.get("tool_call_id") or msg.get("actionExecutionId", "") result_content = msg.get("result", msg.get("content", "")) - chat_msg = ChatMessage( + chat_msg = Message( role="tool", contents=[Content.from_function_result(call_id=str(tool_call_id), result=result_content)], ) @@ -592,7 +592,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha arguments=arguments, ) ) - chat_msg = ChatMessage(role="assistant", contents=contents) + chat_msg = Message(role="assistant", contents=contents) if "id" in msg: chat_msg.message_id = msg["id"] result.append(chat_msg) @@ -622,14 +622,14 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha ) approval_contents.append(approval_response) - chat_msg = ChatMessage(role=role, contents=approval_contents) # type: ignore[call-overload] + chat_msg = Message(role=role, contents=approval_contents) # type: ignore[call-overload] else: # Regular text message content = msg.get("content", "") if isinstance(content, str): - chat_msg = ChatMessage(role=role, contents=[Content.from_text(text=content)]) # type: ignore[call-overload] + chat_msg = Message(role=role, contents=[Content.from_text(text=content)]) # type: ignore[call-overload] else: - chat_msg = ChatMessage(role=role, contents=[Content.from_text(text=str(content))]) # type: ignore[call-overload] + chat_msg = Message(role=role, contents=[Content.from_text(text=str(content))]) # type: ignore[call-overload] if "id" in msg: chat_msg.message_id = msg["id"] @@ -639,11 +639,11 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha return result -def agent_framework_messages_to_agui(messages: list[ChatMessage] | list[dict[str, Any]]) -> list[dict[str, Any]]: +def agent_framework_messages_to_agui(messages: list[Message] | list[dict[str, Any]]) -> list[dict[str, Any]]: """Convert Agent Framework messages to AG-UI format. Args: - messages: List of Agent Framework ChatMessage objects or AG-UI dicts (already converted) + messages: List of Agent Framework Message objects or AG-UI dicts (already converted) Returns: List of AG-UI message dictionaries @@ -672,7 +672,7 @@ def agent_framework_messages_to_agui(messages: list[ChatMessage] | list[dict[str result.append(normalized_msg) continue - # Convert ChatMessage to AG-UI format + # Convert Message to AG-UI format role_value: str = msg.role if hasattr(msg.role, "value") else msg.role # type: ignore[assignment] role = FRAMEWORK_TO_AGUI_ROLE.get(role_value, "user") diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_orchestration/_helpers.py b/python/packages/ag-ui/agent_framework_ag_ui/_orchestration/_helpers.py index 277b5effce..aea3eb66c5 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_orchestration/_helpers.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_orchestration/_helpers.py @@ -13,8 +13,8 @@ import logging from typing import Any from agent_framework import ( - ChatMessage, Content, + Message, ) from .._utils import get_role_value @@ -22,7 +22,7 @@ from .._utils import get_role_value logger = logging.getLogger(__name__) -def pending_tool_call_ids(messages: list[ChatMessage]) -> set[str]: +def pending_tool_call_ids(messages: list[Message]) -> set[str]: """Get IDs of tool calls without corresponding results. Args: @@ -42,7 +42,7 @@ def pending_tool_call_ids(messages: list[ChatMessage]) -> set[str]: return pending_ids - resolved_ids -def is_state_context_message(message: ChatMessage) -> bool: +def is_state_context_message(message: Message) -> bool: """Check if a message is a state context system message. Args: @@ -178,7 +178,7 @@ def build_safe_metadata(thread_metadata: dict[str, Any] | None) -> dict[str, Any return safe_metadata -def latest_approval_response(messages: list[ChatMessage]) -> Content | None: +def latest_approval_response(messages: list[Message]) -> Content | None: """Get the latest approval response from messages. Args: diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_orchestration/_tooling.py b/python/packages/ag-ui/agent_framework_ag_ui/_orchestration/_tooling.py index 069622f490..442138649a 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_orchestration/_tooling.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_orchestration/_tooling.py @@ -39,7 +39,7 @@ def collect_server_tools(agent: SupportsAgentRun) -> list[Any]: functions need to be included for tool execution during approval flows. Args: - agent: Agent instance to collect tools from. Works with ChatAgent + agent: Agent instance to collect tools from. Works with Agent or any agent with default_options and optional mcp_tools attributes. Returns: @@ -53,7 +53,7 @@ def collect_server_tools(agent: SupportsAgentRun) -> list[Any]: tools_from_agent = default_options.get("tools") if isinstance(default_options, dict) else None server_tools = list(tools_from_agent) if tools_from_agent else [] - # Include functions from connected MCP tools (only available on ChatAgent) + # Include functions from connected MCP tools (only available on Agent) mcp_tools = getattr(agent, "mcp_tools", None) if mcp_tools: server_tools.extend(_collect_mcp_tool_functions(mcp_tools)) @@ -70,19 +70,19 @@ def register_additional_client_tools(agent: SupportsAgentRun, client_tools: list """Register client tools as additional declaration-only tools to avoid server execution. Args: - agent: Agent instance to register tools on. Works with ChatAgent - or any agent with a chat_client attribute. + agent: Agent instance to register tools on. Works with Agent + or any agent with a client attribute. client_tools: List of client tools to register. """ if not client_tools: return - chat_client = getattr(agent, "chat_client", None) - if chat_client is None: + client = getattr(agent, "client", None) + if client is None: return - if isinstance(chat_client, BaseChatClient) and chat_client.function_invocation_configuration is not None: # type: ignore[attr-defined] - chat_client.function_invocation_configuration["additional_tools"] = client_tools # type: ignore[attr-defined] + if isinstance(client, BaseChatClient) and client.function_invocation_configuration is not None: # type: ignore[attr-defined] + client.function_invocation_configuration["additional_tools"] = client_tools # type: ignore[attr-defined] logger.debug(f"[TOOLS] Registered {len(client_tools)} client tools as additional_tools (declaration-only)") diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_run.py b/python/packages/ag-ui/agent_framework_ag_ui/_run.py index d47fdc4d67..853127e630 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_run.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_run.py @@ -28,8 +28,8 @@ from ag_ui.core import ( ) from agent_framework import ( AgentThread, - ChatMessage, Content, + Message, SupportsAgentRun, prepare_function_call_results, ) @@ -195,7 +195,7 @@ class FlowState: def _create_state_context_message( current_state: dict[str, Any], state_schema: dict[str, Any], -) -> ChatMessage | None: +) -> Message | None: """Create a system message with current state context. This injects the current state into the conversation so the model @@ -206,13 +206,13 @@ def _create_state_context_message( state_schema: The state schema (used to determine if injection is needed) Returns: - ChatMessage with state context, or None if not needed + Message with state context, or None if not needed """ if not current_state or not state_schema: return None state_json = json.dumps(current_state, indent=2) - return ChatMessage( + return Message( role="system", contents=[ Content.from_text( @@ -229,10 +229,10 @@ def _create_state_context_message( def _inject_state_context( - messages: list[ChatMessage], + messages: list[Message], current_state: dict[str, Any], state_schema: dict[str, Any], -) -> list[ChatMessage]: +) -> list[Message]: """Inject state context message into messages if appropriate. The state context is injected before the last user message to give @@ -592,7 +592,7 @@ async def _resolve_approval_responses( Args: messages: List of messages (will be modified in place) tools: List of available tools - agent: The agent instance (to get chat_client and config) + agent: The agent instance (to get client and config) run_kwargs: Kwargs for tool execution """ fcc_todo = _collect_approval_responses(messages) @@ -605,12 +605,10 @@ async def _resolve_approval_responses( # Execute approved tool calls if approved_responses and tools: - chat_client = getattr(agent, "chat_client", None) - config = normalize_function_invocation_configuration( - getattr(chat_client, "function_invocation_configuration", None) - ) + client = getattr(agent, "client", None) + config = normalize_function_invocation_configuration(getattr(client, "function_invocation_configuration", None)) middleware_pipeline = FunctionMiddlewarePipeline( - *getattr(chat_client, "function_middleware", ()), + *getattr(client, "function_middleware", ()), *run_kwargs.get("middleware", ()), ) # Filter out AG-UI-specific kwargs that should not be passed to tool execution @@ -672,7 +670,7 @@ def _convert_approval_results_to_tool_messages(messages: list[Any]) -> None: This modifies the messages list in place. Args: - messages: List of ChatMessage objects to process + messages: List of Message objects to process """ result: list[Any] = [] @@ -694,11 +692,11 @@ def _convert_approval_results_to_tool_messages(messages: list[Any]) -> None: # Tool messages first (right after the preceding assistant message per OpenAI requirements) for func_result in function_results: - result.append(ChatMessage(role="tool", contents=[func_result])) + result.append(Message(role="tool", contents=[func_result])) # Then user message with remaining content (if any) if other_contents: - result.append(ChatMessage(role=msg.role, contents=other_contents)) + result.append(Message(role=msg.role, contents=other_contents)) messages[:] = result @@ -793,9 +791,9 @@ async def run_agent_stream( # Check for structured output mode (skip text content) skip_text = False response_format = None - from agent_framework import ChatAgent + from agent_framework import Agent - if isinstance(agent, ChatAgent): + if isinstance(agent, Agent): response_format = agent.default_options.get("response_format") skip_text = response_format is not None diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_utils.py b/python/packages/ag-ui/agent_framework_ag_ui/_utils.py index 356ad7da96..fd63202a47 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_utils.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_utils.py @@ -12,7 +12,7 @@ from dataclasses import asdict, is_dataclass from datetime import date, datetime from typing import Any -from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool, ToolProtocol +from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool # Role mapping constants AGUI_TO_FRAMEWORK_ROLE: dict[str, str] = { @@ -200,10 +200,10 @@ def convert_agui_tools_to_agent_framework( def convert_tools_to_agui_format( tools: ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None ), ) -> list[dict[str, Any]] | None: @@ -225,7 +225,7 @@ def convert_tools_to_agui_format( # Normalize to list if not isinstance(tools, list): - tool_list: list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] = [tools] # type: ignore[list-item] + tool_list: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] = [tools] # type: ignore[list-item] else: tool_list = tools # type: ignore[assignment] @@ -256,12 +256,8 @@ def convert_tools_to_agui_format( "parameters": ai_func.parameters(), } ) - elif isinstance(tool_item, ToolProtocol): - # Handle other ToolProtocol implementations - # For now, we'll skip non-FunctionTool instances as they may not have - # the parameters() method. This matches .NET behavior which only - # converts FunctionToolDeclaration instances. - continue + # Note: dict-based hosted tools (CodeInterpreter, WebSearch, etc.) are passed through + # as-is in the first branch. Non-FunctionTool, non-dict items are skipped. return results if results else None diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/README.md b/python/packages/ag-ui/agent_framework_ag_ui_examples/README.md index df07cff85d..e11a05d863 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/README.md +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/README.md @@ -12,7 +12,7 @@ pip install agent-framework-ag-ui ### Using Example Agents with Any Chat Client -All example agents are factory functions that accept any `ChatClientProtocol`-compatible chat client: +All example agents are factory functions that accept any `SupportsChatGetResponse`-compatible chat client: ```python from fastapi import FastAPI @@ -38,15 +38,15 @@ add_agent_framework_fastapi_endpoint(app, weather_agent(openai_client), "/weathe ```python from fastapi import FastAPI -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureOpenAIChatClient from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint # Create your agent -agent = ChatAgent( +agent = Agent( name="my_agent", instructions="You are a helpful assistant.", - chat_client=AzureOpenAIChatClient(model_id="gpt-4o"), + client=AzureOpenAIChatClient(model_id="gpt-4o"), ) # Create FastAPI app and add AG-UI endpoint @@ -70,21 +70,21 @@ This integration supports all 7 AG-UI features: ## Examples -All example agents are implemented as **factory functions** that accept any chat client implementing `ChatClientProtocol`. This provides maximum flexibility to use Azure OpenAI, OpenAI, Anthropic, or any custom chat client implementation. +All example agents are implemented as **factory functions** that accept any chat client implementing `SupportsChatGetResponse`. This provides maximum flexibility to use Azure OpenAI, OpenAI, Anthropic, or any custom chat client implementation. ### Available Example Agents Complete examples for all AG-UI features are available: -- `simple_agent(chat_client)` - Basic agentic chat (Feature 1) -- `weather_agent(chat_client)` - Backend tool rendering (Feature 2) -- `human_in_the_loop_agent(chat_client)` - Human-in-the-loop with step customization (Feature 3) -- `task_steps_agent_wrapped(chat_client)` - Agentic generative UI with step execution (Feature 4) -- `ui_generator_agent(chat_client)` - Tool-based generative UI (Feature 5) -- `recipe_agent(chat_client)` - Shared state management (Feature 6) -- `document_writer_agent(chat_client)` - Predictive state updates (Feature 7) -- `research_assistant_agent(chat_client)` - Research with progress events -- `task_planner_agent(chat_client)` - Task planning with approvals +- `simple_agent(client)` - Basic agentic chat (Feature 1) +- `weather_agent(client)` - Backend tool rendering (Feature 2) +- `human_in_the_loop_agent(client)` - Human-in-the-loop with step customization (Feature 3) +- `task_steps_agent_wrapped(client)` - Agentic generative UI with step execution (Feature 4) +- `ui_generator_agent(client)` - Tool-based generative UI (Feature 5) +- `recipe_agent(client)` - Shared state management (Feature 6) +- `document_writer_agent(client)` - Predictive state updates (Feature 7) +- `research_assistant_agent(client)` - Research with progress events +- `task_planner_agent(client)` - Task planning with approvals ### Using Example Agents @@ -97,7 +97,7 @@ from agent_framework_ag_ui_examples.agents import ( recipe_agent, ) -# Create a chat client (use any ChatClientProtocol implementation) +# Create a chat client (use any SupportsChatGetResponse implementation) azure_client = AzureOpenAIChatClient(model_id="gpt-4") openai_client = OpenAIChatClient(model_id="gpt-4o") @@ -150,16 +150,16 @@ from agent_framework_ag_ui_examples.agents import ( app = FastAPI(title="AG-UI Examples") # Create a chat client (shared across all agents, or create individual ones) -chat_client = AzureOpenAIChatClient(model_id="gpt-4") +client = AzureOpenAIChatClient(model_id="gpt-4") # Add all example endpoints -add_agent_framework_fastapi_endpoint(app, simple_agent(chat_client), "/agentic_chat") -add_agent_framework_fastapi_endpoint(app, weather_agent(chat_client), "/backend_tool_rendering") -add_agent_framework_fastapi_endpoint(app, human_in_the_loop_agent(chat_client), "/human_in_the_loop") -add_agent_framework_fastapi_endpoint(app, task_steps_agent_wrapped(chat_client), "/agentic_generative_ui") # type: ignore[arg-type] -add_agent_framework_fastapi_endpoint(app, ui_generator_agent(chat_client), "/tool_based_generative_ui") -add_agent_framework_fastapi_endpoint(app, recipe_agent(chat_client), "/shared_state") -add_agent_framework_fastapi_endpoint(app, document_writer_agent(chat_client), "/predictive_state_updates") +add_agent_framework_fastapi_endpoint(app, simple_agent(client), "/agentic_chat") +add_agent_framework_fastapi_endpoint(app, weather_agent(client), "/backend_tool_rendering") +add_agent_framework_fastapi_endpoint(app, human_in_the_loop_agent(client), "/human_in_the_loop") +add_agent_framework_fastapi_endpoint(app, task_steps_agent_wrapped(client), "/agentic_generative_ui") # type: ignore[arg-type] +add_agent_framework_fastapi_endpoint(app, ui_generator_agent(client), "/tool_based_generative_ui") +add_agent_framework_fastapi_endpoint(app, recipe_agent(client), "/shared_state") +add_agent_framework_fastapi_endpoint(app, document_writer_agent(client), "/predictive_state_updates") ``` ## Architecture @@ -187,8 +187,8 @@ The package uses a clean, orchestrator-based architecture: You can create your own agent factories following the same pattern as the examples: ```python -from agent_framework import ChatAgent, tool -from agent_framework import ChatClientProtocol +from agent_framework import Agent, tool +from agent_framework import SupportsChatGetResponse from agent_framework.ag_ui import AgentFrameworkAgent @tool @@ -196,19 +196,19 @@ def my_tool(param: str) -> str: """My custom tool.""" return f"Result: {param}" -def my_custom_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent: +def my_custom_agent(client: SupportsChatGetResponse) -> AgentFrameworkAgent: """Create a custom agent with the specified chat client. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: A configured AgentFrameworkAgent instance """ - agent = ChatAgent( + agent = Agent( name="my_custom_agent", instructions="Custom instructions here", - chat_client=chat_client, + client=client, tools=[my_tool], ) @@ -220,8 +220,8 @@ def my_custom_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent: # Use it from agent_framework.azure import AzureOpenAIChatClient -chat_client = AzureOpenAIChatClient() -agent = my_custom_agent(chat_client) +client = AzureOpenAIChatClient() +agent = my_custom_agent(client) ``` ### Shared State @@ -229,14 +229,14 @@ agent = my_custom_agent(chat_client) State is injected as system messages and updated via predictive state updates: ```python -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureOpenAIChatClient from agent_framework.ag_ui import AgentFrameworkAgent # Create your agent -agent = ChatAgent( +agent = Agent( name="recipe_agent", - chat_client=AzureOpenAIChatClient(model_id="gpt-4o"), + client=AzureOpenAIChatClient(model_id="gpt-4o"), ) state_schema = { @@ -266,14 +266,14 @@ wrapped_agent = AgentFrameworkAgent( Predictive state updates automatically stream tool arguments as optimistic state updates: ```python -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureOpenAIChatClient from agent_framework.ag_ui import AgentFrameworkAgent # Create your agent -agent = ChatAgent( +agent = Agent( name="document_writer", - chat_client=AzureOpenAIChatClient(model_id="gpt-4o"), + client=AzureOpenAIChatClient(model_id="gpt-4o"), ) predict_state_config = { diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/document_writer_agent.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/document_writer_agent.py index 3a74af346a..427583a589 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/document_writer_agent.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/document_writer_agent.py @@ -4,7 +4,7 @@ from __future__ import annotations -from agent_framework import ChatAgent, ChatClientProtocol, tool +from agent_framework import Agent, SupportsChatGetResponse, tool from agent_framework.ag_ui import AgentFrameworkAgent @@ -40,19 +40,19 @@ _DOCUMENT_WRITER_INSTRUCTIONS = ( ) -def document_writer_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent: +def document_writer_agent(client: SupportsChatGetResponse) -> AgentFrameworkAgent: """Create a document writer agent with predictive state updates. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: A configured AgentFrameworkAgent instance with document writing capabilities """ - agent = ChatAgent( + agent = Agent( name="document_writer", instructions=_DOCUMENT_WRITER_INSTRUCTIONS, - chat_client=chat_client, + client=client, tools=[write_document], ) diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/human_in_the_loop_agent.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/human_in_the_loop_agent.py index 368c4e47ed..b04b6619e4 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/human_in_the_loop_agent.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/human_in_the_loop_agent.py @@ -5,7 +5,7 @@ from enum import Enum from typing import Any -from agent_framework import ChatAgent, ChatClientProtocol, tool +from agent_framework import Agent, SupportsChatGetResponse, tool from pydantic import BaseModel, Field @@ -43,16 +43,16 @@ def generate_task_steps(steps: list[TaskStep]) -> str: return f"Generated {len(steps)} execution steps for the task." -def human_in_the_loop_agent(chat_client: ChatClientProtocol[Any]) -> ChatAgent[Any]: +def human_in_the_loop_agent(client: SupportsChatGetResponse[Any]) -> Agent[Any]: """Create a human-in-the-loop agent using tool-based approach for predictive state. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: - A configured ChatAgent instance with human-in-the-loop capabilities + A configured Agent instance with human-in-the-loop capabilities """ - return ChatAgent( + return Agent( name="human_in_the_loop_agent", instructions="""You are a helpful assistant that can perform any task by breaking it down into steps. @@ -81,6 +81,6 @@ def human_in_the_loop_agent(chat_client: ChatClientProtocol[Any]) -> ChatAgent[A After the user approves and the function executes, THEN provide a brief acknowledgment like: "The plan has been created with X steps selected." """, - chat_client=chat_client, + client=client, tools=[generate_task_steps], ) diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/recipe_agent.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/recipe_agent.py index 2d9bb066ba..f2d1aecdff 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/recipe_agent.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/recipe_agent.py @@ -7,7 +7,7 @@ from __future__ import annotations from enum import Enum from typing import Any -from agent_framework import ChatAgent, ChatClientProtocol, tool +from agent_framework import Agent, SupportsChatGetResponse, tool from agent_framework.ag_ui import AgentFrameworkAgent from pydantic import BaseModel, Field @@ -104,19 +104,19 @@ _RECIPE_INSTRUCTIONS = """You are a helpful recipe assistant that creates and mo """ -def recipe_agent(chat_client: ChatClientProtocol[Any]) -> AgentFrameworkAgent: +def recipe_agent(client: SupportsChatGetResponse[Any]) -> AgentFrameworkAgent: """Create a recipe agent with streaming state updates. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: A configured AgentFrameworkAgent instance with recipe management """ - agent = ChatAgent( + agent = Agent( name="recipe_agent", instructions=_RECIPE_INSTRUCTIONS, - chat_client=chat_client, + client=client, tools=[update_recipe], ) diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/research_assistant_agent.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/research_assistant_agent.py index b92874421a..fbd6a0b89c 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/research_assistant_agent.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/research_assistant_agent.py @@ -5,7 +5,7 @@ import asyncio from typing import Any -from agent_framework import ChatAgent, ChatClientProtocol, tool +from agent_framework import Agent, SupportsChatGetResponse, tool from agent_framework.ag_ui import AgentFrameworkAgent @@ -88,19 +88,19 @@ _RESEARCH_ASSISTANT_INSTRUCTIONS = ( ) -def research_assistant_agent(chat_client: ChatClientProtocol[Any]) -> AgentFrameworkAgent: +def research_assistant_agent(client: SupportsChatGetResponse[Any]) -> AgentFrameworkAgent: """Create a research assistant agent. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: A configured AgentFrameworkAgent instance with research capabilities """ - agent = ChatAgent( + agent = Agent( name="research_assistant", instructions=_RESEARCH_ASSISTANT_INSTRUCTIONS, - chat_client=chat_client, + client=client, tools=[research_topic, create_presentation, analyze_data], ) diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/simple_agent.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/simple_agent.py index 3e72fd3a11..5be88cbfd3 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/simple_agent.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/simple_agent.py @@ -4,20 +4,20 @@ from typing import Any -from agent_framework import ChatAgent, ChatClientProtocol +from agent_framework import Agent, SupportsChatGetResponse -def simple_agent(chat_client: ChatClientProtocol[Any]) -> ChatAgent[Any]: +def simple_agent(client: SupportsChatGetResponse[Any]) -> Agent[Any]: """Create a simple chat agent. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: - A configured ChatAgent instance + A configured Agent instance """ - return ChatAgent[Any]( + return Agent[Any]( name="simple_chat_agent", instructions="You are a helpful assistant. Be concise and friendly.", - chat_client=chat_client, + client=client, ) diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/task_planner_agent.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/task_planner_agent.py index 57e14bb6c3..18065dd15f 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/task_planner_agent.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/task_planner_agent.py @@ -4,7 +4,7 @@ from typing import Any -from agent_framework import ChatAgent, ChatClientProtocol, tool +from agent_framework import Agent, SupportsChatGetResponse, tool from agent_framework.ag_ui import AgentFrameworkAgent @@ -61,19 +61,19 @@ _TASK_PLANNER_INSTRUCTIONS = ( ) -def task_planner_agent(chat_client: ChatClientProtocol[Any]) -> AgentFrameworkAgent: +def task_planner_agent(client: SupportsChatGetResponse[Any]) -> AgentFrameworkAgent: """Create a task planner agent with user approval for actions. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: A configured AgentFrameworkAgent instance with task planning capabilities """ - agent = ChatAgent( + agent = Agent( name="task_planner", instructions=_TASK_PLANNER_INSTRUCTIONS, - chat_client=chat_client, + client=client, tools=[create_calendar_event, send_email, book_meeting_room], ) diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/task_steps_agent.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/task_steps_agent.py index 2fe79d063f..be2da28a9d 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/task_steps_agent.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/task_steps_agent.py @@ -20,7 +20,7 @@ from ag_ui.core import ( TextMessageStartEvent, ToolCallStartEvent, ) -from agent_framework import ChatAgent, ChatClientProtocol, ChatMessage, Content, tool +from agent_framework import Agent, Content, Message, SupportsChatGetResponse, tool from agent_framework.ag_ui import AgentFrameworkAgent from pydantic import BaseModel, Field @@ -54,16 +54,16 @@ def generate_task_steps(steps: list[TaskStep]) -> str: return "Steps generated." -def _create_task_steps_agent(chat_client: ChatClientProtocol[Any]) -> AgentFrameworkAgent: +def _create_task_steps_agent(client: SupportsChatGetResponse[Any]) -> AgentFrameworkAgent: """Create the task steps agent using tool-based approach for streaming. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: A configured AgentFrameworkAgent instance """ - agent = ChatAgent[Any]( + agent = Agent[Any]( name="task_steps_agent", instructions="""You are a helpful assistant that breaks down tasks into actionable steps. @@ -83,7 +83,7 @@ def _create_task_steps_agent(chat_client: ChatClientProtocol[Any]) -> AgentFrame - "Installing platform" - "Adding finishing touches" """, - chat_client=chat_client, + client=client, tools=[generate_task_steps], ) @@ -220,30 +220,30 @@ class TaskStepsAgentWithExecution: # Get the underlying chat agent and client chat_agent = self._base_agent.agent # type: ignore - chat_client = chat_agent.chat_client # type: ignore + client = chat_agent.client # type: ignore # Build messages for summary call original_messages = input_data.get("messages", []) - # Convert to ChatMessage objects if needed - messages: list[ChatMessage] = [] + # Convert to Message objects if needed + messages: list[Message] = [] for msg in original_messages: if isinstance(msg, dict): content_str = msg.get("content", "") if isinstance(content_str, str): messages.append( - ChatMessage( + Message( role=msg.get("role", "user"), contents=[Content.from_text(text=content_str)], ) ) - elif isinstance(msg, ChatMessage): + elif isinstance(msg, Message): messages.append(msg) # Add completion message messages.append( - ChatMessage( + Message( role="user", contents=[ Content.from_text( @@ -270,7 +270,7 @@ class TaskStepsAgentWithExecution: # Stream completion accumulated_text = "" - async for chunk in chat_client.get_response(messages=messages, stream=True): + async for chunk in client.get_response(messages=messages, stream=True): # chunk is ChatResponseUpdate if hasattr(chunk, "text") and chunk.text: accumulated_text += chunk.text @@ -332,14 +332,14 @@ class TaskStepsAgentWithExecution: yield run_finished_event -def task_steps_agent_wrapped(chat_client: ChatClientProtocol[Any]) -> TaskStepsAgentWithExecution: +def task_steps_agent_wrapped(client: SupportsChatGetResponse[Any]) -> TaskStepsAgentWithExecution: """Create a task steps agent with execution simulation. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: A wrapped agent instance with step execution simulation """ - base_agent = _create_task_steps_agent(chat_client) + base_agent = _create_task_steps_agent(client) return TaskStepsAgentWithExecution(base_agent) diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/ui_generator_agent.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/ui_generator_agent.py index 3f50fc9c07..961f276603 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/ui_generator_agent.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/ui_generator_agent.py @@ -7,7 +7,7 @@ from __future__ import annotations import sys from typing import TYPE_CHECKING, Any, TypedDict -from agent_framework import ChatAgent, ChatClientProtocol, FunctionTool +from agent_framework import Agent, FunctionTool, SupportsChatGetResponse from agent_framework.ag_ui import AgentFrameworkAgent if sys.version_info >= (3, 13): @@ -168,19 +168,19 @@ _UI_GENERATOR_INSTRUCTIONS = """You MUST use the provided tools to generate cont OptionsT = TypeVar("OptionsT", bound=TypedDict, default="ChatOptions") # type: ignore[valid-type] -def ui_generator_agent(chat_client: ChatClientProtocol[OptionsT]) -> AgentFrameworkAgent: +def ui_generator_agent(client: SupportsChatGetResponse[OptionsT]) -> AgentFrameworkAgent: """Create a UI generator agent with custom React component rendering. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: A configured AgentFrameworkAgent instance with UI generation capabilities """ - agent = ChatAgent( + agent = Agent( name="ui_generator", instructions=_UI_GENERATOR_INSTRUCTIONS, - chat_client=chat_client, + client=client, tools=[generate_haiku, create_chart, display_timeline, show_comparison_table], # Force tool usage - the LLM MUST call a tool, cannot respond with plain text default_options={"tool_choice": "required"}, # type: ignore diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/weather_agent.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/weather_agent.py index f8b03c2d0e..7e80fccfd7 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/weather_agent.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/weather_agent.py @@ -6,7 +6,7 @@ from __future__ import annotations from typing import Any -from agent_framework import ChatAgent, ChatClientProtocol, tool +from agent_framework import Agent, SupportsChatGetResponse, tool @tool @@ -59,16 +59,16 @@ def get_forecast(location: str, days: int = 3) -> str: return f"{days}-day forecast for {location}:\n" + "\n".join(forecast) -def weather_agent(chat_client: ChatClientProtocol[Any]) -> ChatAgent[Any]: +def weather_agent(client: SupportsChatGetResponse[Any]) -> Agent[Any]: """Create a weather agent with get_weather and get_forecast tools. Args: - chat_client: The chat client to use for the agent + client: The chat client to use for the agent Returns: - A configured ChatAgent instance with weather tools + A configured Agent instance with weather tools """ - return ChatAgent[Any]( + return Agent[Any]( name="weather_agent", instructions=( "You are a helpful weather assistant. " @@ -76,6 +76,6 @@ def weather_agent(chat_client: ChatClientProtocol[Any]) -> ChatAgent[Any]: "Always provide friendly and informative responses. " "First return the weather result, and then return details about the forecast." ), - chat_client=chat_client, + client=client, tools=[get_weather, get_forecast], ) diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/server/api/backend_tool_rendering.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/server/api/backend_tool_rendering.py index 915e57c6e2..b18fc103e8 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/server/api/backend_tool_rendering.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/server/api/backend_tool_rendering.py @@ -4,7 +4,7 @@ from typing import Any, cast -from agent_framework._clients import ChatClientProtocol +from agent_framework._clients import SupportsChatGetResponse from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint from agent_framework.azure import AzureOpenAIChatClient from fastapi import FastAPI @@ -19,10 +19,10 @@ def register_backend_tool_rendering(app: FastAPI) -> None: app: The FastAPI application. """ # Create a chat client and call the factory function - chat_client = cast(ChatClientProtocol[Any], AzureOpenAIChatClient()) + client = cast(SupportsChatGetResponse[Any], AzureOpenAIChatClient()) add_agent_framework_fastapi_endpoint( app, - weather_agent(chat_client), + weather_agent(client), "/backend_tool_rendering", ) diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/server/main.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/server/main.py index 8c2f4be261..f45b30816f 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/server/main.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/server/main.py @@ -10,7 +10,7 @@ from typing import cast import uvicorn from agent_framework import ChatOptions -from agent_framework._clients import ChatClientProtocol +from agent_framework._clients import SupportsChatGetResponse from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint from agent_framework.anthropic import AnthropicClient from agent_framework.azure import AzureOpenAIChatClient @@ -67,43 +67,43 @@ app.add_middleware( # Create a shared chat client for all agents # You can use different chat clients for different agents if needed # Set CHAT_CLIENT=anthropic to use Anthropic, defaults to Azure OpenAI -chat_client: ChatClientProtocol[ChatOptions] = cast( - ChatClientProtocol[ChatOptions], +client: SupportsChatGetResponse[ChatOptions] = cast( + SupportsChatGetResponse[ChatOptions], AnthropicClient() if os.getenv("CHAT_CLIENT", "").lower() == "anthropic" else AzureOpenAIChatClient(), ) # Agentic Chat - basic chat agent add_agent_framework_fastapi_endpoint( app=app, - agent=simple_agent(chat_client), + agent=simple_agent(client), path="/agentic_chat", ) # Backend Tool Rendering - agent with tools add_agent_framework_fastapi_endpoint( app=app, - agent=weather_agent(chat_client), + agent=weather_agent(client), path="/backend_tool_rendering", ) # Shared State - recipe agent with structured output add_agent_framework_fastapi_endpoint( app=app, - agent=recipe_agent(chat_client), + agent=recipe_agent(client), path="/shared_state", ) # Predictive State Updates - document writer with predictive state add_agent_framework_fastapi_endpoint( app=app, - agent=document_writer_agent(chat_client), + agent=document_writer_agent(client), path="/predictive_state_updates", ) # Human in the Loop - human-in-the-loop agent with step customization add_agent_framework_fastapi_endpoint( app=app, - agent=human_in_the_loop_agent(chat_client), + agent=human_in_the_loop_agent(client), path="/human_in_the_loop", state_schema={"steps": {"type": "array"}}, predict_state_config={"steps": {"tool": "generate_task_steps", "tool_argument": "steps"}}, @@ -112,14 +112,14 @@ add_agent_framework_fastapi_endpoint( # Agentic Generative UI - task steps agent with streaming state updates add_agent_framework_fastapi_endpoint( app=app, - agent=task_steps_agent_wrapped(chat_client), # type: ignore[arg-type] + agent=task_steps_agent_wrapped(client), # type: ignore[arg-type] path="/agentic_generative_ui", ) # Tool-based Generative UI - UI generator with frontend-rendered tools add_agent_framework_fastapi_endpoint( app=app, - agent=ui_generator_agent(chat_client), + agent=ui_generator_agent(client), path="/tool_based_generative_ui", ) diff --git a/python/packages/ag-ui/getting_started/README.md b/python/packages/ag-ui/getting_started/README.md index 9421935a4d..d3d14694a5 100644 --- a/python/packages/ag-ui/getting_started/README.md +++ b/python/packages/ag-ui/getting_started/README.md @@ -35,9 +35,9 @@ python client_advanced.py **Note:** This example shows direct `AGUIChatClient` usage. Tool execution and conversation continuity depend on server-side configuration and capabilities. -### ChatAgent Integration (`client_with_agent.py`) +### Agent Integration (`client_with_agent.py`) -Best practice example using `ChatAgent` wrapper with **AgentThread** +Best practice example using `Agent` wrapper with **AgentThread** - **AgentThread** maintains conversation state - Client-side conversation history management via `thread.message_store` - **Hybrid tool execution**: client-side + server-side tools simultaneously @@ -77,7 +77,7 @@ The AG-UI protocol supports two approaches to conversation history: - Full message history sent with each request - Works with any AG-UI server (stateful or stateless) -The `ChatAgent` wrapper (used in client_with_agent.py) collects messages from local storage and sends the full history to `AGUIChatClient`, which then forwards everything to the server. +The `Agent` wrapper (used in client_with_agent.py) collects messages from local storage and sends the full history to `AGUIChatClient`, which then forwards everything to the server. ### Tool/Function Calling @@ -91,14 +91,14 @@ Client defines: Server defines: User: "What's the weather in SF and what time is it?" ↓ -ChatAgent sends: full history + tool definitions for get_weather, read_sensors +Agent sends: full history + tool definitions for get_weather, read_sensors ↓ Server LLM decides: "I need get_weather('SF') and get_current_time()" ↓ Server executes get_current_time() → "2025-11-11 14:30:00 UTC" Server sends function call request → get_weather('SF') ↓ -ChatAgent intercepts get_weather call → executes locally +Agent intercepts get_weather call → executes locally ↓ Client sends result → "Sunny, 72°F" ↓ @@ -110,7 +110,7 @@ Client receives final response **How it works:** 1. **Client-Side Tools** (`client_with_agent.py`): - - Tools defined in ChatAgent's `tools` parameter execute locally + - Tools defined in Agent's `tools` parameter execute locally - Tool metadata (name, description, schema) sent to server for planning - When server requests client tool → client intercepts → executes locally → sends result @@ -126,7 +126,7 @@ Client receives final response - Client tools execute client-side **Direct AGUIChatClient Usage** (client_advanced.py): -Even without ChatAgent wrapper, client-side tools work: +Even without Agent wrapper, client-side tools work: - Tools passed in ChatOptions execute locally - Server can also have its own tools - Hybrid execution works automatically @@ -184,7 +184,7 @@ Create a file named `server.py`: import os -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureOpenAIChatClient from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint from fastapi import FastAPI @@ -202,10 +202,10 @@ if not api_key: raise ValueError("AZURE_OPENAI_API_KEY environment variable is required") # Create the AI agent -agent = ChatAgent( +agent = Agent( name="AGUIAssistant", instructions="You are a helpful assistant.", - chat_client=AzureOpenAIChatClient( + client=AzureOpenAIChatClient( endpoint=endpoint, deployment_name=deployment_name, api_key=api_key, @@ -227,7 +227,7 @@ if __name__ == "__main__": ### Key Concepts - **`add_agent_framework_fastapi_endpoint`**: Registers the AG-UI endpoint with automatic request/response handling and SSE streaming -- **`ChatAgent`**: The agent that will handle incoming requests +- **`Agent`**: The agent that will handle incoming requests - **FastAPI Integration**: Uses FastAPI's native async support for streaming responses - **Instructions**: The agent is created with default instructions, which can be overridden by client messages - **Configuration**: `AzureOpenAIChatClient` can read from environment variables (`AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`, `AZURE_OPENAI_API_KEY`) or accept parameters directly @@ -236,10 +236,10 @@ if __name__ == "__main__": ```python # No need to read environment variables manually -agent = ChatAgent( +agent = Agent( name="AGUIAssistant", instructions="You are a helpful assistant.", - chat_client=AzureOpenAIChatClient(), # Reads from environment automatically + client=AzureOpenAIChatClient(), # Reads from environment automatically ) ``` @@ -354,7 +354,7 @@ if __name__ == "__main__": - **Thread Management**: Pass `thread_id` in metadata to maintain conversation context across requests - **Streaming Responses**: Use `get_response(..., stream=True)` for real-time streaming or `get_response(..., stream=False)` for non-streaming - **Context Manager**: Use `async with` for automatic cleanup of HTTP connections -- **Standard Interface**: Works with all Agent Framework patterns (ChatAgent, tools, etc.) +- **Standard Interface**: Works with all Agent Framework patterns (Agent, tools, etc.) - **Hybrid Tool Execution**: Supports both client-side and server-side tools executing together in the same conversation ### Configure and Run the Client diff --git a/python/packages/ag-ui/getting_started/client_advanced.py b/python/packages/ag-ui/getting_started/client_advanced.py index 65f5e896bf..dcb4e5ca3c 100644 --- a/python/packages/ag-ui/getting_started/client_advanced.py +++ b/python/packages/ag-ui/getting_started/client_advanced.py @@ -114,15 +114,15 @@ async def non_streaming_example(client: AGUIChatClient, thread_id: str | None = async def tool_example(client: AGUIChatClient, thread_id: str | None = None): """Demonstrate sending tool definitions to the server. - IMPORTANT: When using AGUIChatClient directly (without ChatAgent wrapper): + IMPORTANT: When using AGUIChatClient directly (without Agent wrapper): - Tools are sent as DEFINITIONS only - No automatic client-side execution (no function invocation middleware) - Server must have matching tool implementations to execute them For CLIENT-SIDE tool execution (like .NET AGUIClient sample): - - Use ChatAgent wrapper with tools + - Use Agent wrapper with tools - See client_with_agent.py for the hybrid pattern - - ChatAgent middleware intercepts and executes client tools locally + - Agent middleware intercepts and executes client tools locally - Server can have its own tools that execute server-side - Both client and server tools work together in same conversation @@ -186,7 +186,7 @@ async def conversation_example(client: AGUIChatClient): # Check if context was maintained if "alice" not in response2.text.lower(): - print("\n[Note: Server may not maintain thread context - consider using ChatAgent for history management]") + print("\n[Note: Server may not maintain thread context - consider using Agent for history management]") # Third turn print("\nUser: Can you also tell me what 10 * 5 is?\n") diff --git a/python/packages/ag-ui/getting_started/client_with_agent.py b/python/packages/ag-ui/getting_started/client_with_agent.py index 5d9917327b..f0d7630294 100644 --- a/python/packages/ag-ui/getting_started/client_with_agent.py +++ b/python/packages/ag-ui/getting_started/client_with_agent.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -"""Example showing ChatAgent with AGUIChatClient for hybrid tool execution. +"""Example showing Agent with AGUIChatClient for hybrid tool execution. This demonstrates the HYBRID pattern matching .NET AGUIClient implementation: @@ -24,7 +24,7 @@ import asyncio import logging import os -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.ag_ui import AGUIChatClient # Enable debug logging @@ -55,7 +55,7 @@ def get_weather(location: str) -> str: async def main(): - """Demonstrate ChatAgent + AGUIChatClient hybrid tool execution. + """Demonstrate Agent + AGUIChatClient hybrid tool execution. This matches the .NET pattern from Program.cs where: - AIAgent agent = chatClient.CreateAIAgent(tools: [...]) @@ -63,14 +63,14 @@ async def main(): - RunStreamingAsync(messages, thread) Python equivalent: - - agent = ChatAgent(chat_client=AGUIChatClient(...), tools=[...]) + - agent = Agent(client=AGUIChatClient(...), tools=[...]) - thread = agent.get_new_thread() # Creates thread with message_store - agent.run(message, stream=True, thread=thread) # Thread accumulates history """ server_url = os.environ.get("AGUI_SERVER_URL", "http://127.0.0.1:5100/") print("=" * 70) - print("ChatAgent + AGUIChatClient: Hybrid Tool Execution") + print("Agent + AGUIChatClient: Hybrid Tool Execution") print("=" * 70) print(f"\nServer: {server_url}") print("\nThis example demonstrates:") @@ -82,11 +82,11 @@ async def main(): try: # Create remote client in async context manager async with AGUIChatClient(endpoint=server_url) as remote_client: - # Wrap in ChatAgent for conversation history management - agent = ChatAgent( + # Wrap in Agent for conversation history management + agent = Agent( name="remote_assistant", instructions="You are a helpful assistant. Remember user information across the conversation.", - chat_client=remote_client, + client=remote_client, tools=[get_weather], ) diff --git a/python/packages/ag-ui/getting_started/server.py b/python/packages/ag-ui/getting_started/server.py index fa3f21c3e7..8d32009fb1 100644 --- a/python/packages/ag-ui/getting_started/server.py +++ b/python/packages/ag-ui/getting_started/server.py @@ -7,7 +7,7 @@ from __future__ import annotations import logging import os -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint from agent_framework.azure import AzureOpenAIChatClient from dotenv import load_dotenv @@ -116,10 +116,10 @@ def get_time_zone(location: str) -> str: # The client will send get_weather tool metadata so the LLM knows about it, # and the function invocation mixin on AGUIChatClient will execute it client-side. # This matches the .NET AG-UI hybrid execution pattern. -agent = ChatAgent( +agent = Agent( name="AGUIAssistant", instructions="You are a helpful assistant. Use get_weather for weather and get_time_zone for time zones.", - chat_client=AzureOpenAIChatClient( + client=AzureOpenAIChatClient( endpoint=endpoint, deployment_name=deployment_name, ), diff --git a/python/packages/ag-ui/pyproject.toml b/python/packages/ag-ui/pyproject.toml index 128c684d35..991f33d78c 100644 --- a/python/packages/ag-ui/pyproject.toml +++ b/python/packages/ag-ui/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-framework-ag-ui" -version = "1.0.0b260130" +version = "1.0.0b260210" description = "AG-UI protocol integration for Agent Framework" readme = "README.md" license-files = ["LICENSE"] @@ -22,7 +22,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "ag-ui-protocol>=0.1.9", "fastapi>=0.115.0", "uvicorn>=0.30.0" diff --git a/python/packages/ag-ui/tests/ag_ui/conftest.py b/python/packages/ag-ui/tests/ag_ui/conftest.py index 4612750f5f..82f6267863 100644 --- a/python/packages/ag-ui/tests/ag_ui/conftest.py +++ b/python/packages/ag-ui/tests/ag_ui/conftest.py @@ -13,13 +13,13 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseChatClient, - ChatClientProtocol, - ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, Content, + Message, SupportsAgentRun, + SupportsChatGetResponse, ) from agent_framework._clients import OptionsCoT from agent_framework._middleware import ChatMiddlewareLayer @@ -43,7 +43,7 @@ class StreamingChatClientStub( BaseChatClient[OptionsCoT], Generic[OptionsCoT], ): - """Typed streaming stub that satisfies ChatClientProtocol.""" + """Typed streaming stub that satisfies SupportsChatGetResponse.""" def __init__(self, stream_fn: StreamFn, response_fn: ResponseFn | None = None) -> None: super().__init__(function_middleware=[]) @@ -55,7 +55,7 @@ class StreamingChatClientStub( @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: ChatOptions[Any], @@ -65,7 +65,7 @@ class StreamingChatClientStub( @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: OptionsCoT | ChatOptions[None] | None = ..., @@ -75,7 +75,7 @@ class StreamingChatClientStub( @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[True], options: OptionsCoT | ChatOptions[Any] | None = ..., @@ -84,7 +84,7 @@ class StreamingChatClientStub( def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: bool = False, options: OptionsCoT | ChatOptions[Any] | None = None, @@ -106,7 +106,7 @@ class StreamingChatClientStub( def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], stream: bool = False, options: Mapping[str, Any], **kwargs: Any, @@ -121,7 +121,7 @@ class StreamingChatClientStub( return self._get_response_impl(messages, options, **kwargs) async def _get_response_impl( - self, messages: Sequence[ChatMessage], options: Mapping[str, Any], **kwargs: Any + self, messages: Sequence[Message], options: Mapping[str, Any], **kwargs: Any ) -> ChatResponse: """Non-streaming implementation.""" if self._response_fn is not None: @@ -132,7 +132,7 @@ class StreamingChatClientStub( contents.extend(update.contents) return ChatResponse( - messages=[ChatMessage(role="assistant", contents=contents)], + messages=[Message(role="assistant", contents=contents)], response_id="stub-response", ) @@ -141,7 +141,7 @@ def stream_from_updates(updates: list[ChatResponseUpdate]) -> StreamFn: """Create a stream function that yields from a static list of updates.""" async def _stream( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: for update in updates: yield update @@ -159,7 +159,7 @@ class StubAgent(SupportsAgentRun): agent_id: str = "stub-agent", agent_name: str | None = "stub-agent", default_options: Any | None = None, - chat_client: Any | None = None, + client: Any | None = None, ) -> None: self.id = agent_id self.name = agent_name @@ -168,14 +168,14 @@ class StubAgent(SupportsAgentRun): self.default_options: dict[str, Any] = ( default_options if isinstance(default_options, dict) else {"tools": None, "response_format": None} ) - self.chat_client = chat_client or SimpleNamespace(function_invocation_configuration=None) + self.client = client or SimpleNamespace(function_invocation_configuration=None) self.messages_received: list[Any] = [] self.tools_received: list[Any] | None = None @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[False] = ..., thread: AgentThread | None = None, @@ -185,7 +185,7 @@ class StubAgent(SupportsAgentRun): @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[True], thread: AgentThread | None = None, @@ -194,7 +194,7 @@ class StubAgent(SupportsAgentRun): def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -226,7 +226,7 @@ class StubAgent(SupportsAgentRun): @pytest.fixture -def streaming_chat_client_stub() -> type[ChatClientProtocol]: +def streaming_chat_client_stub() -> type[SupportsChatGetResponse]: """Return the StreamingChatClientStub class for creating test instances.""" return StreamingChatClientStub # type: ignore[return-value] diff --git a/python/packages/ag-ui/tests/ag_ui/test_ag_ui_client.py b/python/packages/ag-ui/tests/ag_ui/test_ag_ui_client.py index b5dc73bd02..7e7fd7cded 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_ag_ui_client.py +++ b/python/packages/ag-ui/tests/ag_ui/test_ag_ui_client.py @@ -7,11 +7,11 @@ from collections.abc import AsyncGenerator, Awaitable, MutableSequence from typing import Any from agent_framework import ( - ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, Content, + Message, ResponseStream, tool, ) @@ -29,13 +29,11 @@ class TestableAGUIChatClient(AGUIChatClient): """Expose http service for monkeypatching.""" return self._http_service - def extract_state_from_messages( - self, messages: list[ChatMessage] - ) -> tuple[list[ChatMessage], dict[str, Any] | None]: + def extract_state_from_messages(self, messages: list[Message]) -> tuple[list[Message], dict[str, Any] | None]: """Expose state extraction helper.""" return self._extract_state_from_messages(messages) - def convert_messages_to_agui_format(self, messages: list[ChatMessage]) -> list[dict[str, Any]]: + def convert_messages_to_agui_format(self, messages: list[Message]) -> list[dict[str, Any]]: """Expose message conversion helper.""" return self._convert_messages_to_agui_format(messages) @@ -44,7 +42,7 @@ class TestableAGUIChatClient(AGUIChatClient): return self._get_thread_id(options) def inner_get_response( - self, *, messages: MutableSequence[ChatMessage], options: dict[str, Any], stream: bool = False + self, *, messages: MutableSequence[Message], options: dict[str, Any], stream: bool = False ) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]: """Proxy to protected response call.""" return self._inner_get_response(messages=messages, options=options, stream=stream) @@ -69,8 +67,8 @@ class TestAGUIChatClient: """Test state extraction when no state is present.""" client = TestableAGUIChatClient(endpoint="http://localhost:8888/") messages = [ - ChatMessage(role="user", text="Hello"), - ChatMessage(role="assistant", text="Hi there"), + Message(role="user", text="Hello"), + Message(role="assistant", text="Hi there"), ] result_messages, state = client.extract_state_from_messages(messages) @@ -89,8 +87,8 @@ class TestAGUIChatClient: state_b64 = base64.b64encode(state_json.encode("utf-8")).decode("utf-8") messages = [ - ChatMessage(role="user", text="Hello"), - ChatMessage( + Message(role="user", text="Hello"), + Message( role="user", contents=[Content.from_uri(uri=f"data:application/json;base64,{state_b64}")], ), @@ -112,7 +110,7 @@ class TestAGUIChatClient: state_b64 = base64.b64encode(invalid_json.encode("utf-8")).decode("utf-8") messages = [ - ChatMessage( + Message( role="user", contents=[Content.from_uri(uri=f"data:application/json;base64,{state_b64}")], ), @@ -127,8 +125,8 @@ class TestAGUIChatClient: """Test message conversion to AG-UI format.""" client = TestableAGUIChatClient(endpoint="http://localhost:8888/") messages = [ - ChatMessage(role="user", text="What is the weather?"), - ChatMessage(role="assistant", text="Let me check.", message_id="msg_123"), + Message(role="user", text="What is the weather?"), + Message(role="assistant", text="Let me check.", message_id="msg_123"), ] agui_messages = client.convert_messages_to_agui_format(messages) @@ -175,7 +173,7 @@ class TestAGUIChatClient: client = TestableAGUIChatClient(endpoint="http://localhost:8888/") monkeypatch.setattr(client.http_service, "post_run", mock_post_run) - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] chat_options = ChatOptions() updates: list[ChatResponseUpdate] = [] @@ -208,7 +206,7 @@ class TestAGUIChatClient: client = TestableAGUIChatClient(endpoint="http://localhost:8888/") monkeypatch.setattr(client.http_service, "post_run", mock_post_run) - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] chat_options = {} response = await client.inner_get_response(messages=messages, options=chat_options) @@ -251,7 +249,7 @@ class TestAGUIChatClient: client = TestableAGUIChatClient(endpoint="http://localhost:8888/") monkeypatch.setattr(client.http_service, "post_run", mock_post_run) - messages = [ChatMessage(role="user", text="Test with tools")] + messages = [Message(role="user", text="Test with tools")] chat_options = ChatOptions(tools=[test_tool]) response = await client.inner_get_response(messages=messages, options=chat_options) @@ -275,7 +273,7 @@ class TestAGUIChatClient: client = TestableAGUIChatClient(endpoint="http://localhost:8888/") monkeypatch.setattr(client.http_service, "post_run", mock_post_run) - messages = [ChatMessage(role="user", text="Test server tool execution")] + messages = [Message(role="user", text="Test server tool execution")] updates: list[ChatResponseUpdate] = [] async for update in client.get_response(messages, stream=True): @@ -317,7 +315,7 @@ class TestAGUIChatClient: client = TestableAGUIChatClient(endpoint="http://localhost:8888/") monkeypatch.setattr(client.http_service, "post_run", mock_post_run) - messages = [ChatMessage(role="user", text="Test server tool execution")] + messages = [Message(role="user", text="Test server tool execution")] async for _ in client.get_response( messages, stream=True, options={"tool_choice": "auto", "tools": [client_tool]} @@ -333,8 +331,8 @@ class TestAGUIChatClient: state_b64 = base64.b64encode(state_json.encode("utf-8")).decode("utf-8") messages = [ - ChatMessage(role="user", text="Hello"), - ChatMessage( + Message(role="user", text="Hello"), + Message( role="user", contents=[Content.from_uri(uri=f"data:application/json;base64,{state_b64}")], ), diff --git a/python/packages/ag-ui/tests/ag_ui/test_agent_wrapper_comprehensive.py b/python/packages/ag-ui/tests/ag_ui/test_agent_wrapper_comprehensive.py index b61aa1edd3..f597c081f4 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_agent_wrapper_comprehensive.py +++ b/python/packages/ag-ui/tests/ag_ui/test_agent_wrapper_comprehensive.py @@ -7,7 +7,7 @@ from collections.abc import AsyncIterator, MutableSequence from typing import Any import pytest -from agent_framework import ChatAgent, ChatMessage, ChatOptions, ChatResponseUpdate, Content +from agent_framework import Agent, ChatOptions, ChatResponseUpdate, Content, Message from pydantic import BaseModel @@ -16,12 +16,12 @@ async def test_agent_initialization_basic(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent[ChatOptions]( - chat_client=streaming_chat_client_stub(stream_fn), + agent = Agent[ChatOptions]( + client=streaming_chat_client_stub(stream_fn), name="test_agent", instructions="Test", ) @@ -38,11 +38,11 @@ async def test_agent_initialization_with_state_schema(streaming_chat_client_stub from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) state_schema: dict[str, dict[str, Any]] = {"document": {"type": "string"}} wrapper = AgentFrameworkAgent(agent=agent, state_schema=state_schema) @@ -54,11 +54,11 @@ async def test_agent_initialization_with_predict_state_config(streaming_chat_cli from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) predict_config = {"document": {"tool": "write_doc", "tool_argument": "content"}} wrapper = AgentFrameworkAgent(agent=agent, predict_state_config=predict_config) @@ -70,7 +70,7 @@ async def test_agent_initialization_with_pydantic_state_schema(streaming_chat_cl from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) @@ -78,7 +78,7 @@ async def test_agent_initialization_with_pydantic_state_schema(streaming_chat_cl document: str tags: list[str] = [] - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper_class_schema = AgentFrameworkAgent(agent=agent, state_schema=MyState) wrapper_instance_schema = AgentFrameworkAgent(agent=agent, state_schema=MyState(document="hi")) @@ -93,11 +93,11 @@ async def test_run_started_event_emission(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent) input_data = {"messages": [{"role": "user", "content": "Hi"}]} @@ -117,11 +117,11 @@ async def test_predict_state_custom_event_emission(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) predict_config = { "document": {"tool": "write_doc", "tool_argument": "content"}, "summary": {"tool": "summarize", "tool_argument": "text"}, @@ -149,11 +149,11 @@ async def test_initial_state_snapshot_with_schema(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) state_schema = {"document": {"type": "string"}} wrapper = AgentFrameworkAgent(agent=agent, state_schema=state_schema) @@ -179,11 +179,11 @@ async def test_state_initialization_object_type(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) state_schema: dict[str, dict[str, Any]] = {"recipe": {"type": "object", "properties": {}}} wrapper = AgentFrameworkAgent(agent=agent, state_schema=state_schema) @@ -206,11 +206,11 @@ async def test_state_initialization_array_type(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) state_schema: dict[str, dict[str, Any]] = {"steps": {"type": "array", "items": {}}} wrapper = AgentFrameworkAgent(agent=agent, state_schema=state_schema) @@ -233,11 +233,11 @@ async def test_run_finished_event_emission(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent) input_data = {"messages": [{"role": "user", "content": "Hi"}]} @@ -255,11 +255,11 @@ async def test_tool_result_confirm_changes_accepted(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Document updated")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent( agent=agent, state_schema={"document": {"type": "string"}}, @@ -302,11 +302,11 @@ async def test_tool_result_confirm_changes_rejected(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="OK")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent) # Simulate tool result message with rejection @@ -336,11 +336,11 @@ async def test_tool_result_function_approval_accepted(streaming_chat_client_stub from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="OK")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent) # Simulate tool result with multiple steps @@ -382,11 +382,11 @@ async def test_tool_result_function_approval_rejected(streaming_chat_client_stub from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="OK")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent) # Simulate tool result rejection with steps @@ -425,13 +425,13 @@ async def test_thread_metadata_tracking(streaming_chat_client_stub): captured_options: dict[str, Any] = {} async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: # Capture options to verify internal keys are NOT passed to chat client captured_options.update(options) yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent) input_data = { @@ -445,7 +445,7 @@ async def test_thread_metadata_tracking(streaming_chat_client_stub): events.append(event) # AG-UI internal metadata should be stored in thread.metadata - thread = agent.chat_client.last_thread + thread = agent.client.last_thread thread_metadata = thread.metadata if thread and hasattr(thread, "metadata") else {} assert thread_metadata.get("ag_ui_thread_id") == "test_thread_123" assert thread_metadata.get("ag_ui_run_id") == "test_run_456" @@ -467,13 +467,13 @@ async def test_state_context_injection(streaming_chat_client_stub): captured_options: dict[str, Any] = {} async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: # Capture options to verify internal keys are NOT passed to chat client captured_options.update(options) yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent( agent=agent, state_schema={"document": {"type": "string"}}, @@ -489,7 +489,7 @@ async def test_state_context_injection(streaming_chat_client_stub): events.append(event) # Current state should be stored in thread.metadata - thread = agent.chat_client.last_thread + thread = agent.client.last_thread thread_metadata = thread.metadata if thread and hasattr(thread, "metadata") else {} current_state = thread_metadata.get("current_state") if isinstance(current_state, str): @@ -506,11 +506,11 @@ async def test_no_messages_provided(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent) input_data: dict[str, Any] = {"messages": []} @@ -530,11 +530,11 @@ async def test_message_end_event_emission(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text="Hello world")]) - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent) input_data: dict[str, Any] = {"messages": [{"role": "user", "content": "Hi"}]} @@ -558,13 +558,13 @@ async def test_error_handling_with_exception(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: if False: yield ChatResponseUpdate(contents=[]) raise RuntimeError("Simulated failure") - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent) input_data: dict[str, Any] = {"messages": [{"role": "user", "content": "Hi"}]} @@ -579,13 +579,13 @@ async def test_json_decode_error_in_tool_result(streaming_chat_client_stub): from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: if False: yield ChatResponseUpdate(contents=[]) raise AssertionError("ChatClient should not be called with orphaned tool result") - agent = ChatAgent(name="test_agent", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test_agent", instructions="Test", client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent) # Send invalid JSON as tool result without preceding tool call @@ -618,13 +618,13 @@ async def test_agent_with_use_service_thread_is_false(streaming_chat_client_stub request_service_thread_id: str | None = None async def stream_fn( - messages: MutableSequence[ChatMessage], chat_options: ChatOptions, **kwargs: Any + messages: MutableSequence[Message], chat_options: ChatOptions, **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate( contents=[Content.from_text(text="Response")], response_id="resp_67890", conversation_id="conv_12345" ) - agent = ChatAgent(chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent, use_service_thread=False) input_data = {"messages": [{"role": "user", "content": "Hi"}], "thread_id": "conv_123456"} @@ -642,7 +642,7 @@ async def test_agent_with_use_service_thread_is_true(streaming_chat_client_stub) request_service_thread_id: str | None = None async def stream_fn( - messages: MutableSequence[ChatMessage], chat_options: ChatOptions, **kwargs: Any + messages: MutableSequence[Message], chat_options: ChatOptions, **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: nonlocal request_service_thread_id thread = kwargs.get("thread") @@ -651,7 +651,7 @@ async def test_agent_with_use_service_thread_is_true(streaming_chat_client_stub) contents=[Content.from_text(text="Response")], response_id="resp_67890", conversation_id="conv_12345" ) - agent = ChatAgent(chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(client=streaming_chat_client_stub(stream_fn)) wrapper = AgentFrameworkAgent(agent=agent, use_service_thread=True) input_data = {"messages": [{"role": "user", "content": "Hi"}], "thread_id": "conv_123456"} @@ -659,7 +659,7 @@ async def test_agent_with_use_service_thread_is_true(streaming_chat_client_stub) events: list[Any] = [] async for event in wrapper.run_agent(input_data): events.append(event) - request_service_thread_id = agent.chat_client.last_service_thread_id + request_service_thread_id = agent.client.last_service_thread_id assert request_service_thread_id == "conv_123456" # type: ignore[attr-defined] (service_thread_id should be set) @@ -679,15 +679,15 @@ async def test_function_approval_mode_executes_tool(streaming_chat_client_stub): return "2025/12/01 12:00:00" async def stream_fn( - messages: MutableSequence[ChatMessage], options: ChatOptions, **kwargs: Any + messages: MutableSequence[Message], options: ChatOptions, **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: # Capture the messages received by the chat client messages_received.clear() messages_received.extend(messages) yield ChatResponseUpdate(contents=[Content.from_text(text="Processing completed")]) - agent = ChatAgent( - chat_client=streaming_chat_client_stub(stream_fn), + agent = Agent( + client=streaming_chat_client_stub(stream_fn), name="test_agent", instructions="Test", tools=[get_datetime], @@ -770,17 +770,17 @@ async def test_function_approval_mode_rejection(streaming_chat_client_stub): return "All data deleted" async def stream_fn( - messages: MutableSequence[ChatMessage], options: ChatOptions, **kwargs: Any + messages: MutableSequence[Message], options: ChatOptions, **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: # Capture the messages received by the chat client messages_received.clear() messages_received.extend(messages) yield ChatResponseUpdate(contents=[Content.from_text(text="Operation cancelled")]) - agent = ChatAgent( + agent = Agent( name="test_agent", instructions="Test", - chat_client=streaming_chat_client_stub(stream_fn), + client=streaming_chat_client_stub(stream_fn), tools=[delete_all_data], ) wrapper = AgentFrameworkAgent(agent=agent) diff --git a/python/packages/ag-ui/tests/ag_ui/test_endpoint.py b/python/packages/ag-ui/tests/ag_ui/test_endpoint.py index 4c1f03a49d..6d80fff588 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_endpoint.py +++ b/python/packages/ag-ui/tests/ag_ui/test_endpoint.py @@ -5,7 +5,7 @@ import json import pytest -from agent_framework import ChatAgent, ChatResponseUpdate, Content +from agent_framework import Agent, ChatResponseUpdate, Content from fastapi import FastAPI, Header, HTTPException from fastapi.params import Depends from fastapi.testclient import TestClient @@ -28,7 +28,7 @@ def build_chat_client(streaming_chat_client_stub, stream_from_updates_fixture): async def test_add_endpoint_with_agent_protocol(build_chat_client): """Test adding endpoint with raw SupportsAgentRun.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent, path="/test-agent") @@ -42,7 +42,7 @@ async def test_add_endpoint_with_agent_protocol(build_chat_client): async def test_add_endpoint_with_wrapped_agent(build_chat_client): """Test adding endpoint with pre-wrapped AgentFrameworkAgent.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) wrapped_agent = AgentFrameworkAgent(agent=agent, name="wrapped") add_agent_framework_fastapi_endpoint(app, wrapped_agent, path="/wrapped-agent") @@ -57,7 +57,7 @@ async def test_add_endpoint_with_wrapped_agent(build_chat_client): async def test_endpoint_with_state_schema(build_chat_client): """Test endpoint with state_schema parameter.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) state_schema = {"document": {"type": "string"}} add_agent_framework_fastapi_endpoint(app, agent, path="/stateful", state_schema=state_schema) @@ -73,7 +73,7 @@ async def test_endpoint_with_state_schema(build_chat_client): async def test_endpoint_with_default_state_seed(build_chat_client): """Test endpoint seeds default state when client omits it.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) state_schema = {"proverbs": {"type": "array"}} default_state = {"proverbs": ["Keep the original."]} @@ -100,7 +100,7 @@ async def test_endpoint_with_default_state_seed(build_chat_client): async def test_endpoint_with_predict_state_config(build_chat_client): """Test endpoint with predict_state_config parameter.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) predict_config = {"document": {"tool": "write_doc", "tool_argument": "content"}} add_agent_framework_fastapi_endpoint(app, agent, path="/predictive", predict_state_config=predict_config) @@ -114,7 +114,7 @@ async def test_endpoint_with_predict_state_config(build_chat_client): async def test_endpoint_request_logging(build_chat_client): """Test that endpoint logs request details.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent, path="/logged") @@ -134,7 +134,7 @@ async def test_endpoint_request_logging(build_chat_client): async def test_endpoint_event_streaming(build_chat_client): """Test that endpoint streams events correctly.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client("Streamed response")) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client("Streamed response")) add_agent_framework_fastapi_endpoint(app, agent, path="/stream") @@ -168,7 +168,7 @@ async def test_endpoint_event_streaming(build_chat_client): async def test_endpoint_error_handling(build_chat_client): """Test endpoint error handling during request parsing.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent, path="/failing") @@ -184,8 +184,8 @@ async def test_endpoint_error_handling(build_chat_client): async def test_endpoint_multiple_paths(build_chat_client): """Test adding multiple endpoints with different paths.""" app = FastAPI() - agent1 = ChatAgent(name="agent1", instructions="First agent", chat_client=build_chat_client("Response 1")) - agent2 = ChatAgent(name="agent2", instructions="Second agent", chat_client=build_chat_client("Response 2")) + agent1 = Agent(name="agent1", instructions="First agent", client=build_chat_client("Response 1")) + agent2 = Agent(name="agent2", instructions="Second agent", client=build_chat_client("Response 2")) add_agent_framework_fastapi_endpoint(app, agent1, path="/agent1") add_agent_framework_fastapi_endpoint(app, agent2, path="/agent2") @@ -202,7 +202,7 @@ async def test_endpoint_multiple_paths(build_chat_client): async def test_endpoint_default_path(build_chat_client): """Test endpoint with default path.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent) @@ -215,7 +215,7 @@ async def test_endpoint_default_path(build_chat_client): async def test_endpoint_response_headers(build_chat_client): """Test that endpoint sets correct response headers.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent, path="/headers") @@ -231,7 +231,7 @@ async def test_endpoint_response_headers(build_chat_client): async def test_endpoint_empty_messages(build_chat_client): """Test endpoint with empty messages list.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent, path="/empty") @@ -244,7 +244,7 @@ async def test_endpoint_empty_messages(build_chat_client): async def test_endpoint_complex_input(build_chat_client): """Test endpoint with complex input data.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent, path="/complex") @@ -269,7 +269,7 @@ async def test_endpoint_complex_input(build_chat_client): async def test_endpoint_openapi_schema(build_chat_client): """Test that endpoint generates proper OpenAPI schema with request model.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent, path="/schema-test") @@ -313,7 +313,7 @@ async def test_endpoint_openapi_schema(build_chat_client): async def test_endpoint_default_tags(build_chat_client): """Test that endpoint uses default 'AG-UI' tag.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent, path="/default-tags") @@ -331,7 +331,7 @@ async def test_endpoint_default_tags(build_chat_client): async def test_endpoint_custom_tags(build_chat_client): """Test that endpoint accepts custom tags.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent, path="/custom-tags", tags=["Custom", "Agent"]) @@ -349,7 +349,7 @@ async def test_endpoint_custom_tags(build_chat_client): async def test_endpoint_missing_required_field(build_chat_client): """Test that endpoint validates required fields with Pydantic.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) add_agent_framework_fastapi_endpoint(app, agent, path="/validation") @@ -368,7 +368,7 @@ async def test_endpoint_internal_error_handling(build_chat_client): from unittest.mock import patch app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) # Use default_state to trigger the code path that can raise an exception add_agent_framework_fastapi_endpoint(app, agent, path="/error-test", default_state={"key": "value"}) @@ -387,7 +387,7 @@ async def test_endpoint_internal_error_handling(build_chat_client): async def test_endpoint_with_dependencies_blocks_unauthorized(build_chat_client): """Test that endpoint blocks requests when authentication dependency fails.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) async def require_api_key(x_api_key: str | None = Header(None)): if x_api_key != "secret-key": @@ -406,7 +406,7 @@ async def test_endpoint_with_dependencies_blocks_unauthorized(build_chat_client) async def test_endpoint_with_dependencies_allows_authorized(build_chat_client): """Test that endpoint allows requests when authentication dependency passes.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) async def require_api_key(x_api_key: str | None = Header(None)): if x_api_key != "secret-key": @@ -429,7 +429,7 @@ async def test_endpoint_with_dependencies_allows_authorized(build_chat_client): async def test_endpoint_with_multiple_dependencies(build_chat_client): """Test that endpoint supports multiple dependencies.""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) execution_order: list[str] = [] @@ -457,7 +457,7 @@ async def test_endpoint_with_multiple_dependencies(build_chat_client): async def test_endpoint_without_dependencies_is_accessible(build_chat_client): """Test that endpoint without dependencies remains accessible (backward compatibility).""" app = FastAPI() - agent = ChatAgent(name="test", instructions="Test agent", chat_client=build_chat_client()) + agent = Agent(name="test", instructions="Test agent", client=build_chat_client()) # No dependencies parameter - should be accessible without auth add_agent_framework_fastapi_endpoint(app, agent, path="/open") diff --git a/python/packages/ag-ui/tests/ag_ui/test_helpers.py b/python/packages/ag-ui/tests/ag_ui/test_helpers.py index b4a7e9f047..7173f5c6b3 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_helpers.py +++ b/python/packages/ag-ui/tests/ag_ui/test_helpers.py @@ -2,7 +2,7 @@ """Tests for orchestration helper functions.""" -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework_ag_ui._orchestration._helpers import ( approval_steps, @@ -29,8 +29,8 @@ class TestPendingToolCallIds: def test_no_tool_calls(self): """Returns empty set when no tool calls in messages.""" messages = [ - ChatMessage(role="user", contents=[Content.from_text("Hello")]), - ChatMessage(role="assistant", contents=[Content.from_text("Hi there")]), + Message(role="user", contents=[Content.from_text("Hello")]), + Message(role="assistant", contents=[Content.from_text("Hi there")]), ] result = pending_tool_call_ids(messages) assert result == set() @@ -38,7 +38,7 @@ class TestPendingToolCallIds: def test_pending_tool_call(self): """Returns pending tool call ID when no result exists.""" messages = [ - ChatMessage( + Message( role="assistant", contents=[Content.from_function_call(call_id="call_123", name="get_weather", arguments="{}")], ), @@ -49,11 +49,11 @@ class TestPendingToolCallIds: def test_resolved_tool_call(self): """Returns empty set when tool call has result.""" messages = [ - ChatMessage( + Message( role="assistant", contents=[Content.from_function_call(call_id="call_123", name="get_weather", arguments="{}")], ), - ChatMessage( + Message( role="tool", contents=[Content.from_function_result(call_id="call_123", result="sunny")], ), @@ -64,7 +64,7 @@ class TestPendingToolCallIds: def test_multiple_tool_calls_some_resolved(self): """Returns only unresolved tool call IDs.""" messages = [ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call(call_id="call_1", name="tool_a", arguments="{}"), @@ -72,11 +72,11 @@ class TestPendingToolCallIds: Content.from_function_call(call_id="call_3", name="tool_c", arguments="{}"), ], ), - ChatMessage( + Message( role="tool", contents=[Content.from_function_result(call_id="call_1", result="result_a")], ), - ChatMessage( + Message( role="tool", contents=[Content.from_function_result(call_id="call_3", result="result_c")], ), @@ -90,7 +90,7 @@ class TestIsStateContextMessage: def test_state_context_message(self): """Returns True for state context message.""" - message = ChatMessage( + message = Message( role="system", contents=[Content.from_text("Current state of the application: {}")], ) @@ -98,7 +98,7 @@ class TestIsStateContextMessage: def test_non_system_message(self): """Returns False for non-system message.""" - message = ChatMessage( + message = Message( role="user", contents=[Content.from_text("Current state of the application: {}")], ) @@ -106,7 +106,7 @@ class TestIsStateContextMessage: def test_system_message_without_state_prefix(self): """Returns False for system message without state prefix.""" - message = ChatMessage( + message = Message( role="system", contents=[Content.from_text("You are a helpful assistant.")], ) @@ -114,7 +114,7 @@ class TestIsStateContextMessage: def test_empty_contents(self): """Returns False for message with empty contents.""" - message = ChatMessage(role="system", contents=[]) + message = Message(role="system", contents=[]) assert is_state_context_message(message) is False @@ -342,7 +342,7 @@ class TestLatestApprovalResponse: def test_no_approval_response(self): """Returns None when no approval response in last message.""" messages = [ - ChatMessage(role="assistant", contents=[Content.from_text("Hello")]), + Message(role="assistant", contents=[Content.from_text("Hello")]), ] result = latest_approval_response(messages) assert result is None @@ -357,7 +357,7 @@ class TestLatestApprovalResponse: function_call=fc, ) messages = [ - ChatMessage(role="user", contents=[approval_content]), + Message(role="user", contents=[approval_content]), ] result = latest_approval_response(messages) assert result is approval_content diff --git a/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py b/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py index 47970d7005..61cd9f1d06 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py +++ b/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py @@ -5,7 +5,7 @@ import json import pytest -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework_ag_ui._message_adapters import ( agent_framework_messages_to_agui, @@ -24,7 +24,7 @@ def sample_agui_message(): @pytest.fixture def sample_agent_framework_message(): """Create a sample Agent Framework message.""" - return ChatMessage(role="user", contents=[Content.from_text(text="Hello")], message_id="msg-123") + return Message(role="user", contents=[Content.from_text(text="Hello")], message_id="msg-123") def test_agui_to_agent_framework_basic(sample_agui_message): @@ -100,7 +100,7 @@ def test_agui_tool_result_to_agent_framework(): def test_agui_tool_approval_updates_tool_call_arguments(): """Tool approval updates matching tool call arguments for snapshots and agent context. - The LLM context (ChatMessage) should contain only enabled steps, so the LLM + The LLM context (Message) should contain only enabled steps, so the LLM generates responses based on what was actually approved/executed. The raw messages (for MESSAGES_SNAPSHOT) should contain all steps with status, @@ -446,7 +446,7 @@ def test_agui_with_tool_calls_to_agent_framework(): def test_agent_framework_to_agui_with_tool_calls(): """Test converting Agent Framework message with tool calls to AG-UI.""" - msg = ChatMessage( + msg = Message( role="assistant", contents=[ Content.from_text(text="Calling tool"), @@ -471,7 +471,7 @@ def test_agent_framework_to_agui_with_tool_calls(): def test_agent_framework_to_agui_multiple_text_contents(): """Test concatenating multiple text contents.""" - msg = ChatMessage( + msg = Message( role="assistant", contents=[Content.from_text(text="Part 1 "), Content.from_text(text="Part 2")], ) @@ -484,7 +484,7 @@ def test_agent_framework_to_agui_multiple_text_contents(): def test_agent_framework_to_agui_no_message_id(): """Test message without message_id - should auto-generate ID.""" - msg = ChatMessage(role="user", contents=[Content.from_text(text="Hello")]) + msg = Message(role="user", contents=[Content.from_text(text="Hello")]) messages = agent_framework_messages_to_agui([msg]) @@ -496,7 +496,7 @@ def test_agent_framework_to_agui_no_message_id(): def test_agent_framework_to_agui_system_role(): """Test system role conversion.""" - msg = ChatMessage(role="system", contents=[Content.from_text(text="System")]) + msg = Message(role="system", contents=[Content.from_text(text="System")]) messages = agent_framework_messages_to_agui([msg]) @@ -541,7 +541,7 @@ def test_extract_text_from_custom_contents(): def test_agent_framework_to_agui_function_result_dict(): """Test converting FunctionResultContent with dict result to AG-UI.""" - msg = ChatMessage( + msg = Message( role="tool", contents=[Content.from_function_result(call_id="call-123", result={"key": "value", "count": 42})], message_id="msg-789", @@ -558,7 +558,7 @@ def test_agent_framework_to_agui_function_result_dict(): def test_agent_framework_to_agui_function_result_none(): """Test converting FunctionResultContent with None result to AG-UI.""" - msg = ChatMessage( + msg = Message( role="tool", contents=[Content.from_function_result(call_id="call-123", result=None)], message_id="msg-789", @@ -574,7 +574,7 @@ def test_agent_framework_to_agui_function_result_none(): def test_agent_framework_to_agui_function_result_string(): """Test converting FunctionResultContent with string result to AG-UI.""" - msg = ChatMessage( + msg = Message( role="tool", contents=[Content.from_function_result(call_id="call-123", result="plain text result")], message_id="msg-789", @@ -589,7 +589,7 @@ def test_agent_framework_to_agui_function_result_string(): def test_agent_framework_to_agui_function_result_empty_list(): """Test converting FunctionResultContent with empty list result to AG-UI.""" - msg = ChatMessage( + msg = Message( role="tool", contents=[Content.from_function_result(call_id="call-123", result=[])], message_id="msg-789", @@ -611,7 +611,7 @@ def test_agent_framework_to_agui_function_result_single_text_content(): class MockTextContent: text: str - msg = ChatMessage( + msg = Message( role="tool", contents=[Content.from_function_result(call_id="call-123", result=[MockTextContent("Hello from MCP!")])], message_id="msg-789", @@ -633,7 +633,7 @@ def test_agent_framework_to_agui_function_result_multiple_text_contents(): class MockTextContent: text: str - msg = ChatMessage( + msg = Message( role="tool", contents=[ Content.from_function_result( diff --git a/python/packages/ag-ui/tests/ag_ui/test_message_hygiene.py b/python/packages/ag-ui/tests/ag_ui/test_message_hygiene.py index d1773bf10c..ed8526e592 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_message_hygiene.py +++ b/python/packages/ag-ui/tests/ag_ui/test_message_hygiene.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework_ag_ui._message_adapters import _deduplicate_messages, _sanitize_tool_history @@ -13,7 +13,7 @@ def test_sanitize_tool_history_filters_out_confirm_changes_only_message() -> Non tool for the approval UI flow that shouldn't be sent to the LLM. """ messages = [ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -23,7 +23,7 @@ def test_sanitize_tool_history_filters_out_confirm_changes_only_message() -> Non ) ], ), - ChatMessage( + Message( role="user", contents=[Content.from_text(text='{"accepted": true}')], ), @@ -44,11 +44,11 @@ def test_sanitize_tool_history_filters_out_confirm_changes_only_message() -> Non def test_deduplicate_messages_prefers_non_empty_tool_results() -> None: messages = [ - ChatMessage( + Message( role="tool", contents=[Content.from_function_result(call_id="call1", result="")], ), - ChatMessage( + Message( role="tool", contents=[Content.from_function_result(call_id="call1", result="result data")], ), @@ -71,13 +71,13 @@ def test_convert_approval_results_to_tool_messages() -> None: # Simulate what happens after _resolve_approval_responses: # A user message contains function_result content (the executed tool result) messages = [ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call(call_id="call_123", name="my_mcp_tool", arguments="{}"), ], ), - ChatMessage( + Message( role="user", contents=[ Content.from_function_result(call_id="call_123", result="tool execution result"), @@ -109,13 +109,13 @@ def test_convert_approval_results_preserves_other_user_content() -> None: from agent_framework_ag_ui._run import _convert_approval_results_to_tool_messages messages = [ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call(call_id="call_123", name="my_tool", arguments="{}"), ], ), - ChatMessage( + Message( role="user", contents=[ Content.from_text(text="User also said something"), @@ -152,12 +152,12 @@ def test_sanitize_tool_history_filters_confirm_changes_keeps_other_tools() -> No """ messages = [ # User asks something - ChatMessage( + Message( role="user", contents=[Content.from_text(text="What time is it?")], ), # Assistant calls MCP tool + confirm_changes - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call(call_id="call_1", name="get_datetime", arguments="{}"), @@ -165,12 +165,12 @@ def test_sanitize_tool_history_filters_confirm_changes_keeps_other_tools() -> No ], ), # Tool result for the actual MCP tool - ChatMessage( + Message( role="tool", contents=[Content.from_function_result(call_id="call_1", result="2024-01-01 12:00:00")], ), # User asks something else - ChatMessage( + Message( role="user", contents=[Content.from_text(text="What's the date?")], ), @@ -204,12 +204,12 @@ def test_sanitize_tool_history_filters_confirm_changes_from_assistant_messages() respond with "Here's your 5-step plan" instead of "Here's your 2-step plan". """ messages = [ - ChatMessage( + Message( role="user", contents=[Content.from_text(text="Build a robot")], ), # Assistant message with both generate_task_steps and confirm_changes - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -225,7 +225,7 @@ def test_sanitize_tool_history_filters_confirm_changes_from_assistant_messages() ], ), # Approval response - ChatMessage( + Message( role="user", contents=[ Content.from_function_approval_response( diff --git a/python/packages/ag-ui/tests/ag_ui/test_run.py b/python/packages/ag-ui/tests/ag_ui/test_run.py index 6428180fc0..9d21bd2d0a 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_run.py +++ b/python/packages/ag-ui/tests/ag_ui/test_run.py @@ -6,7 +6,7 @@ from ag_ui.core import ( TextMessageEndEvent, TextMessageStartEvent, ) -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework_ag_ui._run import ( FlowState, @@ -212,7 +212,7 @@ class TestInjectStateContext: def test_no_state_message(self): """Returns original messages when no state context needed.""" - messages = [ChatMessage(role="user", contents=[Content.from_text("Hello")])] + messages = [Message(role="user", contents=[Content.from_text("Hello")])] result = _inject_state_context(messages, {}, {}) assert result == messages @@ -224,8 +224,8 @@ class TestInjectStateContext: def test_last_message_not_user(self): """Returns original messages when last message is not from user.""" messages = [ - ChatMessage(role="user", contents=[Content.from_text("Hello")]), - ChatMessage(role="assistant", contents=[Content.from_text("Hi")]), + Message(role="user", contents=[Content.from_text("Hello")]), + Message(role="assistant", contents=[Content.from_text("Hi")]), ] state = {"key": "value"} schema = {"properties": {"key": {"type": "string"}}} @@ -237,8 +237,8 @@ class TestInjectStateContext: """Injects state context before last user message.""" messages = [ - ChatMessage(role="system", contents=[Content.from_text("You are helpful")]), - ChatMessage(role="user", contents=[Content.from_text("Hello")]), + Message(role="system", contents=[Content.from_text("You are helpful")]), + Message(role="user", contents=[Content.from_text("Hello")]), ] state = {"document": "content"} schema = {"properties": {"document": {"type": "string"}}} @@ -405,7 +405,7 @@ def test_extract_approved_state_updates_no_handler(): """Test _extract_approved_state_updates returns empty with no handler.""" from agent_framework_ag_ui._run import _extract_approved_state_updates - messages = [ChatMessage(role="user", contents=[Content.from_text("Hello")])] + messages = [Message(role="user", contents=[Content.from_text("Hello")])] result = _extract_approved_state_updates(messages, None) assert result == {} @@ -416,7 +416,7 @@ def test_extract_approved_state_updates_no_approval(): from agent_framework_ag_ui._run import _extract_approved_state_updates handler = PredictiveStateHandler(predict_state_config={"doc": {"tool": "write", "tool_argument": "content"}}) - messages = [ChatMessage(role="user", contents=[Content.from_text("Hello")])] + messages = [Message(role="user", contents=[Content.from_text("Hello")])] result = _extract_approved_state_updates(messages, handler) assert result == {} diff --git a/python/packages/ag-ui/tests/ag_ui/test_structured_output.py b/python/packages/ag-ui/tests/ag_ui/test_structured_output.py index d1afdc971c..a8d9404a42 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_structured_output.py +++ b/python/packages/ag-ui/tests/ag_ui/test_structured_output.py @@ -6,7 +6,7 @@ import json from collections.abc import AsyncIterator, MutableSequence from typing import Any -from agent_framework import ChatAgent, ChatMessage, ChatOptions, ChatResponseUpdate, Content +from agent_framework import Agent, ChatOptions, ChatResponseUpdate, Content, Message from pydantic import BaseModel @@ -35,13 +35,13 @@ async def test_structured_output_with_recipe(streaming_chat_client_stub, stream_ from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: ChatOptions, **kwargs: Any + messages: MutableSequence[Message], options: ChatOptions, **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate( contents=[Content.from_text(text='{"recipe": {"name": "Pasta"}, "message": "Here is your recipe"}')] ) - agent = ChatAgent(name="test", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test", instructions="Test", client=streaming_chat_client_stub(stream_fn)) agent.default_options = ChatOptions(response_format=RecipeOutput) wrapper = AgentFrameworkAgent( @@ -73,7 +73,7 @@ async def test_structured_output_with_steps(streaming_chat_client_stub, stream_f from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: ChatOptions, **kwargs: Any + messages: MutableSequence[Message], options: ChatOptions, **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: steps_data = { "steps": [ @@ -83,7 +83,7 @@ async def test_structured_output_with_steps(streaming_chat_client_stub, stream_f } yield ChatResponseUpdate(contents=[Content.from_text(text=json.dumps(steps_data))]) - agent = ChatAgent(name="test", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test", instructions="Test", client=streaming_chat_client_stub(stream_fn)) agent.default_options = ChatOptions(response_format=StepsOutput) wrapper = AgentFrameworkAgent( @@ -116,8 +116,8 @@ async def test_structured_output_with_no_schema_match(streaming_chat_client_stub ChatResponseUpdate(contents=[Content.from_text(text='{"data": {"key": "value"}}')]), ] - agent = ChatAgent( - name="test", instructions="Test", chat_client=streaming_chat_client_stub(stream_from_updates_fixture(updates)) + agent = Agent( + name="test", instructions="Test", client=streaming_chat_client_stub(stream_from_updates_fixture(updates)) ) agent.default_options = ChatOptions(response_format=GenericOutput) @@ -149,11 +149,11 @@ async def test_structured_output_without_schema(streaming_chat_client_stub, stre info: str async def stream_fn( - messages: MutableSequence[ChatMessage], options: ChatOptions, **kwargs: Any + messages: MutableSequence[Message], options: ChatOptions, **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text(text='{"data": {"key": "value"}, "info": "processed"}')]) - agent = ChatAgent(name="test", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test", instructions="Test", client=streaming_chat_client_stub(stream_fn)) agent.default_options = ChatOptions(response_format=DataOutput) wrapper = AgentFrameworkAgent( @@ -182,10 +182,10 @@ async def test_no_structured_output_when_no_response_format(streaming_chat_clien updates = [ChatResponseUpdate(contents=[Content.from_text(text="Regular text")])] - agent = ChatAgent( + agent = Agent( name="test", instructions="Test", - chat_client=streaming_chat_client_stub(stream_from_updates_fixture(updates)), + client=streaming_chat_client_stub(stream_from_updates_fixture(updates)), ) # No response_format set @@ -208,12 +208,12 @@ async def test_structured_output_with_message_field(streaming_chat_client_stub, from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: ChatOptions, **kwargs: Any + messages: MutableSequence[Message], options: ChatOptions, **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: output_data = {"recipe": {"name": "Salad"}, "message": "Fresh salad recipe ready"} yield ChatResponseUpdate(contents=[Content.from_text(text=json.dumps(output_data))]) - agent = ChatAgent(name="test", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test", instructions="Test", client=streaming_chat_client_stub(stream_fn)) agent.default_options = ChatOptions(response_format=RecipeOutput) wrapper = AgentFrameworkAgent( @@ -243,12 +243,12 @@ async def test_empty_updates_no_structured_processing(streaming_chat_client_stub from agent_framework.ag_ui import AgentFrameworkAgent async def stream_fn( - messages: MutableSequence[ChatMessage], options: ChatOptions, **kwargs: Any + messages: MutableSequence[Message], options: ChatOptions, **kwargs: Any ) -> AsyncIterator[ChatResponseUpdate]: if False: yield ChatResponseUpdate(contents=[]) - agent = ChatAgent(name="test", instructions="Test", chat_client=streaming_chat_client_stub(stream_fn)) + agent = Agent(name="test", instructions="Test", client=streaming_chat_client_stub(stream_fn)) agent.default_options = ChatOptions(response_format=RecipeOutput) wrapper = AgentFrameworkAgent(agent=agent) diff --git a/python/packages/ag-ui/tests/ag_ui/test_tooling.py b/python/packages/ag-ui/tests/ag_ui/test_tooling.py index 242f5fd668..e8567a586d 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_tooling.py +++ b/python/packages/ag-ui/tests/ag_ui/test_tooling.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework_ag_ui._orchestration._tooling import ( collect_server_tools, @@ -31,14 +31,14 @@ def regular_tool() -> str: return "result" -def _create_chat_agent_with_tool(tool_name: str = "regular_tool") -> ChatAgent: - """Create a ChatAgent with a mocked chat client and a simple tool. +def _create_chat_agent_with_tool(tool_name: str = "regular_tool") -> Agent: + """Create a Agent with a mocked chat client and a simple tool. Note: tool_name parameter is kept for API compatibility but the tool will always be named 'regular_tool' since tool uses the function name. """ mock_chat_client = MagicMock() - return ChatAgent(chat_client=mock_chat_client, tools=[regular_tool]) + return Agent(client=mock_chat_client, tools=[regular_tool]) def test_merge_tools_filters_duplicates() -> None: @@ -59,7 +59,7 @@ def test_register_additional_client_tools_assigns_when_configured() -> None: mock_chat_client = MagicMock(spec=BaseChatClient) mock_chat_client.function_invocation_configuration = normalize_function_invocation_configuration(None) - agent = ChatAgent(chat_client=mock_chat_client) + agent = Agent(client=mock_chat_client) tools = [DummyTool("x")] register_additional_client_tools(agent, tools) @@ -148,14 +148,14 @@ def test_collect_server_tools_no_default_options() -> None: def test_register_additional_client_tools_no_tools() -> None: """register_additional_client_tools does nothing with None tools.""" mock_chat_client = MagicMock() - agent = ChatAgent(chat_client=mock_chat_client) + agent = Agent(client=mock_chat_client) # Should not raise register_additional_client_tools(agent, None) def test_register_additional_client_tools_no_chat_client() -> None: - """register_additional_client_tools does nothing when agent has no chat_client.""" + """register_additional_client_tools does nothing when agent has no client.""" from agent_framework_ag_ui._orchestration._tooling import register_additional_client_tools class MockAgent: diff --git a/python/packages/ag-ui/tests/ag_ui/test_utils.py b/python/packages/ag-ui/tests/ag_ui/test_utils.py index 4b680d4b71..0f453132f7 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_utils.py +++ b/python/packages/ag-ui/tests/ag_ui/test_utils.py @@ -404,11 +404,11 @@ def test_safe_json_parse_with_none(): def test_get_role_value_with_enum(): """Test get_role_value with enum role.""" - from agent_framework import ChatMessage, Content + from agent_framework import Content, Message from agent_framework_ag_ui._utils import get_role_value - message = ChatMessage(role="user", contents=[Content.from_text("test")]) + message = Message(role="user", contents=[Content.from_text("test")]) result = get_role_value(message) assert result == "user" diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 1a000ebd69..91bba87f12 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -11,7 +11,6 @@ from agent_framework import ( Annotation, BaseChatClient, ChatAndFunctionMiddlewareTypes, - ChatMessage, ChatMiddlewareLayer, ChatOptions, ChatResponse, @@ -21,9 +20,7 @@ from agent_framework import ( FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, - HostedCodeInterpreterTool, - HostedMCPTool, - HostedWebSearchTool, + Message, ResponseStream, TextSpanRegion, UsageDetails, @@ -350,13 +347,116 @@ class AnthropicClient( # streaming requires tracking the last function call ID and name self._last_call_id_name: tuple[str, str] | None = None + # region Static factory methods for hosted tools + + @staticmethod + def get_code_interpreter_tool( + *, + type_name: str | None = None, + ) -> dict[str, Any]: + """Create a code interpreter tool configuration for Anthropic. + + Keyword Args: + type_name: Override the tool type name. Defaults to "code_execution_20250825". + + Returns: + A dict-based tool configuration ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.anthropic import AnthropicClient + + tool = AnthropicClient.get_code_interpreter_tool() + agent = AnthropicClient().as_agent(tools=[tool]) + """ + return {"type": type_name or "code_execution_20250825"} + + @staticmethod + def get_web_search_tool( + *, + type_name: str | None = None, + ) -> dict[str, Any]: + """Create a web search tool configuration for Anthropic. + + Keyword Args: + type_name: Override the tool type name. Defaults to "web_search_20250305". + + Returns: + A dict-based tool configuration ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.anthropic import AnthropicClient + + tool = AnthropicClient.get_web_search_tool() + agent = AnthropicClient().as_agent(tools=[tool]) + """ + return {"type": type_name or "web_search_20250305"} + + @staticmethod + def get_mcp_tool( + *, + name: str, + url: str, + allowed_tools: list[str] | None = None, + authorization_token: str | None = None, + ) -> dict[str, Any]: + """Create a hosted MCP tool configuration for Anthropic. + + This configures an MCP (Model Context Protocol) server that will be called + by Anthropic's service. The tools from this MCP server are executed remotely + by Anthropic, not locally by your application. + + Note: + For local MCP execution where your application calls the MCP server + directly, use the MCP client tools instead of this method. + + Keyword Args: + name: A label/name for the MCP server. + url: The URL of the MCP server. + allowed_tools: List of tool names that are allowed to be used from this MCP server. + authorization_token: Authorization token for the MCP server (e.g., Bearer token). + + Returns: + A dict-based tool configuration ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.anthropic import AnthropicClient + + tool = AnthropicClient.get_mcp_tool( + name="GitHub", + url="https://api.githubcopilot.com/mcp/", + authorization_token="Bearer ghp_xxx", + ) + agent = AnthropicClient().as_agent(tools=[tool]) + """ + result: dict[str, Any] = { + "type": "mcp", + "server_label": name.replace(" ", "_"), + "server_url": url, + } + + if allowed_tools: + result["allowed_tools"] = allowed_tools + + if authorization_token: + result["headers"] = {"authorization": authorization_token} + + return result + + # endregion + # region Get response methods @override def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], stream: bool = False, **kwargs: Any, @@ -385,7 +485,7 @@ class AnthropicClient( def _prepare_options( self, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], **kwargs: Any, ) -> dict[str, Any]: @@ -430,7 +530,7 @@ class AnthropicClient( run_options["messages"] = self._prepare_messages_for_anthropic(messages) # system message - first system message is passed as instructions - if messages and isinstance(messages[0], ChatMessage) and messages[0].role == "system": + if messages and isinstance(messages[0], Message) and messages[0].role == "system": run_options["system"] = messages[0].text # betas @@ -516,22 +616,22 @@ class AnthropicClient( "schema": schema, } - def _prepare_messages_for_anthropic(self, messages: Sequence[ChatMessage]) -> list[dict[str, Any]]: + def _prepare_messages_for_anthropic(self, messages: Sequence[Message]) -> list[dict[str, Any]]: """Prepare a list of ChatMessages for the Anthropic client. This skips the first message if it is a system message, as Anthropic expects system instructions as a separate parameter. """ # first system message is passed as instructions - if messages and isinstance(messages[0], ChatMessage) and messages[0].role == "system": + if messages and isinstance(messages[0], Message) and messages[0].role == "system": return [self._prepare_message_for_anthropic(msg) for msg in messages[1:]] return [self._prepare_message_for_anthropic(msg) for msg in messages] - def _prepare_message_for_anthropic(self, message: ChatMessage) -> dict[str, Any]: - """Prepare a ChatMessage for the Anthropic client. + def _prepare_message_for_anthropic(self, message: Message) -> dict[str, Any]: + """Prepare a Message for the Anthropic client. Args: - message: The ChatMessage to convert. + message: The Message to convert. Returns: A dictionary representing the message in Anthropic format. @@ -590,6 +690,9 @@ class AnthropicClient( def _prepare_tools_for_anthropic(self, options: Mapping[str, Any]) -> dict[str, Any] | None: """Prepare tools and tool choice configuration for the Anthropic API request. + Converts FunctionTool to Anthropic format. MCP tools are routed to separate + mcp_servers parameter. All other tools pass through unchanged. + Args: options: The options dict containing tools and tool choice settings. @@ -603,46 +706,32 @@ class AnthropicClient( # Process tools if tools: - tool_list: list[MutableMapping[str, Any]] = [] - mcp_server_list: list[MutableMapping[str, Any]] = [] + tool_list: list[Any] = [] + mcp_server_list: list[Any] = [] for tool in tools: - match tool: - case MutableMapping(): - tool_list.append(tool) - case FunctionTool(): - tool_list.append({ - "type": "custom", - "name": tool.name, - "description": tool.description, - "input_schema": tool.parameters(), - }) - case HostedWebSearchTool(): - search_tool: dict[str, Any] = { - "type": "web_search_20250305", - "name": "web_search", - } - if tool.additional_properties: - search_tool.update(tool.additional_properties) - tool_list.append(search_tool) - case HostedCodeInterpreterTool(): - code_tool: dict[str, Any] = { - "type": "code_execution_20250825", - "name": "code_execution", - } - tool_list.append(code_tool) - case HostedMCPTool(): - server_def: dict[str, Any] = { - "type": "url", - "name": tool.name, - "url": str(tool.url), - } - if tool.allowed_tools: - server_def["tool_configuration"] = {"allowed_tools": list(tool.allowed_tools)} - if tool.headers and (auth := tool.headers.get("authorization")): - server_def["authorization_token"] = auth - mcp_server_list.append(server_def) - case _: - logger.debug(f"Ignoring unsupported tool type: {type(tool)} for now") + if isinstance(tool, FunctionTool): + tool_list.append({ + "type": "custom", + "name": tool.name, + "description": tool.description, + "input_schema": tool.parameters(), + }) + elif isinstance(tool, MutableMapping) and tool.get("type") == "mcp": + # MCP servers must be routed to separate mcp_servers parameter + server_def: dict[str, Any] = { + "type": "url", + "name": tool.get("server_label", ""), + "url": tool.get("server_url", ""), + } + if allowed_tools := tool.get("allowed_tools"): + server_def["tool_configuration"] = {"allowed_tools": list(allowed_tools)} + headers = tool.get("headers") + if isinstance(headers, dict) and (auth := headers.get("authorization")): + server_def["authorization_token"] = auth + mcp_server_list.append(server_def) + else: + # Pass through all other tools (dicts, SDK types) unchanged + tool_list.append(tool) if tool_list: result["tools"] = tool_list @@ -693,7 +782,7 @@ class AnthropicClient( return ChatResponse( response_id=message.id, messages=[ - ChatMessage( + Message( role="assistant", contents=self._parse_contents_from_anthropic(message.content), raw_representation=message, diff --git a/python/packages/anthropic/pyproject.toml b/python/packages/anthropic/pyproject.toml index 7106f8adb0..85ca301df9 100644 --- a/python/packages/anthropic/pyproject.toml +++ b/python/packages/anthropic/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "anthropic>=0.70.0,<1", ] diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 75c2144258..80d57de07f 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -6,14 +6,11 @@ from unittest.mock import MagicMock, patch import pytest from agent_framework import ( - ChatClientProtocol, - ChatMessage, ChatOptions, ChatResponseUpdate, Content, - HostedCodeInterpreterTool, - HostedMCPTool, - HostedWebSearchTool, + Message, + SupportsChatGetResponse, tool, ) from agent_framework.exceptions import ServiceInitializationError @@ -98,11 +95,11 @@ def test_anthropic_settings_missing_api_key(anthropic_unit_test_env: dict[str, s def test_anthropic_client_init_with_client(mock_anthropic_client: MagicMock) -> None: """Test AnthropicClient initialization with existing anthropic_client.""" - chat_client = create_test_anthropic_client(mock_anthropic_client, model_id="claude-3-5-sonnet-20241022") + client = create_test_anthropic_client(mock_anthropic_client, model_id="claude-3-5-sonnet-20241022") - assert chat_client.anthropic_client is mock_anthropic_client - assert chat_client.model_id == "claude-3-5-sonnet-20241022" - assert isinstance(chat_client, ChatClientProtocol) + assert client.anthropic_client is mock_anthropic_client + assert client.model_id == "claude-3-5-sonnet-20241022" + assert isinstance(client, SupportsChatGetResponse) def test_anthropic_client_init_auto_create_client(anthropic_unit_test_env: dict[str, str]) -> None: @@ -138,8 +135,8 @@ def test_anthropic_client_init_validation_error() -> None: def test_anthropic_client_service_url(mock_anthropic_client: MagicMock) -> None: """Test service_url method.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) - assert chat_client.service_url() == "https://api.anthropic.com" + client = create_test_anthropic_client(mock_anthropic_client) + assert client.service_url() == "https://api.anthropic.com" # Message Conversion Tests @@ -147,10 +144,10 @@ def test_anthropic_client_service_url(mock_anthropic_client: MagicMock) -> None: def test_prepare_message_for_anthropic_text(mock_anthropic_client: MagicMock) -> None: """Test converting text message to Anthropic format.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) - message = ChatMessage(role="user", text="Hello, world!") + client = create_test_anthropic_client(mock_anthropic_client) + message = Message(role="user", text="Hello, world!") - result = chat_client._prepare_message_for_anthropic(message) + result = client._prepare_message_for_anthropic(message) assert result["role"] == "user" assert len(result["content"]) == 1 @@ -160,8 +157,8 @@ def test_prepare_message_for_anthropic_text(mock_anthropic_client: MagicMock) -> def test_prepare_message_for_anthropic_function_call(mock_anthropic_client: MagicMock) -> None: """Test converting function call message to Anthropic format.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) - message = ChatMessage( + client = create_test_anthropic_client(mock_anthropic_client) + message = Message( role="assistant", contents=[ Content.from_function_call( @@ -172,7 +169,7 @@ def test_prepare_message_for_anthropic_function_call(mock_anthropic_client: Magi ], ) - result = chat_client._prepare_message_for_anthropic(message) + result = client._prepare_message_for_anthropic(message) assert result["role"] == "assistant" assert len(result["content"]) == 1 @@ -184,8 +181,8 @@ def test_prepare_message_for_anthropic_function_call(mock_anthropic_client: Magi def test_prepare_message_for_anthropic_function_result(mock_anthropic_client: MagicMock) -> None: """Test converting function result message to Anthropic format.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) - message = ChatMessage( + client = create_test_anthropic_client(mock_anthropic_client) + message = Message( role="tool", contents=[ Content.from_function_result( @@ -195,7 +192,7 @@ def test_prepare_message_for_anthropic_function_result(mock_anthropic_client: Ma ], ) - result = chat_client._prepare_message_for_anthropic(message) + result = client._prepare_message_for_anthropic(message) assert result["role"] == "user" assert len(result["content"]) == 1 @@ -209,13 +206,13 @@ def test_prepare_message_for_anthropic_function_result(mock_anthropic_client: Ma def test_prepare_message_for_anthropic_text_reasoning(mock_anthropic_client: MagicMock) -> None: """Test converting text reasoning message to Anthropic format.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) - message = ChatMessage( + client = create_test_anthropic_client(mock_anthropic_client) + message = Message( role="assistant", contents=[Content.from_text_reasoning(text="Let me think about this...")], ) - result = chat_client._prepare_message_for_anthropic(message) + result = client._prepare_message_for_anthropic(message) assert result["role"] == "assistant" assert len(result["content"]) == 1 @@ -225,13 +222,13 @@ def test_prepare_message_for_anthropic_text_reasoning(mock_anthropic_client: Mag def test_prepare_messages_for_anthropic_with_system(mock_anthropic_client: MagicMock) -> None: """Test converting messages list with system message.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) messages = [ - ChatMessage(role="system", text="You are a helpful assistant."), - ChatMessage(role="user", text="Hello!"), + Message(role="system", text="You are a helpful assistant."), + Message(role="user", text="Hello!"), ] - result = chat_client._prepare_messages_for_anthropic(messages) + result = client._prepare_messages_for_anthropic(messages) # System message should be skipped assert len(result) == 1 @@ -241,13 +238,13 @@ def test_prepare_messages_for_anthropic_with_system(mock_anthropic_client: Magic def test_prepare_messages_for_anthropic_without_system(mock_anthropic_client: MagicMock) -> None: """Test converting messages list without system message.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) messages = [ - ChatMessage(role="user", text="Hello!"), - ChatMessage(role="assistant", text="Hi there!"), + Message(role="user", text="Hello!"), + Message(role="assistant", text="Hi there!"), ] - result = chat_client._prepare_messages_for_anthropic(messages) + result = client._prepare_messages_for_anthropic(messages) assert len(result) == 2 assert result[0]["role"] == "user" @@ -259,7 +256,7 @@ def test_prepare_messages_for_anthropic_without_system(mock_anthropic_client: Ma def test_prepare_tools_for_anthropic_tool(mock_anthropic_client: MagicMock) -> None: """Test converting FunctionTool to Anthropic format.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) @tool(approval_mode="never_require") def get_weather(location: Annotated[str, Field(description="Location to get weather for")]) -> str: @@ -267,7 +264,7 @@ def test_prepare_tools_for_anthropic_tool(mock_anthropic_client: MagicMock) -> N return f"Weather for {location}" chat_options = ChatOptions(tools=[get_weather]) - result = chat_client._prepare_tools_for_anthropic(chat_options) + result = client._prepare_tools_for_anthropic(chat_options) assert result is not None assert "tools" in result @@ -278,39 +275,37 @@ def test_prepare_tools_for_anthropic_tool(mock_anthropic_client: MagicMock) -> N def test_prepare_tools_for_anthropic_web_search(mock_anthropic_client: MagicMock) -> None: - """Test converting HostedWebSearchTool to Anthropic format.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) - chat_options = ChatOptions(tools=[HostedWebSearchTool()]) + """Test converting web_search dict tool to Anthropic format.""" + client = create_test_anthropic_client(mock_anthropic_client) + chat_options = ChatOptions(tools=[client.get_web_search_tool()]) - result = chat_client._prepare_tools_for_anthropic(chat_options) + result = client._prepare_tools_for_anthropic(chat_options) assert result is not None assert "tools" in result assert len(result["tools"]) == 1 assert result["tools"][0]["type"] == "web_search_20250305" - assert result["tools"][0]["name"] == "web_search" def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: MagicMock) -> None: - """Test converting HostedCodeInterpreterTool to Anthropic format.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) - chat_options = ChatOptions(tools=[HostedCodeInterpreterTool()]) + """Test converting code_interpreter dict tool to Anthropic format.""" + client = create_test_anthropic_client(mock_anthropic_client) + chat_options = ChatOptions(tools=[client.get_code_interpreter_tool()]) - result = chat_client._prepare_tools_for_anthropic(chat_options) + result = client._prepare_tools_for_anthropic(chat_options) assert result is not None assert "tools" in result assert len(result["tools"]) == 1 assert result["tools"][0]["type"] == "code_execution_20250825" - assert result["tools"][0]["name"] == "code_execution" def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock) -> None: - """Test converting HostedMCPTool to Anthropic format.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) - chat_options = ChatOptions(tools=[HostedMCPTool(name="test-mcp", url="https://example.com/mcp")]) + """Test converting MCP dict tool to Anthropic format.""" + client = create_test_anthropic_client(mock_anthropic_client) + chat_options = ChatOptions(tools=[client.get_mcp_tool(name="test-mcp", url="https://example.com/mcp")]) - result = chat_client._prepare_tools_for_anthropic(chat_options) + result = client._prepare_tools_for_anthropic(chat_options) assert result is not None assert "mcp_servers" in result @@ -321,33 +316,31 @@ def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock) def test_prepare_tools_for_anthropic_mcp_with_auth(mock_anthropic_client: MagicMock) -> None: - """Test converting HostedMCPTool with authorization headers.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) - chat_options = ChatOptions( - tools=[ - HostedMCPTool( - name="test-mcp", - url="https://example.com/mcp", - headers={"authorization": "Bearer token123"}, - ) - ] + """Test converting MCP dict tool with authorization token.""" + client = create_test_anthropic_client(mock_anthropic_client) + # Use the static method with authorization_token + mcp_tool = client.get_mcp_tool( + name="test-mcp", + url="https://example.com/mcp", + authorization_token="Bearer token123", ) + chat_options = ChatOptions(tools=[mcp_tool]) - result = chat_client._prepare_tools_for_anthropic(chat_options) + result = client._prepare_tools_for_anthropic(chat_options) assert result is not None assert "mcp_servers" in result - # The authorization header is converted to authorization_token + # The authorization_token should be passed through assert "authorization_token" in result["mcp_servers"][0] assert result["mcp_servers"][0]["authorization_token"] == "Bearer token123" def test_prepare_tools_for_anthropic_dict_tool(mock_anthropic_client: MagicMock) -> None: """Test converting dict tool to Anthropic format.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) chat_options = ChatOptions(tools=[{"type": "custom", "name": "custom_tool", "description": "A custom tool"}]) - result = chat_client._prepare_tools_for_anthropic(chat_options) + result = client._prepare_tools_for_anthropic(chat_options) assert result is not None assert "tools" in result @@ -357,10 +350,10 @@ def test_prepare_tools_for_anthropic_dict_tool(mock_anthropic_client: MagicMock) def test_prepare_tools_for_anthropic_none(mock_anthropic_client: MagicMock) -> None: """Test converting None tools.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) chat_options = ChatOptions() - result = chat_client._prepare_tools_for_anthropic(chat_options) + result = client._prepare_tools_for_anthropic(chat_options) assert result is None @@ -370,14 +363,14 @@ def test_prepare_tools_for_anthropic_none(mock_anthropic_client: MagicMock) -> N async def test_prepare_options_basic(mock_anthropic_client: MagicMock) -> None: """Test _prepare_options with basic ChatOptions.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options = ChatOptions(max_tokens=100, temperature=0.7) - run_options = chat_client._prepare_options(messages, chat_options) + run_options = client._prepare_options(messages, chat_options) - assert run_options["model"] == chat_client.model_id + assert run_options["model"] == client.model_id assert run_options["max_tokens"] == 100 assert run_options["temperature"] == 0.7 assert "messages" in run_options @@ -385,15 +378,15 @@ async def test_prepare_options_basic(mock_anthropic_client: MagicMock) -> None: async def test_prepare_options_with_system_message(mock_anthropic_client: MagicMock) -> None: """Test _prepare_options with system message.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) messages = [ - ChatMessage(role="system", text="You are helpful."), - ChatMessage(role="user", text="Hello"), + Message(role="system", text="You are helpful."), + Message(role="user", text="Hello"), ] chat_options = ChatOptions() - run_options = chat_client._prepare_options(messages, chat_options) + run_options = client._prepare_options(messages, chat_options) assert run_options["system"] == "You are helpful." assert len(run_options["messages"]) == 1 # System message not in messages list @@ -401,25 +394,25 @@ async def test_prepare_options_with_system_message(mock_anthropic_client: MagicM async def test_prepare_options_with_tool_choice_auto(mock_anthropic_client: MagicMock) -> None: """Test _prepare_options with auto tool choice.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options = ChatOptions(tool_choice="auto") - run_options = chat_client._prepare_options(messages, chat_options) + run_options = client._prepare_options(messages, chat_options) assert run_options["tool_choice"]["type"] == "auto" async def test_prepare_options_with_tool_choice_required(mock_anthropic_client: MagicMock) -> None: """Test _prepare_options with required tool choice.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] # For required with specific function, need to pass as dict chat_options = ChatOptions(tool_choice={"mode": "required", "required_function_name": "get_weather"}) - run_options = chat_client._prepare_options(messages, chat_options) + run_options = client._prepare_options(messages, chat_options) assert run_options["tool_choice"]["type"] == "tool" assert run_options["tool_choice"]["name"] == "get_weather" @@ -427,29 +420,29 @@ async def test_prepare_options_with_tool_choice_required(mock_anthropic_client: async def test_prepare_options_with_tool_choice_none(mock_anthropic_client: MagicMock) -> None: """Test _prepare_options with none tool choice.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options = ChatOptions(tool_choice="none") - run_options = chat_client._prepare_options(messages, chat_options) + run_options = client._prepare_options(messages, chat_options) assert run_options["tool_choice"]["type"] == "none" async def test_prepare_options_with_tools(mock_anthropic_client: MagicMock) -> None: """Test _prepare_options with tools.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) @tool(approval_mode="never_require") def get_weather(location: str) -> str: """Get weather for a location.""" return f"Weather for {location}" - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options = ChatOptions(tools=[get_weather]) - run_options = chat_client._prepare_options(messages, chat_options) + run_options = client._prepare_options(messages, chat_options) assert "tools" in run_options assert len(run_options["tools"]) == 1 @@ -457,24 +450,24 @@ async def test_prepare_options_with_tools(mock_anthropic_client: MagicMock) -> N async def test_prepare_options_with_stop_sequences(mock_anthropic_client: MagicMock) -> None: """Test _prepare_options with stop sequences.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options = ChatOptions(stop=["STOP", "END"]) - run_options = chat_client._prepare_options(messages, chat_options) + run_options = client._prepare_options(messages, chat_options) assert run_options["stop_sequences"] == ["STOP", "END"] async def test_prepare_options_with_top_p(mock_anthropic_client: MagicMock) -> None: """Test _prepare_options with top_p.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options = ChatOptions(top_p=0.9) - run_options = chat_client._prepare_options(messages, chat_options) + run_options = client._prepare_options(messages, chat_options) assert run_options["top_p"] == 0.9 @@ -485,9 +478,9 @@ async def test_prepare_options_filters_internal_kwargs(mock_anthropic_client: Ma Internal kwargs like _function_middleware_pipeline, thread, and middleware should be filtered out before being passed to the Anthropic API. """ - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options: ChatOptions = {} # Simulate internal kwargs that get passed through the middleware pipeline @@ -499,7 +492,7 @@ async def test_prepare_options_filters_internal_kwargs(mock_anthropic_client: Ma "middleware": [object()], } - run_options = chat_client._prepare_options(messages, chat_options, **internal_kwargs) + run_options = client._prepare_options(messages, chat_options, **internal_kwargs) # Internal kwargs should be filtered out assert "_function_middleware_pipeline" not in run_options @@ -514,7 +507,7 @@ async def test_prepare_options_filters_internal_kwargs(mock_anthropic_client: Ma def test_process_message_basic(mock_anthropic_client: MagicMock) -> None: """Test _process_message with basic text response.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) mock_message = MagicMock(spec=BetaMessage) mock_message.id = "msg_123" @@ -523,7 +516,7 @@ def test_process_message_basic(mock_anthropic_client: MagicMock) -> None: mock_message.usage = BetaUsage(input_tokens=10, output_tokens=5) mock_message.stop_reason = "end_turn" - response = chat_client._process_message(mock_message, {}) + response = client._process_message(mock_message, {}) assert response.response_id == "msg_123" assert response.model_id == "claude-3-5-sonnet-20241022" @@ -540,7 +533,7 @@ def test_process_message_basic(mock_anthropic_client: MagicMock) -> None: def test_process_message_with_tool_use(mock_anthropic_client: MagicMock) -> None: """Test _process_message with tool use.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) mock_message = MagicMock(spec=BetaMessage) mock_message.id = "msg_123" @@ -556,7 +549,7 @@ def test_process_message_with_tool_use(mock_anthropic_client: MagicMock) -> None mock_message.usage = BetaUsage(input_tokens=10, output_tokens=5) mock_message.stop_reason = "tool_use" - response = chat_client._process_message(mock_message, {}) + response = client._process_message(mock_message, {}) assert len(response.messages[0].contents) == 1 assert response.messages[0].contents[0].type == "function_call" @@ -567,10 +560,10 @@ def test_process_message_with_tool_use(mock_anthropic_client: MagicMock) -> None def test_parse_usage_from_anthropic_basic(mock_anthropic_client: MagicMock) -> None: """Test _parse_usage_from_anthropic with basic usage.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) usage = BetaUsage(input_tokens=10, output_tokens=5) - result = chat_client._parse_usage_from_anthropic(usage) + result = client._parse_usage_from_anthropic(usage) assert result is not None assert result["input_token_count"] == 10 @@ -579,19 +572,19 @@ def test_parse_usage_from_anthropic_basic(mock_anthropic_client: MagicMock) -> N def test_parse_usage_from_anthropic_none(mock_anthropic_client: MagicMock) -> None: """Test _parse_usage_from_anthropic with None usage.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) - result = chat_client._parse_usage_from_anthropic(None) + result = client._parse_usage_from_anthropic(None) assert result is None def test_parse_contents_from_anthropic_text(mock_anthropic_client: MagicMock) -> None: """Test _parse_contents_from_anthropic with text content.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) content = [BetaTextBlock(type="text", text="Hello!")] - result = chat_client._parse_contents_from_anthropic(content) + result = client._parse_contents_from_anthropic(content) assert len(result) == 1 assert result[0].type == "text" @@ -600,7 +593,7 @@ def test_parse_contents_from_anthropic_text(mock_anthropic_client: MagicMock) -> def test_parse_contents_from_anthropic_tool_use(mock_anthropic_client: MagicMock) -> None: """Test _parse_contents_from_anthropic with tool use.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) content = [ BetaToolUseBlock( @@ -610,7 +603,7 @@ def test_parse_contents_from_anthropic_tool_use(mock_anthropic_client: MagicMock input={"location": "SF"}, ) ] - result = chat_client._parse_contents_from_anthropic(content) + result = client._parse_contents_from_anthropic(content) assert len(result) == 1 assert result[0].type == "function_call" @@ -625,7 +618,7 @@ def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(mock_a and subsequent input_json_delta events should have name="" to prevent ag-ui from emitting duplicate ToolCallStartEvents. """ - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) # First, simulate a tool_use event that sets _last_call_id_name tool_use_content = MagicMock() @@ -634,7 +627,7 @@ def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(mock_a tool_use_content.name = "get_weather" tool_use_content.input = {} - result = chat_client._parse_contents_from_anthropic([tool_use_content]) + result = client._parse_contents_from_anthropic([tool_use_content]) assert len(result) == 1 assert result[0].type == "function_call" assert result[0].call_id == "call_123" @@ -645,7 +638,7 @@ def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(mock_a delta_content_1.type = "input_json_delta" delta_content_1.partial_json = '{"location":' - result = chat_client._parse_contents_from_anthropic([delta_content_1]) + result = client._parse_contents_from_anthropic([delta_content_1]) assert len(result) == 1 assert result[0].type == "function_call" assert result[0].call_id == "call_123" @@ -657,7 +650,7 @@ def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(mock_a delta_content_2.type = "input_json_delta" delta_content_2.partial_json = '"San Francisco"}' - result = chat_client._parse_contents_from_anthropic([delta_content_2]) + result = client._parse_contents_from_anthropic([delta_content_2]) assert len(result) == 1 assert result[0].type == "function_call" assert result[0].call_id == "call_123" @@ -670,13 +663,13 @@ def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(mock_a def test_process_stream_event_simple(mock_anthropic_client: MagicMock) -> None: """Test _process_stream_event with simple mock event.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) # Test with a basic mock event - the actual implementation will handle real events mock_event = MagicMock() mock_event.type = "message_stop" - result = chat_client._process_stream_event(mock_event) + result = client._process_stream_event(mock_event) # message_stop events return None assert result is None @@ -684,7 +677,7 @@ def test_process_stream_event_simple(mock_anthropic_client: MagicMock) -> None: async def test_inner_get_response(mock_anthropic_client: MagicMock) -> None: """Test _inner_get_response method.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) # Create a mock message response mock_message = MagicMock(spec=BetaMessage) @@ -696,10 +689,10 @@ async def test_inner_get_response(mock_anthropic_client: MagicMock) -> None: mock_anthropic_client.beta.messages.create.return_value = mock_message - messages = [ChatMessage(role="user", text="Hi")] + messages = [Message(role="user", text="Hi")] chat_options = ChatOptions(max_tokens=10) - response = await chat_client._inner_get_response( # type: ignore[attr-defined] + response = await client._inner_get_response( # type: ignore[attr-defined] messages=messages, options=chat_options ) @@ -710,7 +703,7 @@ async def test_inner_get_response(mock_anthropic_client: MagicMock) -> None: async def test_inner_get_response_streaming(mock_anthropic_client: MagicMock) -> None: """Test _inner_get_response method with streaming.""" - chat_client = create_test_anthropic_client(mock_anthropic_client) + client = create_test_anthropic_client(mock_anthropic_client) # Create mock streaming response async def mock_stream(): @@ -720,11 +713,11 @@ async def test_inner_get_response_streaming(mock_anthropic_client: MagicMock) -> mock_anthropic_client.beta.messages.create.return_value = mock_stream() - messages = [ChatMessage(role="user", text="Hi")] + messages = [Message(role="user", text="Hi")] chat_options = ChatOptions(max_tokens=10) chunks: list[ChatResponseUpdate] = [] - async for chunk in chat_client._inner_get_response( # type: ignore[attr-defined] + async for chunk in client._inner_get_response( # type: ignore[attr-defined] messages=messages, options=chat_options, stream=True ): if chunk: @@ -751,7 +744,7 @@ async def test_anthropic_client_integration_basic_chat() -> None: """Integration test for basic chat completion.""" client = AnthropicClient() - messages = [ChatMessage(role="user", text="Say 'Hello, World!' and nothing else.")] + messages = [Message(role="user", text="Say 'Hello, World!' and nothing else.")] response = await client.get_response(messages=messages, options={"max_tokens": 50}) @@ -768,7 +761,7 @@ async def test_anthropic_client_integration_streaming_chat() -> None: """Integration test for streaming chat completion.""" client = AnthropicClient() - messages = [ChatMessage(role="user", text="Count from 1 to 5.")] + messages = [Message(role="user", text="Count from 1 to 5.")] chunks = [] async for chunk in client.get_response(messages=messages, stream=True, options={"max_tokens": 50}): @@ -784,7 +777,7 @@ async def test_anthropic_client_integration_function_calling() -> None: """Integration test for function calling.""" client = AnthropicClient() - messages = [ChatMessage(role="user", text="What's the weather in San Francisco?")] + messages = [Message(role="user", text="What's the weather in San Francisco?")] tools = [get_weather] response = await client.get_response( @@ -804,14 +797,13 @@ async def test_anthropic_client_integration_hosted_tools() -> None: """Integration test for hosted tools.""" client = AnthropicClient() - messages = [ChatMessage(role="user", text="What tools do you have available?")] + messages = [Message(role="user", text="What tools do you have available?")] tools = [ - HostedWebSearchTool(), - HostedCodeInterpreterTool(), - HostedMCPTool( + AnthropicClient.get_web_search_tool(), + AnthropicClient.get_code_interpreter_tool(), + AnthropicClient.get_mcp_tool( name="example-mcp", url="https://learn.microsoft.com/api/mcp", - approval_mode="never_require", ), ] @@ -831,8 +823,8 @@ async def test_anthropic_client_integration_with_system_message() -> None: client = AnthropicClient() messages = [ - ChatMessage(role="system", text="You are a pirate. Always respond like a pirate."), - ChatMessage(role="user", text="Hello!"), + Message(role="system", text="You are a pirate. Always respond like a pirate."), + Message(role="user", text="Hello!"), ] response = await client.get_response(messages=messages, options={"max_tokens": 50}) @@ -847,7 +839,7 @@ async def test_anthropic_client_integration_temperature_control() -> None: """Integration test with temperature control.""" client = AnthropicClient() - messages = [ChatMessage(role="user", text="Say hello.")] + messages = [Message(role="user", text="Say hello.")] response = await client.get_response( messages=messages, @@ -865,11 +857,11 @@ async def test_anthropic_client_integration_ordering() -> None: client = AnthropicClient() messages = [ - ChatMessage(role="user", text="Say hello."), - ChatMessage(role="user", text="Then say goodbye."), - ChatMessage(role="assistant", text="Thank you for chatting!"), - ChatMessage(role="assistant", text="Let me know if I can help."), - ChatMessage(role="user", text="Just testing things."), + Message(role="user", text="Say hello."), + Message(role="user", text="Then say goodbye."), + Message(role="assistant", text="Thank you for chatting!"), + Message(role="assistant", text="Let me know if I can help."), + Message(role="user", text="Just testing things."), ] response = await client.get_response(messages=messages) @@ -890,7 +882,7 @@ async def test_anthropic_client_integration_images() -> None: image_bytes = img_file.read() messages = [ - ChatMessage( + Message( role="user", contents=[ Content.from_text(text="Describe this image"), diff --git a/python/packages/azure-ai-search/AGENTS.md b/python/packages/azure-ai-search/AGENTS.md index 14e8f65e96..114ee9d9ab 100644 --- a/python/packages/azure-ai-search/AGENTS.md +++ b/python/packages/azure-ai-search/AGENTS.md @@ -16,7 +16,7 @@ provider = AzureAISearchContextProvider( endpoint="https://your-search.search.windows.net", index_name="your-index", ) -agent = ChatAgent(..., context_provider=provider) +agent = Agent(..., context_provider=provider) ``` ## Import Path diff --git a/python/packages/azure-ai-search/agent_framework_azure_ai_search/__init__.py b/python/packages/azure-ai-search/agent_framework_azure_ai_search/__init__.py index fedfb05bcd..4509c46d3e 100644 --- a/python/packages/azure-ai-search/agent_framework_azure_ai_search/__init__.py +++ b/python/packages/azure-ai-search/agent_framework_azure_ai_search/__init__.py @@ -2,6 +2,7 @@ import importlib.metadata +from ._context_provider import _AzureAISearchContextProvider from ._search_provider import AzureAISearchContextProvider, AzureAISearchSettings try: @@ -12,5 +13,6 @@ except importlib.metadata.PackageNotFoundError: __all__ = [ "AzureAISearchContextProvider", "AzureAISearchSettings", + "_AzureAISearchContextProvider", "__version__", ] diff --git a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py new file mode 100644 index 0000000000..bad955d57e --- /dev/null +++ b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py @@ -0,0 +1,625 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""New-pattern Azure AI Search context provider using BaseContextProvider. + +This module provides ``_AzureAISearchContextProvider``, a side-by-side implementation of +:class:`AzureAISearchContextProvider` built on the new :class:`BaseContextProvider` hooks +pattern. It will replace the existing class in PR2. +""" + +from __future__ import annotations + +import sys +from collections.abc import Awaitable, Callable +from typing import TYPE_CHECKING, Any, ClassVar, Literal + +from agent_framework import AGENT_FRAMEWORK_USER_AGENT, Message +from agent_framework._logging import get_logger +from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext +from agent_framework.exceptions import ServiceInitializationError +from azure.core.credentials import AzureKeyCredential +from azure.core.credentials_async import AsyncTokenCredential +from azure.core.exceptions import ResourceNotFoundError +from azure.search.documents.aio import SearchClient +from azure.search.documents.indexes.aio import SearchIndexClient +from azure.search.documents.indexes.models import ( + AzureOpenAIVectorizerParameters, + KnowledgeBase, + KnowledgeBaseAzureOpenAIModel, + KnowledgeRetrievalLowReasoningEffort, + KnowledgeRetrievalMediumReasoningEffort, + KnowledgeRetrievalMinimalReasoningEffort, + KnowledgeRetrievalOutputMode, + KnowledgeRetrievalReasoningEffort, + KnowledgeSourceReference, + SearchIndexKnowledgeSource, + SearchIndexKnowledgeSourceParameters, +) +from azure.search.documents.models import ( + QueryCaptionType, + QueryType, + VectorizableTextQuery, + VectorizedQuery, +) +from pydantic import ValidationError + +from ._search_provider import AzureAISearchSettings + +if TYPE_CHECKING: + from agent_framework._agents import SupportsAgentRun + from azure.search.documents.knowledgebases.aio import KnowledgeBaseRetrievalClient + from azure.search.documents.knowledgebases.models import ( + KnowledgeBaseMessage, + KnowledgeBaseMessageTextContent, + KnowledgeBaseRetrievalRequest, + KnowledgeRetrievalIntent, + KnowledgeRetrievalSemanticIntent, + ) + from azure.search.documents.knowledgebases.models import ( + KnowledgeRetrievalLowReasoningEffort as KBRetrievalLowReasoningEffort, + ) + from azure.search.documents.knowledgebases.models import ( + KnowledgeRetrievalMediumReasoningEffort as KBRetrievalMediumReasoningEffort, + ) + from azure.search.documents.knowledgebases.models import ( + KnowledgeRetrievalMinimalReasoningEffort as KBRetrievalMinimalReasoningEffort, + ) + from azure.search.documents.knowledgebases.models import ( + KnowledgeRetrievalOutputMode as KBRetrievalOutputMode, + ) + from azure.search.documents.knowledgebases.models import ( + KnowledgeRetrievalReasoningEffort as KBRetrievalReasoningEffort, + ) + +if sys.version_info >= (3, 11): + from typing import Self # pragma: no cover +else: + from typing_extensions import Self # pragma: no cover + +# Runtime imports for agentic mode (optional dependency) +try: + from azure.search.documents.knowledgebases.aio import KnowledgeBaseRetrievalClient + from azure.search.documents.knowledgebases.models import ( + KnowledgeBaseMessage, + KnowledgeBaseMessageTextContent, + KnowledgeBaseRetrievalRequest, + KnowledgeRetrievalIntent, + KnowledgeRetrievalSemanticIntent, + ) + from azure.search.documents.knowledgebases.models import ( + KnowledgeRetrievalLowReasoningEffort as KBRetrievalLowReasoningEffort, + ) + from azure.search.documents.knowledgebases.models import ( + KnowledgeRetrievalMediumReasoningEffort as KBRetrievalMediumReasoningEffort, + ) + from azure.search.documents.knowledgebases.models import ( + KnowledgeRetrievalMinimalReasoningEffort as KBRetrievalMinimalReasoningEffort, + ) + from azure.search.documents.knowledgebases.models import ( + KnowledgeRetrievalOutputMode as KBRetrievalOutputMode, + ) + from azure.search.documents.knowledgebases.models import ( + KnowledgeRetrievalReasoningEffort as KBRetrievalReasoningEffort, + ) + + _agentic_retrieval_available = True +except ImportError: + _agentic_retrieval_available = False + +logger = get_logger(__name__) + +_DEFAULT_AGENTIC_MESSAGE_HISTORY_COUNT = 10 + + +class _AzureAISearchContextProvider(BaseContextProvider): + """Azure AI Search context provider using the new BaseContextProvider hooks pattern. + + Retrieves relevant context from Azure AI Search using semantic or agentic search + modes. This is the new-pattern equivalent of :class:`AzureAISearchContextProvider`. + + Note: + This class uses a temporary ``_`` prefix to coexist with the existing + :class:`AzureAISearchContextProvider`. It will replace the existing class + in PR2. + """ + + _DEFAULT_SEARCH_CONTEXT_PROMPT: ClassVar[str] = "Use the following context to answer the question:" + + def __init__( + self, + source_id: str, + endpoint: str | None = None, + index_name: str | None = None, + api_key: str | AzureKeyCredential | None = None, + credential: AsyncTokenCredential | None = None, + *, + mode: Literal["semantic", "agentic"] = "semantic", + top_k: int = 5, + semantic_configuration_name: str | None = None, + vector_field_name: str | None = None, + embedding_function: Callable[[str], Awaitable[list[float]]] | None = None, + context_prompt: str | None = None, + azure_openai_resource_url: str | None = None, + model_deployment_name: str | None = None, + model_name: str | None = None, + knowledge_base_name: str | None = None, + retrieval_instructions: str | None = None, + azure_openai_api_key: str | None = None, + knowledge_base_output_mode: Literal["extractive_data", "answer_synthesis"] = "extractive_data", + retrieval_reasoning_effort: Literal["minimal", "medium", "low"] = "minimal", + agentic_message_history_count: int = _DEFAULT_AGENTIC_MESSAGE_HISTORY_COUNT, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize Azure AI Search Context Provider. + + Args: + source_id: Unique identifier for this provider instance. + endpoint: Azure AI Search endpoint URL. + index_name: Name of the search index to query. + api_key: API key for authentication. + credential: AsyncTokenCredential for managed identity authentication. + mode: Search mode - "semantic" or "agentic". Default: "semantic". + top_k: Maximum number of documents to retrieve. Default: 5. + semantic_configuration_name: Name of semantic configuration in the index. + vector_field_name: Name of the vector field in the index. + embedding_function: Async function to generate embeddings. + context_prompt: Custom prompt to prepend to retrieved context. + azure_openai_resource_url: Azure OpenAI resource URL for Knowledge Base. + model_deployment_name: Model deployment name in Azure OpenAI. + model_name: The underlying model name. + knowledge_base_name: Name of an existing Knowledge Base to use. + retrieval_instructions: Custom instructions for Knowledge Base retrieval. + azure_openai_api_key: Azure OpenAI API key. + knowledge_base_output_mode: Output mode for Knowledge Base retrieval. + retrieval_reasoning_effort: Reasoning effort for Knowledge Base query planning. + agentic_message_history_count: Number of recent messages for agentic mode. + env_file_path: Path to environment file for loading settings. + env_file_encoding: Encoding of the environment file. + """ + super().__init__(source_id) + + # Load settings from environment/file + try: + settings = AzureAISearchSettings( + endpoint=endpoint, + index_name=index_name, + knowledge_base_name=knowledge_base_name, + api_key=api_key if isinstance(api_key, str) else None, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Azure AI Search settings.", ex) from ex + + if not settings.endpoint: + raise ServiceInitializationError( + "Azure AI Search endpoint is required. Set via 'endpoint' parameter " + "or 'AZURE_SEARCH_ENDPOINT' environment variable." + ) + + if mode == "semantic": + if not settings.index_name: + raise ServiceInitializationError( + "Azure AI Search index name is required for semantic mode. " + "Set via 'index_name' parameter or 'AZURE_SEARCH_INDEX_NAME' environment variable." + ) + elif mode == "agentic": + if settings.index_name and settings.knowledge_base_name: + raise ServiceInitializationError( + "For agentic mode, provide either 'index_name' OR 'knowledge_base_name', not both." + ) + if not settings.index_name and not settings.knowledge_base_name: + raise ServiceInitializationError( + "For agentic mode, provide either 'index_name' or 'knowledge_base_name'." + ) + if settings.index_name and not model_deployment_name: + raise ServiceInitializationError( + "model_deployment_name is required for agentic mode when creating Knowledge Base from index." + ) + + resolved_credential: AzureKeyCredential | AsyncTokenCredential + if credential: + resolved_credential = credential + elif isinstance(api_key, AzureKeyCredential): + resolved_credential = api_key + elif settings.api_key: + resolved_credential = AzureKeyCredential(settings.api_key.get_secret_value()) + else: + raise ServiceInitializationError( + "Azure credential is required. Provide 'api_key' or 'credential' parameter " + "or set 'AZURE_SEARCH_API_KEY' environment variable." + ) + + self.endpoint = settings.endpoint + self.index_name = settings.index_name + self.credential = resolved_credential + self.mode = mode + self.top_k = top_k + self.semantic_configuration_name = semantic_configuration_name + self.vector_field_name = vector_field_name + self.embedding_function = embedding_function + self.context_prompt = context_prompt or self._DEFAULT_SEARCH_CONTEXT_PROMPT + + self.azure_openai_resource_url = azure_openai_resource_url + self.azure_openai_deployment_name = model_deployment_name + self.model_name = model_name or model_deployment_name + self.knowledge_base_name = settings.knowledge_base_name + self.retrieval_instructions = retrieval_instructions + self.azure_openai_api_key = azure_openai_api_key + self.knowledge_base_output_mode = knowledge_base_output_mode + self.retrieval_reasoning_effort = retrieval_reasoning_effort + self.agentic_message_history_count = agentic_message_history_count + + self._use_existing_knowledge_base = False + if mode == "agentic": + if settings.knowledge_base_name: + self._use_existing_knowledge_base = True + else: + self.knowledge_base_name = f"{settings.index_name}-kb" + + self._auto_discovered_vector_field = False + self._use_vectorizable_query = False + + if vector_field_name and not embedding_function: + raise ValueError("embedding_function is required when vector_field_name is specified") + + if mode == "agentic": + if not _agentic_retrieval_available: + raise ImportError( + "Agentic retrieval requires azure-search-documents >= 11.7.0b1 with Knowledge Base support." + ) + if not self._use_existing_knowledge_base and not self.azure_openai_resource_url: + raise ValueError( + "azure_openai_resource_url is required for agentic mode when creating Knowledge Base from index." + ) + + self._search_client: SearchClient | None = None + if self.index_name: + self._search_client = SearchClient( + endpoint=self.endpoint, + index_name=self.index_name, + credential=self.credential, + user_agent=AGENT_FRAMEWORK_USER_AGENT, + ) + + self._index_client: SearchIndexClient | None = None + self._retrieval_client: KnowledgeBaseRetrievalClient | None = None + if mode == "agentic": + self._index_client = SearchIndexClient( + endpoint=self.endpoint, + credential=self.credential, + user_agent=AGENT_FRAMEWORK_USER_AGENT, + ) + + self._knowledge_base_initialized = False + + async def __aenter__(self) -> Self: + """Async context manager entry.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: Any, + ) -> None: + """Async context manager exit - cleanup clients.""" + if self._retrieval_client is not None: + await self._retrieval_client.close() + self._retrieval_client = None + + # -- Hooks pattern --------------------------------------------------------- + + async def before_run( + self, + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Retrieve relevant context from Azure AI Search and add to session context.""" + messages_list = list(context.input_messages) + + def get_role_value(role: str | Any) -> str: + return role.value if hasattr(role, "value") else str(role) + + filtered_messages = [ + msg + for msg in messages_list + if msg and msg.text and msg.text.strip() and get_role_value(msg.role) in ["user", "assistant"] + ] + if not filtered_messages: + return + + if self.mode == "semantic": + query = "\n".join(msg.text for msg in filtered_messages) + search_result_parts = await self._semantic_search(query) + else: + recent_messages = filtered_messages[-self.agentic_message_history_count :] + search_result_parts = await self._agentic_search(recent_messages) + + if not search_result_parts: + return + + context_messages = [Message(role="user", text=self.context_prompt)] + context_messages.extend([Message(role="user", text=part) for part in search_result_parts]) + context.extend_messages(self.source_id, context_messages) + + # -- Internal methods (ported from AzureAISearchContextProvider) ----------- + + def _find_vector_fields(self, index: Any) -> list[str]: + """Find all fields that can store vectors.""" + return [ + field.name + for field in index.fields + if field.vector_search_dimensions is not None and field.vector_search_dimensions > 0 + ] + + def _find_vectorizable_fields(self, index: Any, vector_fields: list[str]) -> list[str]: + """Find vector fields that have auto-vectorization configured.""" + vectorizable_fields: list[str] = [] + if not index.vector_search or not index.vector_search.profiles: + return vectorizable_fields + for field in index.fields: + if field.name in vector_fields and field.vector_search_profile_name: + profile = next( + (p for p in index.vector_search.profiles if p.name == field.vector_search_profile_name), None + ) + if profile and hasattr(profile, "vectorizer_name") and profile.vectorizer_name: + vectorizable_fields.append(field.name) + return vectorizable_fields + + async def _auto_discover_vector_field(self) -> None: + """Auto-discover vector field from index schema.""" + if self._auto_discovered_vector_field or self.vector_field_name: + return + + try: + if not self._index_client: + self._index_client = SearchIndexClient( + endpoint=self.endpoint, + credential=self.credential, + user_agent=AGENT_FRAMEWORK_USER_AGENT, + ) + if not self.index_name: + logger.warning("Cannot auto-discover vector field: index_name is not set.") + self._auto_discovered_vector_field = True + return + + index = await self._index_client.get_index(self.index_name) + vector_fields = self._find_vector_fields(index) + if not vector_fields: + logger.info(f"No vector fields found in index '{self.index_name}'. Using keyword-only search.") + self._auto_discovered_vector_field = True + return + + vectorizable_fields = self._find_vectorizable_fields(index, vector_fields) + if vectorizable_fields: + if len(vectorizable_fields) == 1: + self.vector_field_name = vectorizable_fields[0] + self._auto_discovered_vector_field = True + self._use_vectorizable_query = True + logger.info( + f"Auto-discovered vectorizable field '{self.vector_field_name}' with server-side vectorization." + ) + else: + logger.warning( + f"Multiple vectorizable fields found: {vectorizable_fields}. " + f"Please specify vector_field_name explicitly." + ) + elif len(vector_fields) == 1: + self.vector_field_name = vector_fields[0] + self._auto_discovered_vector_field = True + self._use_vectorizable_query = False + if not self.embedding_function: + logger.warning( + f"Auto-discovered vector field '{self.vector_field_name}' without server-side vectorization. " + f"Provide embedding_function for vector search." + ) + self.vector_field_name = None + else: + logger.warning( + f"Multiple vector fields found: {vector_fields}. Please specify vector_field_name explicitly." + ) + except Exception as e: + logger.warning(f"Failed to auto-discover vector field: {e}. Using keyword-only search.") + + self._auto_discovered_vector_field = True + + async def _semantic_search(self, query: str) -> list[str]: + """Perform semantic hybrid search.""" + await self._auto_discover_vector_field() + + vector_queries: list[VectorizableTextQuery | VectorizedQuery] = [] + if self.vector_field_name: + vector_k = max(self.top_k, 50) if self.semantic_configuration_name else self.top_k + if self._use_vectorizable_query: + vector_queries = [ + VectorizableTextQuery(text=query, k_nearest_neighbors=vector_k, fields=self.vector_field_name) + ] + elif self.embedding_function: + query_vector = await self.embedding_function(query) + vector_queries = [ + VectorizedQuery(vector=query_vector, k_nearest_neighbors=vector_k, fields=self.vector_field_name) + ] + + search_params: dict[str, Any] = {"search_text": query, "top": self.top_k} + if vector_queries: + search_params["vector_queries"] = vector_queries + if self.semantic_configuration_name: + search_params["query_type"] = QueryType.SEMANTIC + search_params["semantic_configuration_name"] = self.semantic_configuration_name + search_params["query_caption"] = QueryCaptionType.EXTRACTIVE + + if not self._search_client: + raise RuntimeError("Search client is not initialized.") + results = await self._search_client.search(**search_params) # type: ignore[reportUnknownVariableType] + + formatted_results: list[str] = [] + async for doc in results: # type: ignore[reportUnknownVariableType] + doc_id = doc.get("id") or doc.get("@search.id") # type: ignore[reportUnknownVariableType] + doc_text: str = self._extract_document_text(doc, doc_id=doc_id) # type: ignore[reportUnknownArgumentType] + if doc_text: + formatted_results.append(doc_text) # type: ignore[reportUnknownArgumentType] + return formatted_results + + async def _ensure_knowledge_base(self) -> None: + """Ensure Knowledge Base and knowledge source are created or use existing KB.""" + if self._knowledge_base_initialized: + return + + if not self.knowledge_base_name: + raise ValueError("knowledge_base_name is required for agentic mode") + + knowledge_base_name = self.knowledge_base_name + + if self._use_existing_knowledge_base: + if _agentic_retrieval_available and self._retrieval_client is None: + self._retrieval_client = KnowledgeBaseRetrievalClient( + endpoint=self.endpoint, + knowledge_base_name=knowledge_base_name, + credential=self.credential, + user_agent=AGENT_FRAMEWORK_USER_AGENT, + ) + self._knowledge_base_initialized = True + return + + if not self._index_client: + raise ValueError("Index client is required when creating Knowledge Base from index") + if not self.azure_openai_resource_url: + raise ValueError("azure_openai_resource_url is required when creating Knowledge Base from index") + if not self.azure_openai_deployment_name: + raise ValueError("model_deployment_name is required when creating Knowledge Base from index") + if not self.index_name: + raise ValueError("index_name is required when creating Knowledge Base from index") + + knowledge_source_name = f"{self.index_name}-source" + try: + await self._index_client.get_knowledge_source(knowledge_source_name) + except ResourceNotFoundError: + knowledge_source = SearchIndexKnowledgeSource( + name=knowledge_source_name, + description=f"Knowledge source for {self.index_name} search index", + search_index_parameters=SearchIndexKnowledgeSourceParameters( + search_index_name=self.index_name, + ), + ) + await self._index_client.create_knowledge_source(knowledge_source) + + aoai_params = AzureOpenAIVectorizerParameters( + resource_url=self.azure_openai_resource_url, + deployment_name=self.azure_openai_deployment_name, + model_name=self.model_name, + api_key=self.azure_openai_api_key, + ) + + output_mode = ( + KnowledgeRetrievalOutputMode.EXTRACTIVE_DATA + if self.knowledge_base_output_mode == "extractive_data" + else KnowledgeRetrievalOutputMode.ANSWER_SYNTHESIS + ) + reasoning_effort_map: dict[str, KnowledgeRetrievalReasoningEffort] = { + "minimal": KnowledgeRetrievalMinimalReasoningEffort(), + "medium": KnowledgeRetrievalMediumReasoningEffort(), + "low": KnowledgeRetrievalLowReasoningEffort(), + } + reasoning_effort = reasoning_effort_map[self.retrieval_reasoning_effort] + + knowledge_base = KnowledgeBase( + name=knowledge_base_name, + description=f"Knowledge Base for multi-hop retrieval across {self.index_name}", + knowledge_sources=[KnowledgeSourceReference(name=knowledge_source_name)], + models=[KnowledgeBaseAzureOpenAIModel(azure_open_ai_parameters=aoai_params)], + output_mode=output_mode, + retrieval_reasoning_effort=reasoning_effort, + ) + await self._index_client.create_or_update_knowledge_base(knowledge_base) + self._knowledge_base_initialized = True + + if _agentic_retrieval_available and self._retrieval_client is None: + self._retrieval_client = KnowledgeBaseRetrievalClient( + endpoint=self.endpoint, + knowledge_base_name=knowledge_base_name, + credential=self.credential, + user_agent=AGENT_FRAMEWORK_USER_AGENT, + ) + + async def _agentic_search(self, messages: list[Message]) -> list[str]: + """Perform agentic retrieval with multi-hop reasoning.""" + await self._ensure_knowledge_base() + + reasoning_effort_map: dict[str, KBRetrievalReasoningEffort] = { + "minimal": KBRetrievalMinimalReasoningEffort(), + "medium": KBRetrievalMediumReasoningEffort(), + "low": KBRetrievalLowReasoningEffort(), + } + reasoning_effort = reasoning_effort_map[self.retrieval_reasoning_effort] + + output_mode = ( + KBRetrievalOutputMode.EXTRACTIVE_DATA + if self.knowledge_base_output_mode == "extractive_data" + else KBRetrievalOutputMode.ANSWER_SYNTHESIS + ) + + if self.retrieval_reasoning_effort == "minimal": + query = "\n".join(msg.text for msg in messages if msg.text) + intents: list[KnowledgeRetrievalIntent] = [KnowledgeRetrievalSemanticIntent(search=query)] + retrieval_request = KnowledgeBaseRetrievalRequest( + intents=intents, + retrieval_reasoning_effort=reasoning_effort, + output_mode=output_mode, + include_activity=True, + ) + else: + kb_messages = [ + KnowledgeBaseMessage( + role=msg.role if hasattr(msg.role, "value") else str(msg.role), + content=[KnowledgeBaseMessageTextContent(text=msg.text)], + ) + for msg in messages + if msg.text + ] + retrieval_request = KnowledgeBaseRetrievalRequest( + messages=kb_messages, + retrieval_reasoning_effort=reasoning_effort, + output_mode=output_mode, + include_activity=True, + ) + + if not self._retrieval_client: + raise RuntimeError("Retrieval client not initialized.") + retrieval_result = await self._retrieval_client.retrieve(retrieval_request=retrieval_request) + + if retrieval_result.response and len(retrieval_result.response) > 0: + assistant_message = retrieval_result.response[-1] + if assistant_message.content: + answer_parts: list[str] = [] + for content_item in assistant_message.content: + if isinstance(content_item, KnowledgeBaseMessageTextContent) and content_item.text: + answer_parts.append(content_item.text) + if answer_parts: + return answer_parts + + return ["No results found from Knowledge Base."] + + def _extract_document_text(self, doc: dict[str, Any], doc_id: str | None = None) -> str: + """Extract readable text from a search document with optional citation.""" + text = "" + for field in ["content", "text", "description", "body", "chunk"]: + if doc.get(field): + text = str(doc[field]) + break + if not text: + text_parts: list[str] = [] + for key, value in doc.items(): + if isinstance(value, str) and not key.startswith("@") and key != "id": + text_parts.append(f"{key}: {value}") + text = " | ".join(text_parts) if text_parts else "" + if doc_id and text: + return f"[Source: {doc_id}] {text}" + return text + + +__all__ = ["_AzureAISearchContextProvider"] diff --git a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py index 734d6c08e7..332c477d85 100644 --- a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py +++ b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_search_provider.py @@ -7,7 +7,7 @@ import sys from collections.abc import Awaitable, Callable, MutableSequence from typing import TYPE_CHECKING, Any, ClassVar, Literal -from agent_framework import AGENT_FRAMEWORK_USER_AGENT, ChatMessage, Context, ContextProvider +from agent_framework import AGENT_FRAMEWORK_USER_AGENT, Context, ContextProvider, Message from agent_framework._logging import get_logger from agent_framework._pydantic import AFBaseSettings from agent_framework.exceptions import ServiceInitializationError @@ -511,7 +511,7 @@ class AzureAISearchContextProvider(ContextProvider): @override async def invoking( self, - messages: ChatMessage | MutableSequence[ChatMessage], + messages: Message | MutableSequence[Message], **kwargs: Any, ) -> Context: """Retrieve relevant context from Azure AI Search before model invocation. @@ -524,7 +524,7 @@ class AzureAISearchContextProvider(ContextProvider): Context object with retrieved documents as messages. """ # Convert to list and filter to USER/ASSISTANT messages with text only - messages_list = [messages] if isinstance(messages, ChatMessage) else list(messages) + messages_list = [messages] if isinstance(messages, Message) else list(messages) def get_role_value(role: str | Any) -> str: return role.value if hasattr(role, "value") else str(role) @@ -553,8 +553,8 @@ class AzureAISearchContextProvider(ContextProvider): return Context() # Create context messages: first message with prompt, then one message per result part - context_messages = [ChatMessage(role="user", text=self.context_prompt)] - context_messages.extend([ChatMessage(role="user", text=part) for part in search_result_parts]) + context_messages = [Message(role="user", text=self.context_prompt)] + context_messages.extend([Message(role="user", text=part) for part in search_result_parts]) return Context(messages=context_messages) @@ -875,7 +875,7 @@ class AzureAISearchContextProvider(ContextProvider): user_agent=AGENT_FRAMEWORK_USER_AGENT, ) - async def _agentic_search(self, messages: list[ChatMessage]) -> list[str]: + async def _agentic_search(self, messages: list[Message]) -> list[str]: """Perform agentic retrieval with multi-hop reasoning using Knowledge Bases. This mode uses query planning and is slightly slower than semantic search, diff --git a/python/packages/azure-ai-search/pyproject.toml b/python/packages/azure-ai-search/pyproject.toml index cfc7c4786e..8e7afcc89c 100644 --- a/python/packages/azure-ai-search/pyproject.toml +++ b/python/packages/azure-ai-search/pyproject.toml @@ -4,7 +4,7 @@ description = "Azure AI Search integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "azure-search-documents==11.7.0b2", ] diff --git a/python/packages/azure-ai-search/tests/test_aisearch_new_context_provider.py b/python/packages/azure-ai-search/tests/test_aisearch_new_context_provider.py new file mode 100644 index 0000000000..e9af893273 --- /dev/null +++ b/python/packages/azure-ai-search/tests/test_aisearch_new_context_provider.py @@ -0,0 +1,293 @@ +# Copyright (c) Microsoft. All rights reserved. +# pyright: reportPrivateUsage=false + +import os +from unittest.mock import AsyncMock, patch + +import pytest +from agent_framework import Message +from agent_framework._sessions import AgentSession, SessionContext +from agent_framework.exceptions import ServiceInitializationError + +from agent_framework_azure_ai_search._context_provider import _AzureAISearchContextProvider + +# -- Helpers ------------------------------------------------------------------- + + +class MockSearchResults: + """Async-iterable mock for Azure SearchClient.search() results.""" + + def __init__(self, docs: list[dict]): + self._docs = docs + self._index = 0 + + def __aiter__(self): + return self + + async def __anext__(self): + if self._index >= len(self._docs): + raise StopAsyncIteration + doc = self._docs[self._index] + self._index += 1 + return doc + + +@pytest.fixture +def mock_search_client() -> AsyncMock: + """Create a mock SearchClient that returns one document.""" + client = AsyncMock() + + async def _search(**kwargs): + return MockSearchResults([{"id": "doc1", "content": "test document"}]) + + client.search = AsyncMock(side_effect=_search) + return client + + +@pytest.fixture +def mock_search_client_empty() -> AsyncMock: + """Create a mock SearchClient that returns no results.""" + client = AsyncMock() + + async def _search(**kwargs): + return MockSearchResults([]) + + client.search = AsyncMock(side_effect=_search) + return client + + +def _make_provider(**overrides) -> _AzureAISearchContextProvider: + """Create a semantic-mode provider with mocked internals (skips auto-discovery).""" + defaults = { + "source_id": "aisearch", + "endpoint": "https://test.search.windows.net", + "index_name": "test-index", + "api_key": "test-key", + } + defaults.update(overrides) + provider = _AzureAISearchContextProvider(**defaults) + provider._auto_discovered_vector_field = True # skip auto-discovery + return provider + + +# -- Initialization: semantic mode --------------------------------------------- + + +class TestInitSemantic: + """Initialization tests for semantic mode.""" + + def test_valid_init(self) -> None: + provider = _make_provider() + assert provider.source_id == "aisearch" + assert provider.endpoint == "https://test.search.windows.net" + assert provider.index_name == "test-index" + assert provider.mode == "semantic" + + def test_source_id_set(self) -> None: + provider = _make_provider(source_id="my-source") + assert provider.source_id == "my-source" + + def test_missing_endpoint_raises(self) -> None: + with patch.dict(os.environ, {}, clear=True), pytest.raises(ServiceInitializationError, match="endpoint"): + _AzureAISearchContextProvider( + source_id="s", + endpoint=None, + index_name="idx", + api_key="key", + ) + + def test_missing_index_name_semantic_raises(self) -> None: + with pytest.raises(ServiceInitializationError, match="index name"): + _AzureAISearchContextProvider( + source_id="s", + endpoint="https://test.search.windows.net", + index_name=None, + api_key="key", + ) + + def test_env_variable_fallback(self) -> None: + env = { + "AZURE_SEARCH_ENDPOINT": "https://env.search.windows.net", + "AZURE_SEARCH_INDEX_NAME": "env-index", + "AZURE_SEARCH_API_KEY": "env-key", + } + with patch.dict(os.environ, env, clear=False): + provider = _AzureAISearchContextProvider(source_id="env-test") + assert provider.endpoint == "https://env.search.windows.net" + assert provider.index_name == "env-index" + + +# -- Initialization: agentic mode validation ----------------------------------- + + +class TestInitAgenticValidation: + """Initialization validation tests for agentic mode.""" + + def test_both_index_and_kb_raises(self) -> None: + with pytest.raises(ServiceInitializationError, match="not both"): + _AzureAISearchContextProvider( + source_id="s", + endpoint="https://test.search.windows.net", + index_name="idx", + knowledge_base_name="kb", + api_key="key", + mode="agentic", + model_deployment_name="deploy", + azure_openai_resource_url="https://aoai.openai.azure.com", + ) + + def test_neither_index_nor_kb_raises(self) -> None: + with pytest.raises(ServiceInitializationError, match="provide either"): + _AzureAISearchContextProvider( + source_id="s", + endpoint="https://test.search.windows.net", + api_key="key", + mode="agentic", + ) + + def test_missing_model_deployment_name_raises(self) -> None: + with pytest.raises(ServiceInitializationError, match="model_deployment_name"): + _AzureAISearchContextProvider( + source_id="s", + endpoint="https://test.search.windows.net", + index_name="idx", + api_key="key", + mode="agentic", + azure_openai_resource_url="https://aoai.openai.azure.com", + ) + + def test_vector_field_without_embedding_raises(self) -> None: + with pytest.raises(ValueError, match="embedding_function"): + _AzureAISearchContextProvider( + source_id="s", + endpoint="https://test.search.windows.net", + index_name="idx", + api_key="key", + vector_field_name="embedding", + ) + + +# -- before_run: semantic mode ------------------------------------------------- + + +class TestBeforeRunSemantic: + """Tests for before_run in semantic mode.""" + + async def test_results_added_to_context(self, mock_search_client: AsyncMock) -> None: + provider = _make_provider() + provider._search_client = mock_search_client + + session = AgentSession(session_id="test-session") + ctx = SessionContext( + input_messages=[Message(role="user", contents=["test query"])], + session_id="s1", + ) + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_search_client.search.assert_awaited_once() + msgs = ctx.context_messages.get("aisearch", []) + assert len(msgs) >= 2 # context_prompt + at least one result + assert msgs[0].text == provider.context_prompt + + async def test_empty_input_no_search(self, mock_search_client: AsyncMock) -> None: + provider = _make_provider() + provider._search_client = mock_search_client + + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[], session_id="s1") + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_search_client.search.assert_not_awaited() + assert ctx.context_messages.get("aisearch") is None + + async def test_no_results_no_messages(self, mock_search_client_empty: AsyncMock) -> None: + provider = _make_provider() + provider._search_client = mock_search_client_empty + + session = AgentSession(session_id="test-session") + ctx = SessionContext( + input_messages=[Message(role="user", contents=["test query"])], + session_id="s1", + ) + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_search_client_empty.search.assert_awaited_once() + assert ctx.context_messages.get("aisearch") is None + + async def test_context_prompt_prepended(self, mock_search_client: AsyncMock) -> None: + custom_prompt = "Custom search context:" + provider = _make_provider(context_prompt=custom_prompt) + provider._search_client = mock_search_client + + session = AgentSession(session_id="test-session") + ctx = SessionContext( + input_messages=[Message(role="user", contents=["test query"])], + session_id="s1", + ) + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + msgs = ctx.context_messages["aisearch"] + assert msgs[0].text == custom_prompt + + +# -- before_run: message filtering --------------------------------------------- + + +class TestBeforeRunFiltering: + """Tests that only user/assistant messages are used for search.""" + + async def test_filters_non_user_assistant(self, mock_search_client: AsyncMock) -> None: + provider = _make_provider() + provider._search_client = mock_search_client + + session = AgentSession(session_id="test-session") + ctx = SessionContext( + input_messages=[ + Message(role="system", contents=["system prompt"]), + Message(role="user", contents=["actual question"]), + ], + session_id="s1", + ) + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_search_client.search.assert_awaited_once() + call_kwargs = mock_search_client.search.call_args[1] + # The search text should contain only the user message, not the system message + assert "actual question" in call_kwargs["search_text"] + assert "system prompt" not in call_kwargs["search_text"] + + async def test_only_system_messages_no_search(self, mock_search_client: AsyncMock) -> None: + provider = _make_provider() + provider._search_client = mock_search_client + + session = AgentSession(session_id="test-session") + ctx = SessionContext( + input_messages=[Message(role="system", contents=["system prompt"])], + session_id="s1", + ) + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_search_client.search.assert_not_awaited() + + +# -- __aexit__ ----------------------------------------------------------------- + + +class TestAexit: + """Tests for async context manager cleanup.""" + + async def test_closes_retrieval_client(self) -> None: + provider = _make_provider() + mock_retrieval = AsyncMock() + provider._retrieval_client = mock_retrieval + + await provider.__aexit__(None, None, None) + + mock_retrieval.close.assert_awaited_once() + assert provider._retrieval_client is None + + async def test_no_retrieval_client_no_error(self) -> None: + provider = _make_provider() + assert provider._retrieval_client is None + + await provider.__aexit__(None, None, None) # should not raise diff --git a/python/packages/azure-ai-search/tests/test_search_provider.py b/python/packages/azure-ai-search/tests/test_search_provider.py index 4e118df02e..def95cd732 100644 --- a/python/packages/azure-ai-search/tests/test_search_provider.py +++ b/python/packages/azure-ai-search/tests/test_search_provider.py @@ -5,7 +5,7 @@ import os from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import ChatMessage, Context +from agent_framework import Context, Message from agent_framework.azure import AzureAISearchContextProvider, AzureAISearchSettings from agent_framework.exceptions import ServiceInitializationError from azure.core.credentials import AzureKeyCredential @@ -36,10 +36,10 @@ def mock_index_client() -> AsyncMock: @pytest.fixture -def sample_messages() -> list[ChatMessage]: +def sample_messages() -> list[Message]: """Create sample chat messages for testing.""" return [ - ChatMessage(role="user", text="What is in the documents?"), + Message(role="user", text="What is in the documents?"), ] @@ -276,9 +276,7 @@ class TestSemanticSearch: @pytest.mark.asyncio @patch("agent_framework_azure_ai_search._search_provider.SearchClient") - async def test_semantic_search_basic( - self, mock_search_class: MagicMock, sample_messages: list[ChatMessage] - ) -> None: + async def test_semantic_search_basic(self, mock_search_class: MagicMock, sample_messages: list[Message]) -> None: """Test basic semantic search without vector search.""" # Setup mock mock_search_client = AsyncMock() @@ -318,7 +316,7 @@ class TestSemanticSearch: ) # Empty message - context = await provider.invoking([ChatMessage(role="user", text="")]) + context = await provider.invoking([Message(role="user", text="")]) assert isinstance(context, Context) assert len(context.messages) == 0 @@ -326,7 +324,7 @@ class TestSemanticSearch: @pytest.mark.asyncio @patch("agent_framework_azure_ai_search._search_provider.SearchClient") async def test_semantic_search_with_vector_query( - self, mock_search_class: MagicMock, sample_messages: list[ChatMessage] + self, mock_search_class: MagicMock, sample_messages: list[Message] ) -> None: """Test semantic search with vector query.""" # Setup mock @@ -520,10 +518,10 @@ class TestMessageFiltering: # Mix of message types messages = [ - ChatMessage(role="system", text="System message"), - ChatMessage(role="user", text="User message"), - ChatMessage(role="assistant", text="Assistant message"), - ChatMessage(role="tool", text="Tool message"), + Message(role="system", text="System message"), + Message(role="user", text="User message"), + Message(role="assistant", text="Assistant message"), + Message(role="tool", text="Tool message"), ] context = await provider.invoking(messages) @@ -548,9 +546,9 @@ class TestMessageFiltering: # Messages with empty/whitespace text messages = [ - ChatMessage(role="user", text=""), - ChatMessage(role="user", text=" "), - ChatMessage(role="user", text=""), # ChatMessage with None text becomes empty string + Message(role="user", text=""), + Message(role="user", text=" "), + Message(role="user", text=""), # Message with None text becomes empty string ] context = await provider.invoking(messages) @@ -581,7 +579,7 @@ class TestCitations: mode="semantic", ) - context = await provider.invoking([ChatMessage(role="user", text="test query")]) + context = await provider.invoking([Message(role="user", text="test query")]) # Check that citation is included assert isinstance(context, Context) @@ -603,7 +601,7 @@ class TestAgenticSearch: mock_search_class: MagicMock, mock_index_class: MagicMock, mock_retrieval_class: MagicMock, - sample_messages: list[ChatMessage], + sample_messages: list[Message], ) -> None: """Test basic agentic search with Knowledge Base retrieval.""" # Setup search client mock @@ -660,7 +658,7 @@ class TestAgenticSearch: mock_search_class: MagicMock, mock_index_class: MagicMock, mock_retrieval_class: MagicMock, - sample_messages: list[ChatMessage], + sample_messages: list[Message], ) -> None: """Test agentic search when no results are returned.""" # Setup mocks @@ -705,7 +703,7 @@ class TestAgenticSearch: mock_search_class: MagicMock, mock_index_class: MagicMock, mock_retrieval_class: MagicMock, - sample_messages: list[ChatMessage], + sample_messages: list[Message], ) -> None: """Test agentic search with medium reasoning effort.""" # Setup mocks diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py index dcc0e9db29..c6d68daaa2 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py @@ -4,30 +4,27 @@ from __future__ import annotations import sys from collections.abc import Callable, MutableMapping, Sequence -from typing import TYPE_CHECKING, Any, Generic, cast +from typing import Any, Generic, cast from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, - ChatAgent, + Agent, ContextProvider, FunctionTool, MiddlewareTypes, - ToolProtocol, normalize_tools, ) from agent_framework._mcp import MCPTool from agent_framework.exceptions import ServiceInitializationError from azure.ai.agents.aio import AgentsClient -from azure.ai.agents.models import Agent, ResponseFormatJsonSchema, ResponseFormatJsonSchemaType +from azure.ai.agents.models import Agent as AzureAgent +from azure.ai.agents.models import ResponseFormatJsonSchema, ResponseFormatJsonSchemaType from azure.core.credentials_async import AsyncTokenCredential from pydantic import BaseModel, ValidationError -from ._chat_client import AzureAIAgentClient +from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions from ._shared import AzureAISettings, from_azure_ai_agent_tools, to_azure_ai_agent_tools -if TYPE_CHECKING: - from ._chat_client import AzureAIAgentOptions - if sys.version_info >= (3, 13): from typing import Self, TypeVar # type: ignore # pragma: no cover else: @@ -38,7 +35,7 @@ else: from typing_extensions import TypedDict # type: ignore # pragma: no cover -# Type variable for options - allows typed ChatAgent[OptionsCoT] returns +# Type variable for options - allows typed Agent[TOptions] returns # Default matches AzureAIAgentClient's default options type OptionsCoT = TypeVar( "OptionsCoT", @@ -51,7 +48,7 @@ OptionsCoT = TypeVar( class AzureAIAgentsProvider(Generic[OptionsCoT]): """Provider for Azure AI Agent Service V1 (Persistent Agents API). - This provider enables creating, retrieving, and wrapping Azure AI agents as ChatAgent + This provider enables creating, retrieving, and wrapping Azure AI agents as Agent instances. It manages the underlying AgentsClient lifecycle and provides a high-level interface for agent operations. @@ -171,19 +168,19 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): model: str | None = None, instructions: str | None = None, description: str | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Create a new agent on the Azure AI service and return a ChatAgent. + ) -> Agent[OptionsCoT]: + """Create a new agent on the Azure AI service and return a Agent. This method creates a persistent agent on the Azure AI service with the specified - configuration and returns a local ChatAgent instance for interaction. + configuration and returns a local Agent instance for interaction. Args: name: The name for the agent. @@ -200,7 +197,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): context_provider: Context provider to include during agent invocation. Returns: - ChatAgent: A ChatAgent instance configured with the created agent. + Agent: A Agent instance configured with the created agent. Raises: ServiceInitializationError: If model deployment name is not available. @@ -240,7 +237,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): args["response_format"] = self._create_response_format_config(response_format) # Normalize and convert tools - # Local MCP tools (MCPTool) are handled by ChatAgent at runtime, not stored on the Azure agent + # Local MCP tools (MCPTool) are handled by Agent at runtime, not stored on the Azure agent normalized_tools = normalize_tools(tools) if normalized_tools: # Only convert non-MCP tools to Azure AI format @@ -255,7 +252,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): # Create the agent on the service created_agent = await self._agents_client.create_agent(**args) - # Create ChatAgent wrapper + # Create Agent wrapper return self._to_chat_agent_from_agent( created_agent, normalized_tools, @@ -268,19 +265,19 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): self, id: str, *, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Retrieve an existing agent from the service and return a ChatAgent. + ) -> Agent[OptionsCoT]: + """Retrieve an existing agent from the service and return a Agent. This method fetches an agent by ID from the Azure AI service - and returns a local ChatAgent instance for interaction. + and returns a local Agent instance for interaction. Args: id: The ID of the agent to retrieve from the service. @@ -294,7 +291,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): context_provider: Context provider to include during agent invocation. Returns: - ChatAgent: A ChatAgent instance configured with the retrieved agent. + Agent: A Agent instance configured with the retrieved agent. Raises: ServiceInitializationError: If required function tools are not provided. @@ -323,17 +320,17 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): def as_agent( self, - agent: Agent, - tools: ToolProtocol + agent: AzureAgent, + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Wrap an existing Agent SDK object as a ChatAgent without making HTTP calls. + ) -> Agent[OptionsCoT]: + """Wrap an existing Agent SDK object as a Agent without making HTTP calls. Use this method when you already have an Agent object from a previous SDK operation and want to use it with the Agent Framework. @@ -348,7 +345,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): context_provider: Context provider to include during agent invocation. Returns: - ChatAgent: A ChatAgent instance configured with the agent. + Agent: A Agent instance configured with the agent. Raises: ServiceInitializationError: If required function tools are not provided. @@ -363,7 +360,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): instructions="...", ) - # Wrap as ChatAgent + # Wrap as Agent chat_agent = provider.as_agent(sdk_agent) """ # Validate function tools @@ -380,13 +377,13 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): def _to_chat_agent_from_agent( self, - agent: Agent, - provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None = None, + agent: AzureAgent, + provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None = None, default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Create a ChatAgent from an Agent SDK object. + ) -> Agent[OptionsCoT]: + """Create a Agent from an Agent SDK object. Args: agent: The Agent SDK object. @@ -408,8 +405,8 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): # Merge tools: convert agent's hosted tools + user-provided function tools merged_tools = self._merge_tools(agent.tools, provided_tools) - return ChatAgent( # type: ignore[return-value] - chat_client=client, + return Agent( # type: ignore[return-value] + client=client, id=agent.id, name=agent.name, description=agent.description, @@ -424,8 +421,8 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): def _merge_tools( self, agent_tools: Sequence[Any] | None, - provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None, - ) -> list[ToolProtocol | dict[str, Any]]: + provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None, + ) -> list[FunctionTool | dict[str, Any]]: """Merge hosted tools from agent with user-provided function tools. Args: @@ -433,9 +430,9 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): provided_tools: User-provided tools (Agent Framework format). Returns: - Combined list of tools for the ChatAgent. + Combined list of tools for the Agent. """ - merged: list[ToolProtocol | dict[str, Any]] = [] + merged: list[FunctionTool | dict[str, Any]] = [] # Convert hosted tools from agent definition hosted_tools = from_azure_ai_agent_tools(agent_tools) @@ -452,7 +449,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): if provided_tools: for provided_tool in provided_tools: # FunctionTool - has implementation for function calling - # MCPTool - ChatAgent handles MCP connection and tool discovery at runtime + # MCPTool - Agent handles MCP connection and tool discovery at runtime if isinstance(provided_tool, (FunctionTool, MCPTool)): merged.append(provided_tool) # type: ignore[reportUnknownArgumentType] @@ -461,7 +458,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): def _validate_function_tools( self, agent_tools: Sequence[Any] | None, - provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None, + provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None, ) -> None: """Validate that required function tools are provided. diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index 504f615a0a..7a11734908 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -12,11 +12,10 @@ from typing import Any, ClassVar, Generic, TypedDict from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, + Agent, Annotation, BaseChatClient, - ChatAgent, ChatAndFunctionMiddlewareTypes, - ChatMessage, ChatMessageStoreProtocol, ChatMiddlewareLayer, ChatOptions, @@ -27,15 +26,11 @@ from agent_framework import ( FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, - HostedCodeInterpreterTool, - HostedFileSearchTool, - HostedMCPTool, - HostedWebSearchTool, + Message, MiddlewareTypes, ResponseStream, Role, TextSpanRegion, - ToolProtocol, UsageDetails, get_logger, prepare_function_call_results, @@ -44,7 +39,9 @@ from agent_framework.exceptions import ServiceInitializationError, ServiceInvali from agent_framework.observability import ChatTelemetryLayer from azure.ai.agents.aio import AgentsClient from azure.ai.agents.models import ( - Agent, + Agent as AzureAgent, +) +from azure.ai.agents.models import ( AgentsNamedToolChoice, AgentsNamedToolChoiceType, AgentsToolChoiceOptionMode, @@ -53,7 +50,7 @@ from azure.ai.agents.models import ( AsyncAgentRunStream, BingCustomSearchTool, BingGroundingTool, - CodeInterpreterToolDefinition, + CodeInterpreterTool, FileSearchTool, FunctionName, FunctionToolDefinition, @@ -215,6 +212,198 @@ class AzureAIAgentClient( OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai" # type: ignore[reportIncompatibleVariableOverride, misc] + # region Hosted Tool Factory Methods + + @staticmethod + def get_code_interpreter_tool() -> CodeInterpreterTool: + """Create a code interpreter tool configuration for Azure AI Agents. + + Returns: + A CodeInterpreterTool instance ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.azure import AzureAIAgentClient + + tool = AzureAIAgentClient.get_code_interpreter_tool() + agent = ChatAgent(client, tools=[tool]) + """ + return CodeInterpreterTool() + + @staticmethod + def get_file_search_tool( + *, + vector_store_ids: list[str], + ) -> FileSearchTool: + """Create a file search tool configuration for Azure AI Agents. + + Keyword Args: + vector_store_ids: List of vector store IDs to search within. + + Returns: + A FileSearchTool instance ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.azure import AzureAIAgentClient + + tool = AzureAIAgentClient.get_file_search_tool( + vector_store_ids=["vs_abc123"], + ) + agent = ChatAgent(client, tools=[tool]) + """ + return FileSearchTool(vector_store_ids=vector_store_ids) + + @staticmethod + def get_web_search_tool( + *, + bing_connection_id: str | None = None, + bing_custom_connection_id: str | None = None, + bing_custom_instance_id: str | None = None, + ) -> BingGroundingTool | BingCustomSearchTool: + """Create a web search tool configuration for Azure AI Agents. + + For Azure AI Agents, web search uses Bing Grounding or Bing Custom Search. + If no arguments are provided, attempts to read from environment variables. + If no connection IDs are found, raises ValueError. + + Keyword Args: + bing_connection_id: The Bing Grounding connection ID for standard web search. + Falls back to BING_CONNECTION_ID environment variable. + bing_custom_connection_id: The Bing Custom Search connection ID. + Falls back to BING_CUSTOM_CONNECTION_ID environment variable. + bing_custom_instance_id: The Bing Custom Search instance ID. + Falls back to BING_CUSTOM_INSTANCE_NAME environment variable. + + Returns: + A BingGroundingTool or BingCustomSearchTool instance ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.azure import AzureAIAgentClient + + # Bing Grounding (explicit) + tool = AzureAIAgentClient.get_web_search_tool( + bing_connection_id="conn_bing_123", + ) + + # Bing Grounding (from environment variable) + tool = AzureAIAgentClient.get_web_search_tool() + + # Bing Custom Search (explicit) + tool = AzureAIAgentClient.get_web_search_tool( + bing_custom_connection_id="conn_custom_123", + bing_custom_instance_id="instance_456", + ) + + # Bing Custom Search (from environment variables) + # Set BING_CUSTOM_CONNECTION_ID and BING_CUSTOM_INSTANCE_NAME + tool = AzureAIAgentClient.get_web_search_tool() + + agent = ChatAgent(client, tools=[tool]) + """ + # Try explicit Bing Custom Search parameters first, then environment variables + resolved_custom_connection = bing_custom_connection_id or os.environ.get("BING_CUSTOM_CONNECTION_ID") + resolved_custom_instance = bing_custom_instance_id or os.environ.get("BING_CUSTOM_INSTANCE_NAME") + + if resolved_custom_connection and resolved_custom_instance: + return BingCustomSearchTool( + connection_id=resolved_custom_connection, + instance_name=resolved_custom_instance, + ) + + # Try explicit Bing Grounding parameter first, then environment variable + resolved_connection_id = bing_connection_id or os.environ.get("BING_CONNECTION_ID") + if resolved_connection_id: + return BingGroundingTool(connection_id=resolved_connection_id) + + # Azure AI Agents requires Bing connection for web search + raise ValueError( + "Azure AI Agents requires a Bing connection for web search. " + "Provide bing_connection_id (or set BING_CONNECTION_ID env var) for Bing Grounding, " + "or provide both bing_custom_connection_id and bing_custom_instance_id " + "(or set BING_CUSTOM_CONNECTION_ID and BING_CUSTOM_INSTANCE_NAME env vars) for Bing Custom Search." + ) + + @staticmethod + def get_mcp_tool( + *, + name: str, + url: str | None = None, + description: str | None = None, + approval_mode: str | dict[str, list[str]] | None = None, + allowed_tools: list[str] | None = None, + headers: dict[str, str] | None = None, + ) -> McpTool: + """Create a hosted MCP tool configuration for Azure AI Agents. + + This configures an MCP (Model Context Protocol) server that will be called + by Azure AI's service. The tools from this MCP server are executed remotely + by Azure AI, not locally by your application. + + Note: + For local MCP execution where your application calls the MCP server + directly, use the MCP client tools instead of this method. + + Keyword Args: + name: A label/name for the MCP server. + url: The URL of the MCP server. + description: A description of what the MCP server provides. + approval_mode: Tool approval mode. Use "always_require" or "never_require" for all tools, + or provide a dict with "always_require_approval" and/or "never_require_approval" + keys mapping to lists of tool names. + allowed_tools: List of tool names that are allowed to be used from this MCP server. + headers: HTTP headers to include in requests to the MCP server. + + Returns: + An McpTool instance ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.azure import AzureAIAgentClient + + tool = AzureAIAgentClient.get_mcp_tool( + name="my_mcp", + url="https://mcp.example.com", + ) + agent = ChatAgent(client, tools=[tool]) + """ + mcp_tool = McpTool( + server_label=name.replace(" ", "_"), + server_url=url or "", + allowed_tools=list(allowed_tools) if allowed_tools else [], + ) + + # Set approval mode if provided + # The SDK's set_approval_mode() accepts dict at runtime even though type hints say str. + if approval_mode: + if isinstance(approval_mode, str): + if approval_mode == "never_require": + mcp_tool.set_approval_mode("never") + elif approval_mode == "always_require": + mcp_tool.set_approval_mode("always") + else: + mcp_tool.set_approval_mode(approval_mode) + elif isinstance(approval_mode, dict): + # Handle dict-based approval mode (per-tool approval settings) + if "never_require_approval" in approval_mode: + mcp_tool.set_approval_mode({"never": {"tool_names": approval_mode["never_require_approval"]}}) # type: ignore[arg-type] + elif "always_require_approval" in approval_mode: + mcp_tool.set_approval_mode({"always": {"tool_names": approval_mode["always_require_approval"]}}) # type: ignore[arg-type] + + # Set headers if provided + if headers: + for key, value in headers.items(): + mcp_tool.update_headers(key, value) + + return mcp_tool + + # endregion + def __init__( self, *, @@ -346,7 +535,7 @@ class AzureAIAgentClient( self.should_cleanup_agent = should_cleanup_agent # Track whether we should delete the agent self._agent_created = False # Track whether agent was created inside this class 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 + self._agent_definition: AzureAgent | None = None # Cached definition for existing agent async def __aenter__(self) -> Self: """Async context manager entry.""" @@ -365,7 +554,7 @@ class AzureAIAgentClient( def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], stream: bool = False, **kwargs: Any, @@ -898,7 +1087,7 @@ class AzureAIAgentClient( self.agent_id = None self._agent_created = False - async def _load_agent_definition_if_needed(self) -> Agent | None: + async def _load_agent_definition_if_needed(self) -> AzureAgent | 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.agents_client.get_agent(self.agent_id) @@ -906,7 +1095,7 @@ class AzureAIAgentClient( async def _prepare_options( self, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], **kwargs: Any, ) -> tuple[dict[str, Any], list[Content] | None]: @@ -1020,7 +1209,7 @@ class AzureAIAgentClient( async def _prepare_tool_definitions_and_resources( self, options: Mapping[str, Any], - agent_definition: Agent | None, + agent_definition: AzureAgent | None, run_options: dict[str, Any], ) -> list[ToolDefinition | dict[str, Any]]: """Prepare tool definitions and resources for the run options.""" @@ -1049,42 +1238,29 @@ class AzureAIAgentClient( return tool_definitions - def _prepare_mcp_resources(self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]]) -> list[dict[str, Any]]: - """Prepare MCP tool resources for approval mode configuration.""" - mcp_tools = [tool for tool in tools if isinstance(tool, HostedMCPTool)] - if not mcp_tools: - return [] + def _prepare_mcp_resources(self, tools: Sequence[Any]) -> list[dict[str, Any]]: + """Prepare MCP tool resources for approval mode configuration. + Extracts MCP resources from McpTool instances including server_label, + require_approval, and headers. + """ mcp_resources: list[dict[str, Any]] = [] - for mcp_tool in mcp_tools: - server_label = mcp_tool.name.replace(" ", "_") - mcp_resource: dict[str, Any] = {"server_label": server_label} - - if mcp_tool.headers: - mcp_resource["headers"] = mcp_tool.headers - - if mcp_tool.approval_mode is not None: - match mcp_tool.approval_mode: - case str(): - # Map agent framework approval modes to Azure AI approval modes - approval_mode = "always" if mcp_tool.approval_mode == "always_require" else "never" - mcp_resource["require_approval"] = approval_mode - case _: - if "always_require_approval" in mcp_tool.approval_mode: - mcp_resource["require_approval"] = { - "always": mcp_tool.approval_mode["always_require_approval"] - } - elif "never_require_approval" in mcp_tool.approval_mode: - mcp_resource["require_approval"] = { - "never": mcp_tool.approval_mode["never_require_approval"] - } - - mcp_resources.append(mcp_resource) - + for tool in tools: + if isinstance(tool, McpTool): + # Use the resources property which includes all config (approval, headers) + tool_resources = tool.resources + if tool_resources and tool_resources.mcp: + for mcp_resource in tool_resources.mcp: + resource_dict: dict[str, Any] = {"server_label": mcp_resource.server_label} + if mcp_resource.require_approval: + resource_dict["require_approval"] = mcp_resource.require_approval + if mcp_resource.headers: + resource_dict["headers"] = mcp_resource.headers + mcp_resources.append(resource_dict) return mcp_resources def _prepare_messages( - self, messages: Sequence[ChatMessage] + self, messages: Sequence[Message] ) -> tuple[ list[ThreadMessageOptions] | None, list[str], @@ -1142,79 +1318,40 @@ class AzureAIAgentClient( return additional_messages, instructions, required_action_results async def _prepare_tools_for_azure_ai( - self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]], run_options: dict[str, Any] | None = None - ) -> list[ToolDefinition | dict[str, Any]]: - """Prepare tool definitions for the Azure AI Agents API.""" - tool_definitions: list[ToolDefinition | dict[str, Any]] = [] + self, tools: Sequence[Any], run_options: dict[str, Any] | None = None + ) -> list[Any]: + """Prepare tool definitions for the Azure AI Agents API. + + Converts FunctionTool to JSON schema format. SDK Tool wrappers with .definitions + are unpacked. All other tools (ToolDefinition, dict, etc.) pass through unchanged. + + Args: + tools: Sequence of tools to prepare. + run_options: Optional run options dict that may be updated with tool_resources. + + Returns: + List of tool definitions ready for the Azure AI API. + """ + tool_definitions: list[Any] = [] for tool in tools: - match tool: - case FunctionTool(): - tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType] - case HostedWebSearchTool(): - additional_props = tool.additional_properties or {} - config_args: dict[str, Any] = {} - if count := additional_props.get("count"): - config_args["count"] = count - if freshness := additional_props.get("freshness"): - config_args["freshness"] = freshness - if market := additional_props.get("market"): - config_args["market"] = market - if set_lang := additional_props.get("set_lang"): - config_args["set_lang"] = set_lang - # Bing Grounding - connection_id = additional_props.get("connection_id") or os.getenv("BING_CONNECTION_ID") - # Custom Bing Search - custom_connection_id = additional_props.get("custom_connection_id") or os.getenv( - "BING_CUSTOM_CONNECTION_ID" - ) - 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) and not custom_connection_id and not custom_instance_name: - if connection_id: - conn_id = connection_id - else: - raise ServiceInitializationError("Parameter connection_id is not provided.") - bing_search = BingGroundingTool(connection_id=conn_id, **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' 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_CUSTOM_CONNECTION_ID', " - "'BING_CUSTOM_INSTANCE_NAME'" - ) - tool_definitions.extend(bing_search.definitions) - case HostedCodeInterpreterTool(): - tool_definitions.append(CodeInterpreterToolDefinition()) - case HostedMCPTool(): - mcp_tool = McpTool( - server_label=tool.name.replace(" ", "_"), - server_url=str(tool.url), - allowed_tools=list(tool.allowed_tools) if tool.allowed_tools else [], - ) - tool_definitions.extend(mcp_tool.definitions) - case HostedFileSearchTool(): - vector_stores = [inp for inp in tool.inputs or [] if inp.type == "hosted_vector_store"] - if vector_stores: - file_search = FileSearchTool(vector_store_ids=[vs.vector_store_id for vs in vector_stores]) # type: ignore[misc] - tool_definitions.extend(file_search.definitions) - # Set tool_resources for file search to work properly with Azure AI - if run_options is not None and "tool_resources" not in run_options: - run_options["tool_resources"] = file_search.resources - case ToolDefinition(): - tool_definitions.append(tool) - case dict(): - tool_definitions.append(tool) - case _: - raise ServiceInitializationError(f"Unsupported tool type: {type(tool)}") + if isinstance(tool, FunctionTool): + tool_definitions.append(tool.to_json_schema_spec()) + elif hasattr(tool, "definitions") and not isinstance(tool, MutableMapping): + # SDK Tool wrappers (McpTool, FileSearchTool, BingGroundingTool, etc.) + tool_definitions.extend(tool.definitions) + # Handle tool resources (MCP resources handled separately by _prepare_mcp_resources) + if ( + run_options is not None + and hasattr(tool, "resources") + and tool.resources + and "mcp" not in tool.resources + ): + if "tool_resources" not in run_options: + run_options["tool_resources"] = {} + run_options["tool_resources"].update(tool.resources) + else: + # Pass through ToolDefinition, dict, and other types unchanged + tool_definitions.append(tool) return tool_definitions def _prepare_tool_outputs_for_azure_ai( @@ -1291,20 +1428,20 @@ class AzureAIAgentClient( name: str | None = None, description: str | None = None, instructions: str | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, default_options: AzureAIAgentOptionsT | Mapping[str, Any] | None = None, chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None, context_provider: ContextProvider | None = None, middleware: Sequence[MiddlewareTypes] | None = None, **kwargs: Any, - ) -> ChatAgent[AzureAIAgentOptionsT]: - """Convert this chat client to a ChatAgent. + ) -> Agent[AzureAIAgentOptionsT]: + """Convert this chat client to a Agent. - This method creates a ChatAgent instance with this client pre-configured. + This method creates a Agent instance with this client pre-configured. It does NOT create an agent on the Azure AI service - the actual agent will be created on the server during the first invocation (run). @@ -1324,7 +1461,7 @@ class AzureAIAgentClient( kwargs: Any additional keyword arguments. Returns: - A ChatAgent instance configured with this chat client. + A Agent instance configured with this chat client. """ return super().as_agent( id=id, diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 8d262b9b13..03fb5e84a8 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -4,21 +4,20 @@ from __future__ import annotations import sys from collections.abc import Callable, Mapping, MutableMapping, Sequence -from typing import Any, ClassVar, Generic, TypedDict, TypeVar, cast +from typing import Any, ClassVar, Generic, Literal, TypedDict, TypeVar, cast from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, - ChatAgent, + Agent, ChatAndFunctionMiddlewareTypes, - ChatMessage, ChatMessageStoreProtocol, ChatMiddlewareLayer, ContextProvider, FunctionInvocationConfiguration, FunctionInvocationLayer, - HostedMCPTool, + FunctionTool, + Message, MiddlewareTypes, - ToolProtocol, get_logger, ) from agent_framework.exceptions import ServiceInitializationError @@ -26,12 +25,24 @@ from agent_framework.observability import ChatTelemetryLayer from agent_framework.openai import OpenAIResponsesOptions from agent_framework.openai._responses_client import RawOpenAIResponsesClient from azure.ai.projects.aio import AIProjectClient -from azure.ai.projects.models import MCPTool, PromptAgentDefinition, PromptAgentDefinitionText, RaiConfig, Reasoning +from azure.ai.projects.models import ( + ApproximateLocation, + CodeInterpreterTool, + CodeInterpreterToolAuto, + ImageGenTool, + MCPTool, + PromptAgentDefinition, + PromptAgentDefinitionText, + RaiConfig, + Reasoning, + WebSearchPreviewTool, +) +from azure.ai.projects.models import FileSearchTool as ProjectsFileSearchTool from azure.core.credentials_async import AsyncTokenCredential from azure.core.exceptions import ResourceNotFoundError from pydantic import ValidationError -from ._shared import AzureAISettings, _extract_project_connection_id, create_text_format_config +from ._shared import AzureAISettings, create_text_format_config if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover @@ -329,7 +340,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ if self.agent_name is None: raise ServiceInitializationError( "Agent name is required. Provide 'agent_name' when initializing AzureAIClient " - "or 'name' when initializing ChatAgent." + "or 'name' when initializing Agent." ) # If no agent_version is provided, either use latest version or create a new agent: @@ -396,7 +407,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ @override async def _prepare_options( self, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], **kwargs: Any, ) -> dict[str, Any]: @@ -489,9 +500,9 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ """Get the current conversation ID from chat options or kwargs.""" return options.get("conversation_id") or kwargs.get("conversation_id") or self.conversation_id - def _prepare_messages_for_azure_ai(self, messages: Sequence[ChatMessage]) -> tuple[list[ChatMessage], str | None]: + def _prepare_messages_for_azure_ai(self, messages: Sequence[Message]) -> tuple[list[Message], str | None]: """Prepare input from messages and convert system/developer messages to instructions.""" - result: list[ChatMessage] = [] + result: list[Message] = [] instructions_list: list[str] = [] instructions: str | None = None @@ -526,37 +537,263 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ if description and not self.agent_description: self.agent_description = description + # region Hosted Tool Factory Methods (Azure-specific overrides) + @staticmethod - def _prepare_mcp_tool(tool: HostedMCPTool) -> MCPTool: # type: ignore[override] - """Get MCP tool from HostedMCPTool.""" - mcp = MCPTool(server_label=tool.name.replace(" ", "_"), server_url=str(tool.url)) + def get_code_interpreter_tool( # type: ignore[override] + *, + file_ids: list[str] | None = None, + container: Literal["auto"] | dict[str, Any] = "auto", + **kwargs: Any, + ) -> CodeInterpreterTool: + """Create a code interpreter tool configuration for Azure AI Projects. - if tool.description: - mcp["server_description"] = tool.description + Keyword Args: + file_ids: Optional list of file IDs to make available to the code interpreter. + container: Container configuration. Use "auto" for automatic container management. + Note: Custom container settings from this parameter are not used by Azure AI Projects; + use file_ids instead. + **kwargs: Additional arguments passed to the SDK CodeInterpreterTool constructor. + + Returns: + A CodeInterpreterTool ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.azure import AzureAIClient + + tool = AzureAIClient.get_code_interpreter_tool() + agent = ChatAgent(client, tools=[tool]) + """ + # Extract file_ids from container if provided as dict and file_ids not explicitly set + if file_ids is None and isinstance(container, dict): + file_ids = container.get("file_ids") + tool_container = CodeInterpreterToolAuto(file_ids=file_ids if file_ids else None) + return CodeInterpreterTool(container=tool_container, **kwargs) + + @staticmethod + def get_file_search_tool( + *, + vector_store_ids: list[str], + max_num_results: int | None = None, + ranking_options: dict[str, Any] | None = None, + filters: dict[str, Any] | None = None, + **kwargs: Any, + ) -> ProjectsFileSearchTool: + """Create a file search tool configuration for Azure AI Projects. + + Keyword Args: + vector_store_ids: List of vector store IDs to search. + max_num_results: Maximum number of results to return (1-50). + ranking_options: Ranking options for search results. + filters: A filter to apply (ComparisonFilter or CompoundFilter). + **kwargs: Additional arguments passed to the SDK FileSearchTool constructor. + + Returns: + A FileSearchTool ready to pass to ChatAgent. + + Raises: + ValueError: If vector_store_ids is empty. + + Examples: + .. code-block:: python + + from agent_framework.azure import AzureAIClient + + tool = AzureAIClient.get_file_search_tool( + vector_store_ids=["vs_abc123"], + ) + agent = ChatAgent(client, tools=[tool]) + """ + if not vector_store_ids: + raise ValueError("File search tool requires 'vector_store_ids' to be specified.") + return ProjectsFileSearchTool( + vector_store_ids=vector_store_ids, + max_num_results=max_num_results, + ranking_options=ranking_options, # type: ignore[arg-type] + filters=filters, # type: ignore[arg-type] + **kwargs, + ) + + @staticmethod + def get_web_search_tool( # type: ignore[override] + *, + user_location: dict[str, str] | None = None, + search_context_size: Literal["low", "medium", "high"] | None = None, + **kwargs: Any, + ) -> WebSearchPreviewTool: + """Create a web search preview tool configuration for Azure AI Projects. + + Keyword Args: + user_location: Location context for search results. Dict with keys like + "city", "country", "region", "timezone". + search_context_size: Amount of context to include from search results. + One of "low", "medium", or "high". Defaults to "medium". + **kwargs: Additional arguments passed to the SDK WebSearchPreviewTool constructor. + + Returns: + A WebSearchPreviewTool ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.azure import AzureAIClient + + tool = AzureAIClient.get_web_search_tool() + agent = ChatAgent(client, tools=[tool]) + + # With location and context size + tool = AzureAIClient.get_web_search_tool( + user_location={"city": "Seattle", "country": "US"}, + search_context_size="high", + ) + """ + ws_tool = WebSearchPreviewTool(search_context_size=search_context_size, **kwargs) + + if user_location: + ws_tool.user_location = ApproximateLocation( + city=user_location.get("city"), + country=user_location.get("country"), + region=user_location.get("region"), + timezone=user_location.get("timezone"), + ) + + return ws_tool + + @staticmethod + def get_image_generation_tool( # type: ignore[override] + *, + model: Literal["gpt-image-1"] | str | None = None, + size: Literal["1024x1024", "1024x1536", "1536x1024", "auto"] | None = None, + output_format: Literal["png", "webp", "jpeg"] | None = None, + quality: Literal["low", "medium", "high", "auto"] | None = None, + background: Literal["transparent", "opaque", "auto"] | None = None, + partial_images: int | None = None, + moderation: Literal["auto", "low"] | None = None, + output_compression: int | None = None, + **kwargs: Any, + ) -> ImageGenTool: + """Create an image generation tool configuration for Azure AI Projects. + + Keyword Args: + model: The model to use for image generation. + size: Output image size. + output_format: Output image format. + quality: Output image quality. + background: Background transparency setting. + partial_images: Number of partial images to return during generation. + moderation: Moderation level. + output_compression: Compression level. + **kwargs: Additional arguments passed to the SDK ImageGenTool constructor. + + Returns: + An ImageGenTool ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.azure import AzureAIClient + + tool = AzureAIClient.get_image_generation_tool() + agent = ChatAgent(client, tools=[tool]) + """ + return ImageGenTool( # type: ignore[misc] + model=model, # type: ignore[arg-type] + size=size, + output_format=output_format, + quality=quality, + background=background, + partial_images=partial_images, + moderation=moderation, + output_compression=output_compression, + **kwargs, + ) + + @staticmethod + def get_mcp_tool( + *, + name: str, + url: str | None = None, + description: str | None = None, + approval_mode: Literal["always_require", "never_require"] | dict[str, list[str]] | None = None, + allowed_tools: list[str] | None = None, + headers: dict[str, str] | None = None, + project_connection_id: str | None = None, + **kwargs: Any, + ) -> MCPTool: + """Create a hosted MCP tool configuration for Azure AI. + + This configures an MCP (Model Context Protocol) server that will be called + by Azure AI's service. The tools from this MCP server are executed remotely + by Azure AI, not locally by your application. + + Note: + For local MCP execution where your application calls the MCP server + directly, use the MCP client tools instead of this method. + + Keyword Args: + name: A label/name for the MCP server. + url: The URL of the MCP server. Required if project_connection_id is not provided. + description: A description of what the MCP server provides. + approval_mode: Tool approval mode. Use "always_require" or "never_require" for all tools, + or provide a dict with "always_require_approval" and/or "never_require_approval" + keys mapping to lists of tool names. + allowed_tools: List of tool names that are allowed to be used from this MCP server. + headers: HTTP headers to include in requests to the MCP server. + project_connection_id: Azure AI Foundry connection ID for managed MCP connections. + If provided, url and headers are not required. + **kwargs: Additional arguments passed to the SDK MCPTool constructor. + + Returns: + An MCPTool configuration ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.azure import AzureAIClient + + # With URL + tool = AzureAIClient.get_mcp_tool( + name="my_mcp", + url="https://mcp.example.com", + ) + + # With Azure AI Foundry connection + tool = AzureAIClient.get_mcp_tool( + name="github_mcp", + project_connection_id="conn_abc123", + description="GitHub MCP via Azure AI Foundry", + ) + + agent = ChatAgent(client, tools=[tool]) + """ + mcp = MCPTool(server_label=name.replace(" ", "_"), server_url=url or "", **kwargs) + + if description: + mcp["server_description"] = description - # Check for project_connection_id in additional_properties (for Azure AI Foundry connections) - project_connection_id = _extract_project_connection_id(tool.additional_properties) if project_connection_id: mcp["project_connection_id"] = project_connection_id - elif tool.headers: - # Only use headers if no project_connection_id is available - mcp["headers"] = tool.headers + elif headers: + mcp["headers"] = headers - if tool.allowed_tools: - mcp["allowed_tools"] = list(tool.allowed_tools) + if allowed_tools: + mcp["allowed_tools"] = allowed_tools - if tool.approval_mode: - match tool.approval_mode: - case str(): - mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never" - case _: - if always_require_approvals := tool.approval_mode.get("always_require_approval"): - mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}} - if never_require_approvals := tool.approval_mode.get("never_require_approval"): - mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}} + if approval_mode: + if isinstance(approval_mode, str): + mcp["require_approval"] = "always" if approval_mode == "always_require" else "never" + else: + if always_require := approval_mode.get("always_require_approval"): + mcp["require_approval"] = {"always": {"tool_names": always_require}} + if never_require := approval_mode.get("never_require_approval"): + mcp["require_approval"] = {"never": {"tool_names": never_require}} return mcp + # endregion + @override def as_agent( self, @@ -565,20 +802,20 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ name: str | None = None, description: str | None = None, instructions: str | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, default_options: AzureAIClientOptionsT | Mapping[str, Any] | None = None, chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None, context_provider: ContextProvider | None = None, middleware: Sequence[MiddlewareTypes] | None = None, **kwargs: Any, - ) -> ChatAgent[AzureAIClientOptionsT]: - """Convert this chat client to a ChatAgent. + ) -> Agent[AzureAIClientOptionsT]: + """Convert this chat client to a Agent. - This method creates a ChatAgent instance with this client pre-configured. + This method creates a Agent instance with this client pre-configured. It does NOT create an agent on the Azure AI service - the actual agent will be created on the server during the first invocation (run). @@ -598,7 +835,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ kwargs: Any additional keyword arguments. Returns: - A ChatAgent instance configured with this chat client. + A Agent instance configured with this chat client. """ return super().as_agent( id=id, diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py index e486a14560..0a6b571e40 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py @@ -8,11 +8,10 @@ from typing import Any, Generic from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, - ChatAgent, + Agent, ContextProvider, FunctionTool, MiddlewareTypes, - ToolProtocol, get_logger, normalize_tools, ) @@ -47,7 +46,7 @@ else: logger = get_logger("agent_framework.azure") -# Type variable for options - allows typed ChatAgent[OptionsT] returns +# Type variable for options - allows typed Agent[OptionsT] returns # Default matches AzureAIClient's default options type OptionsCoT = TypeVar( "OptionsCoT", @@ -162,16 +161,16 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): model: str | None = None, instructions: str | None = None, description: str | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Create a new agent on the Azure AI service and return a local ChatAgent wrapper. + ) -> Agent[OptionsCoT]: + """Create a new agent on the Azure AI service and return a local Agent wrapper. Args: name: The name of the agent to create. @@ -186,7 +185,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): context_provider: Context provider to include during agent invocation. Returns: - ChatAgent: A ChatAgent instance configured with the created agent. + Agent: A Agent instance configured with the created agent. Raises: ServiceInitializationError: If required parameters are missing. @@ -221,7 +220,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): # Normalize tools and separate MCP tools from other tools normalized_tools = normalize_tools(tools) mcp_tools: list[MCPTool] = [] - non_mcp_tools: list[ToolProtocol | MutableMapping[str, Any]] = [] + non_mcp_tools: list[FunctionTool | MutableMapping[str, Any]] = [] if normalized_tools: for tool in normalized_tools: @@ -239,7 +238,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): mcp_discovered_functions.extend(mcp_tool.functions) # Combine non-MCP tools with discovered MCP functions for Azure AI - all_tools_for_azure: list[ToolProtocol | MutableMapping[str, Any]] = list(non_mcp_tools) + all_tools_for_azure: list[FunctionTool | MutableMapping[str, Any]] = list(non_mcp_tools) all_tools_for_azure.extend(mcp_discovered_functions) if all_tools_for_azure: @@ -264,16 +263,16 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): *, name: str | None = None, reference: AgentReference | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Retrieve an existing agent from the Azure AI service and return a local ChatAgent wrapper. + ) -> Agent[OptionsCoT]: + """Retrieve an existing agent from the Azure AI service and return a local Agent wrapper. You must provide either name or reference. Use `as_agent()` if you already have AgentVersionDetails and want to avoid an async call. @@ -288,7 +287,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): context_provider: Context provider to include during agent invocation. Returns: - ChatAgent: A ChatAgent instance configured with the retrieved agent. + Agent: A Agent instance configured with the retrieved agent. Raises: ValueError: If no identifier is provided or required tools are missing. @@ -308,7 +307,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): raise ValueError("Either name or reference must be provided to get an agent.") if not isinstance(existing_agent.definition, PromptAgentDefinition): - raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") + raise ValueError("Agent definition must be PromptAgentDefinition to get a Agent.") # Validate that required function tools are provided self._validate_function_tools(existing_agent.definition.tools, tools) @@ -324,16 +323,16 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): def as_agent( self, details: AgentVersionDetails, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Wrap an SDK agent version object into a ChatAgent without making HTTP calls. + ) -> Agent[OptionsCoT]: + """Wrap an SDK agent version object into a Agent without making HTTP calls. Use this when you already have an AgentVersionDetails from a previous API call. @@ -346,13 +345,13 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): context_provider: Context provider to include during agent invocation. Returns: - ChatAgent: A ChatAgent instance configured with the agent version. + Agent: A Agent instance configured with the agent version. Raises: ValueError: If the agent definition is not a PromptAgentDefinition or required tools are missing. """ if not isinstance(details.definition, PromptAgentDefinition): - raise ValueError("Agent definition must be PromptAgentDefinition to create a ChatAgent.") + raise ValueError("Agent definition must be PromptAgentDefinition to create a Agent.") # Validate that required function tools are provided self._validate_function_tools(details.definition.tools, tools) @@ -368,12 +367,12 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): def _to_chat_agent_from_details( self, details: AgentVersionDetails, - provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None = None, + provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None = None, default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Create a ChatAgent from an AgentVersionDetails. + ) -> Agent[OptionsCoT]: + """Create a Agent from an AgentVersionDetails. Args: details: The AgentVersionDetails containing the agent definition. @@ -385,7 +384,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): context_provider: Context provider to include during agent invocation. """ if not isinstance(details.definition, PromptAgentDefinition): - raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") + raise ValueError("Agent definition must be PromptAgentDefinition to get a Agent.") client = AzureAIClient( project_client=self._project_client, @@ -400,8 +399,8 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): # but function tools need the actual implementations from provided_tools merged_tools = self._merge_tools(details.definition.tools, provided_tools) - return ChatAgent( # type: ignore[return-value] - chat_client=client, + return Agent( # type: ignore[return-value] + client=client, id=details.id, name=details.name, description=details.description, @@ -416,8 +415,8 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): def _merge_tools( self, definition_tools: Sequence[Any] | None, - provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None, - ) -> list[ToolProtocol | dict[str, Any]]: + provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None, + ) -> list[FunctionTool | dict[str, Any]]: """Merge hosted tools from definition with user-provided function tools. Args: @@ -425,9 +424,9 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): provided_tools: User-provided tools (Agent Framework format), including function implementations. Returns: - Combined list of tools for the ChatAgent. + Combined list of tools for the Agent. """ - merged: list[ToolProtocol | dict[str, Any]] = [] + merged: list[FunctionTool | dict[str, Any]] = [] # Convert hosted tools from definition (MCP, code interpreter, file search, web search) # Function tools from the definition are skipped - we use user-provided implementations instead @@ -442,7 +441,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): if provided_tools: for provided_tool in provided_tools: # FunctionTool - has implementation for function calling - # MCPTool - ChatAgent handles MCP connection and tool discovery at runtime + # MCPTool - Agent handles MCP connection and tool discovery at runtime if isinstance(provided_tool, (FunctionTool, MCPTool)): merged.append(provided_tool) # type: ignore[reportUnknownArgumentType] @@ -451,10 +450,10 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): def _validate_function_tools( self, agent_tools: Sequence[Any] | None, - provided_tools: ToolProtocol + provided_tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None, ) -> None: """Validate that required function tools are provided.""" diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py index 065a7d5af2..585fc9a9f5 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py @@ -2,37 +2,21 @@ from __future__ import annotations -import os from collections.abc import Mapping, MutableMapping, Sequence -from typing import Any, ClassVar, Literal, cast +from typing import Any, ClassVar, cast from agent_framework import ( - Content, FunctionTool, - HostedCodeInterpreterTool, - HostedFileSearchTool, - HostedImageGenerationTool, - HostedMCPTool, - HostedWebSearchTool, - ToolProtocol, get_logger, ) from agent_framework._pydantic import AFBaseSettings -from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError +from agent_framework.exceptions import ServiceInvalidRequestError from azure.ai.agents.models import ( - BingCustomSearchTool, - BingGroundingTool, CodeInterpreterToolDefinition, - McpTool, ToolDefinition, ) -from azure.ai.agents.models import FileSearchTool as AgentsFileSearchTool from azure.ai.projects.models import ( - ApproximateLocation, CodeInterpreterTool, - CodeInterpreterToolAuto, - ImageGenTool, - ImageGenToolInputImageMask, MCPTool, ResponseTextFormatConfigurationJsonObject, ResponseTextFormatConfigurationJsonSchema, @@ -93,13 +77,13 @@ class AzureAISettings(AFBaseSettings): def _extract_project_connection_id(additional_properties: dict[str, Any] | None) -> str | None: - """Extract project_connection_id from HostedMCPTool additional_properties. + """Extract project_connection_id from tool additional_properties. Checks for both direct 'project_connection_id' key (programmatic usage) and 'connection.name' structure (declarative/YAML usage). Args: - additional_properties: The additional_properties dict from a HostedMCPTool. + additional_properties: The additional_properties dict from a tool. Returns: The project_connection_id if found, None otherwise. @@ -124,11 +108,13 @@ def _extract_project_connection_id(additional_properties: dict[str, Any] | None) def to_azure_ai_agent_tools( - tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None, + tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None, run_options: dict[str, Any] | None = None, ) -> list[ToolDefinition | dict[str, Any]]: """Convert Agent Framework tools to Azure AI V1 SDK tool definitions. + Handles FunctionTool instances and dict-based tools from static factory methods. + Args: tools: Sequence of Agent Framework tools to convert. run_options: Optional dict with run options. @@ -144,91 +130,53 @@ def to_azure_ai_agent_tools( tool_definitions: list[ToolDefinition | dict[str, Any]] = [] for tool in tools: - match tool: - case FunctionTool(): - tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType] - case HostedWebSearchTool(): - additional_props = tool.additional_properties or {} - config_args: dict[str, Any] = {} - if count := additional_props.get("count"): - config_args["count"] = count - if freshness := additional_props.get("freshness"): - config_args["freshness"] = freshness - if market := additional_props.get("market"): - config_args["market"] = market - if set_lang := additional_props.get("set_lang"): - config_args["set_lang"] = set_lang - # Bing Grounding - connection_id = additional_props.get("connection_id") or os.getenv("BING_CONNECTION_ID") - # Custom Bing Search - custom_connection_id = additional_props.get("custom_connection_id") or os.getenv( - "BING_CUSTOM_CONNECTION_ID" - ) - 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 and not custom_connection_id and not custom_instance_name: - bing_search = BingGroundingTool(connection_id=connection_id, **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' 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_CUSTOM_CONNECTION_ID', 'BING_CUSTOM_INSTANCE_NAME'" - ) - tool_definitions.extend(bing_search.definitions) - case HostedCodeInterpreterTool(): - tool_definitions.append(CodeInterpreterToolDefinition()) - case HostedMCPTool(): - mcp_tool = McpTool( - server_label=tool.name.replace(" ", "_"), - server_url=str(tool.url), - allowed_tools=list(tool.allowed_tools) if tool.allowed_tools else [], - ) - tool_definitions.extend(mcp_tool.definitions) - case HostedFileSearchTool(): - vector_stores = [inp for inp in tool.inputs or [] if inp.type == "hosted_vector_store"] - if vector_stores: - file_search = AgentsFileSearchTool(vector_store_ids=[vs.vector_store_id for vs in vector_stores]) # type: ignore[misc] - tool_definitions.extend(file_search.definitions) - # Set tool_resources for file search to work properly with Azure AI - if run_options is not None and "tool_resources" not in run_options: - run_options["tool_resources"] = file_search.resources - case ToolDefinition(): - tool_definitions.append(tool) - case dict(): - tool_definitions.append(tool) - case _: - raise ServiceInitializationError(f"Unsupported tool type: {type(tool)}") + if isinstance(tool, FunctionTool): + tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType] + elif isinstance(tool, ToolDefinition): + # Pass through ToolDefinition subclasses unchanged (includes CodeInterpreterToolDefinition, etc.) + tool_definitions.append(tool) + elif hasattr(tool, "definitions") and not isinstance(tool, (dict, MutableMapping)): + # SDK Tool wrappers (McpTool, FileSearchTool, BingGroundingTool, etc.) + tool_definitions.extend(tool.definitions) + # Handle tool resources (MCP resources handled separately) + if ( + run_options is not None + and hasattr(tool, "resources") + and tool.resources + and "mcp" not in tool.resources + ): + if "tool_resources" not in run_options: + run_options["tool_resources"] = {} + run_options["tool_resources"].update(tool.resources) + elif isinstance(tool, (dict, MutableMapping)): + # Handle dict-based tools - pass through directly + tool_dict = tool if isinstance(tool, dict) else dict(tool) + tool_definitions.append(tool_dict) + else: + # Pass through other types unchanged + tool_definitions.append(tool) return tool_definitions def from_azure_ai_agent_tools( tools: Sequence[ToolDefinition | dict[str, Any]] | None, -) -> list[ToolProtocol | dict[str, Any]]: - """Convert Azure AI V1 SDK tool definitions to Agent Framework tools. +) -> list[dict[str, Any]]: + """Convert Azure AI V1 SDK tool definitions to dict-based tools. Args: tools: Sequence of Azure AI V1 SDK tool definitions. Returns: - List of Agent Framework tools. + List of dict-based tool definitions. """ if not tools: return [] - result: list[ToolProtocol | dict[str, Any]] = [] + result: list[dict[str, Any]] = [] for tool in tools: # Handle SDK objects if isinstance(tool, CodeInterpreterToolDefinition): - result.append(HostedCodeInterpreterTool()) + result.append({"type": "code_interpreter"}) elif isinstance(tool, dict): # Handle dict format converted = _convert_dict_tool(tool) @@ -242,35 +190,38 @@ def from_azure_ai_agent_tools( return result -def _convert_dict_tool(tool: dict[str, Any]) -> ToolProtocol | dict[str, Any] | None: - """Convert a dict-format Azure AI tool to Agent Framework tool.""" +def _convert_dict_tool(tool: dict[str, Any]) -> dict[str, Any] | None: + """Convert a dict-format Azure AI tool to dict-based tool format.""" tool_type = tool.get("type") if tool_type == "code_interpreter": - return HostedCodeInterpreterTool() + return {"type": "code_interpreter"} if tool_type == "file_search": file_search_config = tool.get("file_search", {}) vector_store_ids = file_search_config.get("vector_store_ids", []) - inputs = [Content.from_hosted_vector_store(vector_store_id=vs_id) for vs_id in vector_store_ids] - return HostedFileSearchTool(inputs=inputs if inputs else None) # type: ignore + return {"type": "file_search", "vector_store_ids": vector_store_ids} if tool_type == "bing_grounding": bing_config = tool.get("bing_grounding", {}) connection_id = bing_config.get("connection_id") - return HostedWebSearchTool(additional_properties={"connection_id": connection_id} if connection_id else None) + return {"type": "bing_grounding", "connection_id": connection_id} if connection_id else None if tool_type == "bing_custom_search": bing_config = tool.get("bing_custom_search", {}) - return HostedWebSearchTool( - additional_properties={ - "custom_connection_id": bing_config.get("connection_id"), - "custom_instance_name": bing_config.get("instance_name"), + connection_id = bing_config.get("connection_id") + instance_name = bing_config.get("instance_name") + # Only return if both required fields are present + if connection_id and instance_name: + return { + "type": "bing_custom_search", + "connection_id": connection_id, + "instance_name": instance_name, } - ) + return None if tool_type == "mcp": - # Hosted MCP tools are defined on the Azure agent, no local handling needed + # MCP tools are defined on the Azure agent, no local handling needed # Azure may not return full server_url, so skip conversion return None @@ -282,35 +233,38 @@ def _convert_dict_tool(tool: dict[str, Any]) -> ToolProtocol | dict[str, Any] | return tool -def _convert_sdk_tool(tool: ToolDefinition) -> ToolProtocol | dict[str, Any] | None: - """Convert an SDK-object Azure AI tool to Agent Framework tool.""" +def _convert_sdk_tool(tool: ToolDefinition) -> dict[str, Any] | None: + """Convert an SDK-object Azure AI tool to dict-based tool format.""" tool_type = getattr(tool, "type", None) if tool_type == "code_interpreter": - return HostedCodeInterpreterTool() + return {"type": "code_interpreter"} if tool_type == "file_search": file_search_config = getattr(tool, "file_search", None) vector_store_ids = getattr(file_search_config, "vector_store_ids", []) if file_search_config else [] - inputs = [Content.from_hosted_vector_store(vector_store_id=vs_id) for vs_id in vector_store_ids] - return HostedFileSearchTool(inputs=inputs if inputs else None) # type: ignore + return {"type": "file_search", "vector_store_ids": vector_store_ids} if tool_type == "bing_grounding": bing_config = getattr(tool, "bing_grounding", None) connection_id = getattr(bing_config, "connection_id", None) if bing_config else None - return HostedWebSearchTool(additional_properties={"connection_id": connection_id} if connection_id else None) + return {"type": "bing_grounding", "connection_id": connection_id} if connection_id else None if tool_type == "bing_custom_search": bing_config = getattr(tool, "bing_custom_search", None) - return HostedWebSearchTool( - additional_properties={ - "custom_connection_id": getattr(bing_config, "connection_id", None) if bing_config else None, - "custom_instance_name": getattr(bing_config, "instance_name", None) if bing_config else None, + connection_id = getattr(bing_config, "connection_id", None) if bing_config else None + instance_name = getattr(bing_config, "instance_name", None) if bing_config else None + # Only return if both required fields are present + if connection_id and instance_name: + return { + "type": "bing_custom_search", + "connection_id": connection_id, + "instance_name": instance_name, } - ) + return None if tool_type == "mcp": - # Hosted MCP tools are defined on the Azure agent, no local handling needed + # MCP tools are defined on the Azure agent, no local handling needed # Azure may not return full server_url, so skip conversion return None @@ -324,18 +278,17 @@ def _convert_sdk_tool(tool: ToolDefinition) -> ToolProtocol | dict[str, Any] | N return {"type": tool_type} if tool_type else {} -def from_azure_ai_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[ToolProtocol | dict[str, Any]]: - """Parses and converts a sequence of Azure AI tools into Agent Framework compatible tools. +def from_azure_ai_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[dict[str, Any]]: + """Parses and converts a sequence of Azure AI tools into dict-based tools. Args: tools: A sequence of tool objects or dictionaries defining the tools to be parsed. Can be None. Returns: - list[ToolProtocol | dict[str, Any]]: A list of converted tools compatible with the - Agent Framework. + list[dict[str, Any]]: A list of dict-based tool definitions. """ - agent_tools: list[ToolProtocol | dict[str, Any]] = [] + agent_tools: list[dict[str, Any]] = [] if not tools: return agent_tools for tool in tools: @@ -345,81 +298,62 @@ def from_azure_ai_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[T if tool_type == "mcp": mcp_tool = cast(MCPTool, tool_dict) - approval_mode: Literal["always_require", "never_require"] | dict[str, set[str]] | None = None + result: dict[str, Any] = { + "type": "mcp", + "server_label": mcp_tool.get("server_label", ""), + "server_url": mcp_tool.get("server_url", ""), + } + if description := mcp_tool.get("server_description"): + result["server_description"] = description + if headers := mcp_tool.get("headers"): + result["headers"] = headers + if allowed_tools := mcp_tool.get("allowed_tools"): + result["allowed_tools"] = allowed_tools if require_approval := mcp_tool.get("require_approval"): - if require_approval == "always": - approval_mode = "always_require" - elif require_approval == "never": - approval_mode = "never_require" - elif isinstance(require_approval, dict): - approval_mode = {} - if "always" in require_approval: - approval_mode["always_require_approval"] = set(require_approval["always"].get("tool_names", [])) # type: ignore - if "never" in require_approval: - approval_mode["never_require_approval"] = set(require_approval["never"].get("tool_names", [])) # type: ignore - - # Preserve project_connection_id in additional_properties - additional_props: dict[str, Any] | None = None + result["require_approval"] = require_approval if project_connection_id := mcp_tool.get("project_connection_id"): - additional_props = {"connection": {"name": project_connection_id}} - - agent_tools.append( - HostedMCPTool( - name=mcp_tool.get("server_label", "").replace("_", " "), - url=mcp_tool.get("server_url", ""), - description=mcp_tool.get("server_description"), - headers=mcp_tool.get("headers"), - allowed_tools=mcp_tool.get("allowed_tools"), - approval_mode=approval_mode, # type: ignore - additional_properties=additional_props, - ) - ) + result["project_connection_id"] = project_connection_id + agent_tools.append(result) elif tool_type == "code_interpreter": ci_tool = cast(CodeInterpreterTool, tool_dict) container = ci_tool.get("container", {}) - ci_inputs: list[Content] = [] + result = {"type": "code_interpreter"} if "file_ids" in container: - for file_id in container["file_ids"]: - ci_inputs.append(Content.from_hosted_file(file_id=file_id)) - - agent_tools.append(HostedCodeInterpreterTool(inputs=ci_inputs if ci_inputs else None)) # type: ignore + result["file_ids"] = container["file_ids"] + agent_tools.append(result) elif tool_type == "file_search": fs_tool = cast(ProjectsFileSearchTool, tool_dict) - fs_inputs: list[Content] = [] + result = {"type": "file_search"} if "vector_store_ids" in fs_tool: - for vs_id in fs_tool["vector_store_ids"]: - fs_inputs.append(Content.from_hosted_vector_store(vector_store_id=vs_id)) - - agent_tools.append( - HostedFileSearchTool( - inputs=fs_inputs if fs_inputs else None, # type: ignore - max_results=fs_tool.get("max_num_results"), - ) - ) + result["vector_store_ids"] = fs_tool["vector_store_ids"] + if max_results := fs_tool.get("max_num_results"): + result["max_num_results"] = max_results + agent_tools.append(result) elif tool_type == "web_search_preview": ws_tool = cast(WebSearchPreviewTool, tool_dict) - additional_properties: dict[str, Any] = {} + result = {"type": "web_search_preview"} if user_location := ws_tool.get("user_location"): - additional_properties["user_location"] = { + result["user_location"] = { "city": user_location.get("city"), "country": user_location.get("country"), "region": user_location.get("region"), "timezone": user_location.get("timezone"), } - - agent_tools.append(HostedWebSearchTool(additional_properties=additional_properties)) + agent_tools.append(result) else: agent_tools.append(tool_dict) return agent_tools def to_azure_ai_tools( - tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None, + tools: Sequence[FunctionTool | MutableMapping[str, Any] | Tool] | None, ) -> list[Tool | dict[str, Any]]: """Converts Agent Framework tools into Azure AI compatible tools. + Handles FunctionTool instances and passes through SDK Tool types directly. + Args: - tools: A sequence of Agent Framework tool objects or dictionaries + tools: A sequence of Agent Framework tool objects, SDK Tool types, or dictionaries defining the tools to be converted. Can be None. Returns: @@ -430,133 +364,54 @@ def to_azure_ai_tools( return azure_tools for tool in tools: - if isinstance(tool, ToolProtocol): - match tool: - case HostedMCPTool(): - azure_tools.append(_prepare_mcp_tool_for_azure_ai(tool)) - case HostedCodeInterpreterTool(): - file_ids: list[str] = [] - if tool.inputs: - for tool_input in tool.inputs: - if tool_input.type == "hosted_file": - file_ids.append(tool_input.file_id) # type: ignore[misc, arg-type] - container = CodeInterpreterToolAuto(file_ids=file_ids if file_ids else None) - ci_tool: CodeInterpreterTool = CodeInterpreterTool(container=container) - azure_tools.append(ci_tool) - case FunctionTool(): - params = tool.parameters() - params["additionalProperties"] = False - azure_tools.append( - AzureFunctionTool( - name=tool.name, - parameters=params, - strict=False, - description=tool.description, - ) - ) - case HostedFileSearchTool(): - if not tool.inputs: - raise ValueError("HostedFileSearchTool requires inputs to be specified.") - vector_store_ids: list[str] = [ - inp.vector_store_id # type: ignore[misc] - for inp in tool.inputs - if inp.type == "hosted_vector_store" - ] - if not vector_store_ids: - raise ValueError( - "HostedFileSearchTool requires inputs to be of type `Content` with " - "type 'hosted_vector_store'." - ) - fs_tool: ProjectsFileSearchTool = ProjectsFileSearchTool(vector_store_ids=vector_store_ids) - if tool.max_results: - fs_tool["max_num_results"] = tool.max_results - azure_tools.append(fs_tool) - case HostedWebSearchTool(): - ws_tool: WebSearchPreviewTool = WebSearchPreviewTool() - if tool.additional_properties: - location: dict[str, str] | None = ( - tool.additional_properties.get("user_location", None) - if tool.additional_properties - else None - ) - if location: - ws_tool.user_location = ApproximateLocation( - city=location.get("city"), - country=location.get("country"), - region=location.get("region"), - timezone=location.get("timezone"), - ) - azure_tools.append(ws_tool) - case HostedImageGenerationTool(): - opts = tool.options or {} - addl = tool.additional_properties or {} - # Azure ImageGenTool requires the constant model "gpt-image-1" - ig_tool: ImageGenTool = ImageGenTool( - model=opts.get("model_id", "gpt-image-1"), # type: ignore - size=cast( - Literal["1024x1024", "1024x1536", "1536x1024", "auto"] | None, opts.get("image_size") - ), - output_format=cast(Literal["png", "webp", "jpeg"] | None, opts.get("media_type")), - input_image_mask=( - ImageGenToolInputImageMask( - image_url=addl.get("input_image_mask", {}).get("image_url"), - file_id=addl.get("input_image_mask", {}).get("file_id"), - ) - if isinstance(addl.get("input_image_mask"), dict) - else None - ), - quality=cast(Literal["low", "medium", "high", "auto"] | None, addl.get("quality")), - background=cast(Literal["transparent", "opaque", "auto"] | None, addl.get("background")), - output_compression=cast(int | None, addl.get("output_compression")), - moderation=cast(Literal["auto", "low"] | None, addl.get("moderation")), - partial_images=opts.get("streaming_count"), - ) - azure_tools.append(ig_tool) - case _: - logger.debug("Unsupported tool passed (type: %s)", type(tool)) + if isinstance(tool, FunctionTool): + params = tool.parameters() + params["additionalProperties"] = False + azure_tools.append( + AzureFunctionTool( + name=tool.name, + parameters=params, + strict=False, + description=tool.description, + ) + ) + elif isinstance(tool, Tool): + # Pass through SDK Tool types directly (CodeInterpreterTool, FileSearchTool, etc.) + azure_tools.append(tool) else: - # Handle raw dictionary tools - tool_dict = tool if isinstance(tool, dict) else dict(tool) - azure_tools.append(tool_dict) + # Pass through dict-based tools directly + azure_tools.append(dict(tool) if isinstance(tool, MutableMapping) else tool) # type: ignore[arg-type] return azure_tools -def _prepare_mcp_tool_for_azure_ai(tool: HostedMCPTool) -> MCPTool: - """Convert HostedMCPTool to Azure AI MCPTool format. +def _prepare_mcp_tool_dict_for_azure_ai(tool_dict: dict[str, Any]) -> MCPTool: + """Convert dict-based MCP tool to Azure AI MCPTool format. Args: - tool: The HostedMCPTool to convert. + tool_dict: The dict-based MCP tool configuration. Returns: MCPTool: The converted Azure AI MCPTool. """ - mcp: MCPTool = MCPTool(server_label=tool.name.replace(" ", "_"), server_url=str(tool.url)) + server_label = tool_dict.get("server_label", "") + server_url = tool_dict.get("server_url", "") + mcp: MCPTool = MCPTool(server_label=server_label, server_url=server_url) - if tool.description: - mcp["server_description"] = tool.description + if description := tool_dict.get("server_description"): + mcp["server_description"] = description - # Check for project_connection_id in additional_properties (for Azure AI Foundry connections) - project_connection_id = _extract_project_connection_id(tool.additional_properties) - if project_connection_id: + # Check for project_connection_id + if project_connection_id := tool_dict.get("project_connection_id"): mcp["project_connection_id"] = project_connection_id - elif tool.headers: - # Only use headers if no project_connection_id is available - # Note: Azure AI Agent Service may reject headers with sensitive info - mcp["headers"] = tool.headers + elif headers := tool_dict.get("headers"): + mcp["headers"] = headers - if tool.allowed_tools: - mcp["allowed_tools"] = list(tool.allowed_tools) + if allowed_tools := tool_dict.get("allowed_tools"): + mcp["allowed_tools"] = list(allowed_tools) - if tool.approval_mode: - match tool.approval_mode: - case str(): - mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never" - case _: - if always_require_approvals := tool.approval_mode.get("always_require_approval"): - mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}} - if never_require_approvals := tool.approval_mode.get("never_require_approval"): - mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}} + if require_approval := tool_dict.get("require_approval"): + mcp["require_approval"] = require_approval return mcp diff --git a/python/packages/azure-ai/pyproject.toml b/python/packages/azure-ai/pyproject.toml index 4efe8ed0b7..86e0d3342f 100644 --- a/python/packages/azure-ai/pyproject.toml +++ b/python/packages/azure-ai/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "azure-ai-projects >= 2.0.0b3", "azure-ai-agents == 1.2.0b5", "aiohttp", diff --git a/python/packages/azure-ai/tests/test_agent_provider.py b/python/packages/azure-ai/tests/test_agent_provider.py index c4bcf0e953..30ef6fcf1c 100644 --- a/python/packages/azure-ai/tests/test_agent_provider.py +++ b/python/packages/azure-ai/tests/test_agent_provider.py @@ -6,23 +6,21 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from agent_framework import ( - ChatAgent, - Content, - HostedCodeInterpreterTool, - HostedFileSearchTool, - HostedMCPTool, - HostedWebSearchTool, + Agent, tool, ) from agent_framework.exceptions import ServiceInitializationError from azure.ai.agents.models import ( - Agent, + Agent as AzureAgent, +) +from azure.ai.agents.models import ( CodeInterpreterToolDefinition, ) from azure.identity.aio import AzureCliCredential from pydantic import BaseModel from agent_framework_azure_ai import ( + AzureAIAgentClient, AzureAIAgentsProvider, AzureAISettings, ) @@ -156,7 +154,7 @@ async def test_create_agent_basic( mock_agents_client: MagicMock, ) -> None: """Test creating a basic agent.""" - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "test-agent-id" mock_agent.name = "TestAgent" mock_agent.description = "A test agent" @@ -175,7 +173,7 @@ async def test_create_agent_basic( description="A test agent", ) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.name == "TestAgent" assert agent.id == "test-agent-id" mock_agents_client.create_agent.assert_called_once() @@ -186,7 +184,7 @@ async def test_create_agent_with_model( mock_agents_client: MagicMock, ) -> None: """Test creating an agent with explicit model.""" - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "test-agent-id" mock_agent.name = "TestAgent" mock_agent.description = None @@ -210,7 +208,7 @@ async def test_create_agent_with_tools( mock_agents_client: MagicMock, ) -> None: """Test creating an agent with tools.""" - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "test-agent-id" mock_agent.name = "TestAgent" mock_agent.description = None @@ -245,7 +243,7 @@ async def test_create_agent_with_response_format( temperature: float description: str - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "test-agent-id" mock_agent.name = "TestAgent" mock_agent.description = None @@ -297,7 +295,7 @@ async def test_get_agent_by_id( mock_agents_client: MagicMock, ) -> None: """Test getting an agent by ID.""" - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "existing-agent-id" mock_agent.name = "ExistingAgent" mock_agent.description = "An existing agent" @@ -312,7 +310,7 @@ async def test_get_agent_by_id( agent = await provider.get_agent("existing-agent-id") - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.id == "existing-agent-id" mock_agents_client.get_agent.assert_called_once_with("existing-agent-id") @@ -327,7 +325,7 @@ async def test_get_agent_with_function_tools( mock_function_tool.function = MagicMock() mock_function_tool.function.name = "get_weather" - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "agent-with-tools" mock_agent.name = "AgentWithTools" mock_agent.description = None @@ -356,7 +354,7 @@ async def test_get_agent_with_provided_function_tools( mock_function_tool.function = MagicMock() mock_function_tool.function.name = "get_weather" - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "agent-with-tools" mock_agent.name = "AgentWithTools" mock_agent.description = None @@ -376,7 +374,7 @@ async def test_get_agent_with_provided_function_tools( agent = await provider.get_agent("agent-with-tools", tools=get_weather) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.id == "agent-with-tools" @@ -391,7 +389,7 @@ def test_as_agent_wraps_without_http( mock_agents_client: MagicMock, ) -> None: """Test as_agent wraps Agent object without making HTTP calls.""" - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "wrap-agent-id" mock_agent.name = "WrapAgent" mock_agent.description = "Wrapped agent" @@ -405,7 +403,7 @@ def test_as_agent_wraps_without_http( agent = provider.as_agent(mock_agent) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.id == "wrap-agent-id" assert agent.name == "WrapAgent" # Ensure no HTTP calls were made @@ -423,7 +421,7 @@ def test_as_agent_with_function_tools_validates( mock_function_tool.function = MagicMock() mock_function_tool.function.name = "my_function" - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "agent-id" mock_agent.name = "Agent" mock_agent.description = None @@ -449,7 +447,7 @@ def test_as_agent_with_hosted_tools( mock_code_interpreter = MagicMock() mock_code_interpreter.type = "code_interpreter" - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "agent-id" mock_agent.name = "Agent" mock_agent.description = None @@ -463,9 +461,10 @@ def test_as_agent_with_hosted_tools( agent = provider.as_agent(mock_agent) - assert isinstance(agent, ChatAgent) - # Should have HostedCodeInterpreterTool in the default_options tools - assert any(isinstance(t, HostedCodeInterpreterTool) for t in (agent.default_options.get("tools") or [])) # type: ignore + assert isinstance(agent, Agent) + # Should have code_interpreter dict tool in the default_options tools + tools = agent.default_options.get("tools") or [] + assert any(isinstance(t, dict) and t.get("type") == "code_interpreter" for t in tools) def test_as_agent_with_dict_function_tools_validates( @@ -483,7 +482,7 @@ def test_as_agent_with_dict_function_tools_validates( }, } - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "agent-id" mock_agent.name = "Agent" mock_agent.description = None @@ -515,7 +514,7 @@ def test_as_agent_with_dict_function_tools_provided( }, } - mock_agent = MagicMock(spec=Agent) + mock_agent = MagicMock(spec=AzureAgent) mock_agent.id = "agent-id" mock_agent.name = "Agent" mock_agent.description = None @@ -534,7 +533,7 @@ def test_as_agent_with_dict_function_tools_provided( agent = provider.as_agent(mock_agent, tools=dict_based_function) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.id == "agent-id" @@ -569,8 +568,8 @@ def test_to_azure_ai_agent_tools_function() -> None: def test_to_azure_ai_agent_tools_code_interpreter() -> None: - """Test converting HostedCodeInterpreterTool.""" - tool = HostedCodeInterpreterTool() + """Test converting code_interpreter dict tool.""" + tool = AzureAIAgentClient.get_code_interpreter_tool() result = to_azure_ai_agent_tools([tool]) @@ -579,8 +578,8 @@ def test_to_azure_ai_agent_tools_code_interpreter() -> None: def test_to_azure_ai_agent_tools_file_search() -> None: - """Test converting HostedFileSearchTool with vector stores.""" - tool = HostedFileSearchTool(inputs=[Content.from_hosted_vector_store(vector_store_id="vs-123")]) + """Test converting file_search dict tool with vector stores.""" + tool = AzureAIAgentClient.get_file_search_tool(vector_store_ids=["vs-123"]) run_options: dict[str, Any] = {} result = to_azure_ai_agent_tools([tool], run_options) @@ -590,15 +589,14 @@ def test_to_azure_ai_agent_tools_file_search() -> None: def test_to_azure_ai_agent_tools_web_search_bing_grounding(monkeypatch: Any) -> None: - """Test converting HostedWebSearchTool for Bing Grounding.""" + """Test converting web_search dict tool for Bing Grounding.""" # Use a properly formatted connection ID as required by Azure SDK valid_conn_id = ( "/subscriptions/test-sub/resourceGroups/test-rg/" "providers/Microsoft.CognitiveServices/accounts/test-account/" "projects/test-project/connections/test-connection" ) - monkeypatch.setenv("BING_CONNECTION_ID", valid_conn_id) - tool = HostedWebSearchTool() + tool = AzureAIAgentClient.get_web_search_tool(bing_connection_id=valid_conn_id) result = to_azure_ai_agent_tools([tool]) @@ -606,10 +604,11 @@ def test_to_azure_ai_agent_tools_web_search_bing_grounding(monkeypatch: Any) -> def test_to_azure_ai_agent_tools_web_search_custom(monkeypatch: Any) -> None: - """Test converting HostedWebSearchTool for Custom Bing Search.""" - monkeypatch.setenv("BING_CUSTOM_CONNECTION_ID", "custom-conn-id") - monkeypatch.setenv("BING_CUSTOM_INSTANCE_NAME", "my-instance") - tool = HostedWebSearchTool() + """Test converting web_search dict tool for Custom Bing Search.""" + tool = AzureAIAgentClient.get_web_search_tool( + bing_custom_connection_id="custom-conn-id", + bing_custom_instance_id="my-instance", + ) result = to_azure_ai_agent_tools([tool]) @@ -617,22 +616,23 @@ def test_to_azure_ai_agent_tools_web_search_custom(monkeypatch: Any) -> None: def test_to_azure_ai_agent_tools_web_search_missing_config(monkeypatch: Any) -> None: - """Test converting HostedWebSearchTool raises error when config is missing.""" + """Test converting web_search dict tool without bing config returns empty.""" monkeypatch.delenv("BING_CONNECTION_ID", raising=False) monkeypatch.delenv("BING_CUSTOM_CONNECTION_ID", raising=False) monkeypatch.delenv("BING_CUSTOM_INSTANCE_NAME", raising=False) - tool = HostedWebSearchTool() + tool = {"type": "web_search"} - with pytest.raises(ServiceInitializationError): - to_azure_ai_agent_tools([tool]) + result = to_azure_ai_agent_tools([tool]) + + # web_search without bing connection is passed through as dict + assert len(result) == 1 def test_to_azure_ai_agent_tools_mcp() -> None: - """Test converting HostedMCPTool.""" - tool = HostedMCPTool( + """Test converting MCP dict tool.""" + tool = AzureAIAgentClient.get_mcp_tool( name="my mcp server", url="https://mcp.example.com", - allowed_tools=["tool1", "tool2"], ) result = to_azure_ai_agent_tools([tool]) @@ -651,13 +651,15 @@ def test_to_azure_ai_agent_tools_dict_passthrough() -> None: def test_to_azure_ai_agent_tools_unsupported_type() -> None: - """Test that unsupported tool types raise error.""" + """Test that unsupported tool types pass through unchanged.""" class UnsupportedTool: pass - with pytest.raises(ServiceInitializationError): - to_azure_ai_agent_tools([UnsupportedTool()]) # type: ignore + unsupported = UnsupportedTool() + result = to_azure_ai_agent_tools([unsupported]) # type: ignore + assert len(result) == 1 + assert result[0] is unsupported # Passed through unchanged # endregion @@ -682,7 +684,7 @@ def test_from_azure_ai_agent_tools_code_interpreter() -> None: result = from_azure_ai_agent_tools([tool]) assert len(result) == 1 - assert isinstance(result[0], HostedCodeInterpreterTool) + assert result[0] == {"type": "code_interpreter"} def test_from_azure_ai_agent_tools_code_interpreter_dict() -> None: @@ -692,7 +694,7 @@ def test_from_azure_ai_agent_tools_code_interpreter_dict() -> None: result = from_azure_ai_agent_tools([tool]) assert len(result) == 1 - assert isinstance(result[0], HostedCodeInterpreterTool) + assert result[0] == {"type": "code_interpreter"} def test_from_azure_ai_agent_tools_file_search_dict() -> None: @@ -705,8 +707,8 @@ def test_from_azure_ai_agent_tools_file_search_dict() -> None: result = from_azure_ai_agent_tools([tool]) assert len(result) == 1 - assert isinstance(result[0], HostedFileSearchTool) - assert len(result[0].inputs or []) == 2 + assert result[0]["type"] == "file_search" + assert result[0]["vector_store_ids"] == ["vs-123", "vs-456"] def test_from_azure_ai_agent_tools_bing_grounding_dict() -> None: @@ -719,12 +721,8 @@ def test_from_azure_ai_agent_tools_bing_grounding_dict() -> None: result = from_azure_ai_agent_tools([tool]) assert len(result) == 1 - assert isinstance(result[0], HostedWebSearchTool) - - additional_properties = result[0].additional_properties - - assert additional_properties - assert additional_properties.get("connection_id") == "conn-123" + assert result[0]["type"] == "bing_grounding" + assert result[0]["connection_id"] == "conn-123" def test_from_azure_ai_agent_tools_bing_custom_search_dict() -> None: @@ -740,11 +738,9 @@ def test_from_azure_ai_agent_tools_bing_custom_search_dict() -> None: result = from_azure_ai_agent_tools([tool]) assert len(result) == 1 - assert isinstance(result[0], HostedWebSearchTool) - additional_properties = result[0].additional_properties - - assert additional_properties - assert additional_properties.get("custom_connection_id") == "custom-conn" + assert result[0]["type"] == "bing_custom_search" + assert result[0]["connection_id"] == "custom-conn" + assert result[0]["instance_name"] == "my-instance" def test_from_azure_ai_agent_tools_mcp_dict() -> None: @@ -810,7 +806,7 @@ async def test_integration_create_agent() -> None: ) try: - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.name == "IntegrationTestAgent" assert agent.id is not None finally: @@ -837,7 +833,7 @@ async def test_integration_get_agent() -> None: # Then get it using the provider agent = await provider.get_agent(created.id) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.id == created.id finally: await provider._agents_client.delete_agent(created.id) # type: ignore diff --git a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py index ef1000b12d..d0007841f2 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py @@ -8,20 +8,16 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from agent_framework import ( + Agent, AgentResponse, AgentResponseUpdate, AgentThread, - ChatAgent, - ChatClientProtocol, - ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, Content, - HostedCodeInterpreterTool, - HostedFileSearchTool, - HostedMCPTool, - HostedWebSearchTool, + Message, + SupportsChatGetResponse, tool, ) from agent_framework._serialization import SerializationMixin @@ -127,14 +123,14 @@ def test_azure_ai_settings_init_with_explicit_values() -> None: def test_azure_ai_chat_client_init_with_client(mock_agents_client: MagicMock) -> None: """Test AzureAIAgentClient initialization with existing agents_client.""" - chat_client = create_test_azure_ai_chat_client( + client = create_test_azure_ai_chat_client( mock_agents_client, agent_id="existing-agent-id", thread_id="test-thread-id" ) - assert chat_client.agents_client is mock_agents_client - assert chat_client.agent_id == "existing-agent-id" - assert chat_client.thread_id == "test-thread-id" - assert isinstance(chat_client, ChatClientProtocol) + assert client.agents_client is mock_agents_client + assert client.agent_id == "existing-agent-id" + assert client.thread_id == "test-thread-id" + assert isinstance(client, SupportsChatGetResponse) def test_azure_ai_chat_client_init_auto_create_client( @@ -145,19 +141,19 @@ def test_azure_ai_chat_client_init_auto_create_client( azure_ai_settings = AzureAISettings(**azure_ai_unit_test_env) # type: ignore # Create client instance directly - chat_client = object.__new__(AzureAIAgentClient) - chat_client.agents_client = mock_agents_client - chat_client.agent_id = None - chat_client.thread_id = None - chat_client._should_close_client = False # type: ignore - chat_client.credential = None - chat_client.model_id = azure_ai_settings.model_deployment_name - chat_client.agent_name = None - chat_client.additional_properties = {} - chat_client.middleware = None + client = object.__new__(AzureAIAgentClient) + client.agents_client = mock_agents_client + client.agent_id = None + client.thread_id = None + client._should_close_client = False # type: ignore + client.credential = None + client.model_id = azure_ai_settings.model_deployment_name + client.agent_name = None + client.additional_properties = {} + client.middleware = None - assert chat_client.agents_client is mock_agents_client - assert chat_client.agent_id is None + assert client.agents_client is mock_agents_client + assert client.agent_id is None def test_azure_ai_chat_client_init_missing_project_endpoint() -> None: @@ -253,7 +249,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_temperature_and_ ) -> None: """Test _get_agent_id_or_create with temperature and top_p in run_options.""" azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]) - chat_client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) + client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) run_options = { "model": azure_ai_settings.model_deployment_name, @@ -261,7 +257,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_temperature_and_ "top_p": 0.9, } - agent_id = await chat_client._get_agent_id_or_create(run_options) # type: ignore + agent_id = await client._get_agent_id_or_create(run_options) # type: ignore assert agent_id == "test-agent-id" # Verify create_agent was called with temperature and top_p parameters @@ -275,12 +271,12 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_existing_agent( mock_agents_client: MagicMock, ) -> None: """Test _get_agent_id_or_create when agent_id is already provided.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="existing-agent-id") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="existing-agent-id") - agent_id = await chat_client._get_agent_id_or_create() # type: ignore + agent_id = await client._get_agent_id_or_create() # type: ignore assert agent_id == "existing-agent-id" - assert not chat_client._agent_created + assert not client._agent_created async def test_azure_ai_chat_client_get_agent_id_or_create_create_new( @@ -289,17 +285,17 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_create_new( ) -> None: """Test _get_agent_id_or_create when creating a new agent.""" azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]) - chat_client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) + client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) - agent_id = await chat_client._get_agent_id_or_create(run_options={"model": azure_ai_settings.model_deployment_name}) # type: ignore + agent_id = await client._get_agent_id_or_create(run_options={"model": azure_ai_settings.model_deployment_name}) # type: ignore assert agent_id == "test-agent-id" - assert chat_client._agent_created + assert client._agent_created async def test_azure_ai_chat_client_thread_management_through_public_api(mock_agents_client: MagicMock) -> None: """Test thread creation and management through public API.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Mock get_agent to avoid the async error mock_agents_client.get_agent = AsyncMock(return_value=None) @@ -319,10 +315,10 @@ async def test_azure_ai_chat_client_thread_management_through_public_api(mock_ag mock_stream.__aenter__ = AsyncMock(return_value=empty_async_iter()) mock_stream.__aexit__ = AsyncMock(return_value=None) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] # Call without existing thread - should create new one - response = chat_client.get_response(messages, stream=True) + response = client.get_response(messages, stream=True) # Consume the generator to trigger the method execution async for _ in response: pass @@ -336,20 +332,20 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_missing_model( mock_agents_client: MagicMock, azure_ai_unit_test_env: dict[str, str] ) -> None: """Test _get_agent_id_or_create when model_deployment_name is missing.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) with pytest.raises(ServiceInitializationError, match="Model deployment name is required"): - await chat_client._get_agent_id_or_create() # type: ignore + await client._get_agent_id_or_create() # type: ignore async def test_azure_ai_chat_client_prepare_options_basic(mock_agents_client: MagicMock) -> None: """Test _prepare_options with basic ChatOptions.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options: ChatOptions = {"max_tokens": 100, "temperature": 0.7} - run_options, tool_results = await chat_client._prepare_options(messages, chat_options) # type: ignore + run_options, tool_results = await client._prepare_options(messages, chat_options) # type: ignore assert run_options is not None assert tool_results is None @@ -357,11 +353,11 @@ async def test_azure_ai_chat_client_prepare_options_basic(mock_agents_client: Ma async def test_azure_ai_chat_client_prepare_options_no_chat_options(mock_agents_client: MagicMock) -> None: """Test _prepare_options with default ChatOptions.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] - run_options, tool_results = await chat_client._prepare_options(messages, {}) # type: ignore + run_options, tool_results = await client._prepare_options(messages, {}) # type: ignore assert run_options is not None assert tool_results is None @@ -370,15 +366,15 @@ async def test_azure_ai_chat_client_prepare_options_no_chat_options(mock_agents_ async def test_azure_ai_chat_client_prepare_options_with_image_content(mock_agents_client: MagicMock) -> None: """Test _prepare_options with image content.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Mock get_agent mock_agents_client.get_agent = AsyncMock(return_value=None) image_content = Content.from_uri(uri="https://example.com/image.jpg", media_type="image/jpeg") - messages = [ChatMessage(role="user", contents=[image_content])] + messages = [Message(role="user", contents=[image_content])] - run_options, _ = await chat_client._prepare_options(messages, {}) # type: ignore + run_options, _ = await client._prepare_options(messages, {}) # type: ignore assert "additional_messages" in run_options assert len(run_options["additional_messages"]) == 1 @@ -389,9 +385,9 @@ async def test_azure_ai_chat_client_prepare_options_with_image_content(mock_agen def test_azure_ai_chat_client_prepare_tool_outputs_for_azure_ai_none(mock_agents_client: MagicMock) -> None: """Test _prepare_tool_outputs_for_azure_ai with None input.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) - run_id, tool_outputs, tool_approvals = chat_client._prepare_tool_outputs_for_azure_ai(None) # type: ignore + run_id, tool_outputs, tool_approvals = client._prepare_tool_outputs_for_azure_ai(None) # type: ignore assert run_id is None assert tool_outputs is None @@ -400,22 +396,22 @@ def test_azure_ai_chat_client_prepare_tool_outputs_for_azure_ai_none(mock_agents async def test_azure_ai_chat_client_close_client_when_should_close_true(mock_agents_client: MagicMock) -> None: """Test _close_client_if_needed closes agents_client when should_close_client is True.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) - chat_client._should_close_client = True # type: ignore + client = create_test_azure_ai_chat_client(mock_agents_client) + client._should_close_client = True # type: ignore mock_agents_client.close = AsyncMock() - await chat_client._close_client_if_needed() # type: ignore + await client._close_client_if_needed() # type: ignore mock_agents_client.close.assert_called_once() async def test_azure_ai_chat_client_close_client_when_should_close_false(mock_agents_client: MagicMock) -> None: """Test _close_client_if_needed does not close agents_client when should_close_client is False.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) - chat_client._should_close_client = False # type: ignore + client = create_test_azure_ai_chat_client(mock_agents_client) + client._should_close_client = False # type: ignore - await chat_client._close_client_if_needed() # type: ignore + await client._close_client_if_needed() # type: ignore mock_agents_client.close.assert_not_called() @@ -424,52 +420,52 @@ def test_azure_ai_chat_client_update_agent_name_and_description_when_current_is_ mock_agents_client: MagicMock, ) -> None: """Test _update_agent_name_and_description updates name when current agent_name is None.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) - chat_client.agent_name = None # type: ignore + client = create_test_azure_ai_chat_client(mock_agents_client) + client.agent_name = None # type: ignore - chat_client._update_agent_name_and_description("NewAgentName", "description") # type: ignore + client._update_agent_name_and_description("NewAgentName", "description") # type: ignore - assert chat_client.agent_name == "NewAgentName" - assert chat_client.agent_description == "description" + assert client.agent_name == "NewAgentName" + assert client.agent_description == "description" def test_azure_ai_chat_client_update_agent_name_and_description_when_current_exists( mock_agents_client: MagicMock, ) -> None: """Test _update_agent_name_and_description does not update when current agent_name exists.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) - chat_client.agent_name = "ExistingName" # type: ignore - chat_client.agent_description = "ExistingDescription" # type: ignore + client = create_test_azure_ai_chat_client(mock_agents_client) + client.agent_name = "ExistingName" # type: ignore + client.agent_description = "ExistingDescription" # type: ignore - chat_client._update_agent_name_and_description("NewAgentName", "description") # type: ignore + client._update_agent_name_and_description("NewAgentName", "description") # type: ignore - assert chat_client.agent_name == "ExistingName" - assert chat_client.agent_description == "ExistingDescription" + assert client.agent_name == "ExistingName" + assert client.agent_description == "ExistingDescription" def test_azure_ai_chat_client_update_agent_name_and_description_with_none_input(mock_agents_client: MagicMock) -> None: """Test _update_agent_name_and_description with None input.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) - chat_client.agent_name = None # type: ignore - chat_client.agent_description = None # type: ignore + client = create_test_azure_ai_chat_client(mock_agents_client) + client.agent_name = None # type: ignore + client.agent_description = None # type: ignore - chat_client._update_agent_name_and_description(None, None) # type: ignore + client._update_agent_name_and_description(None, None) # type: ignore - assert chat_client.agent_name is None - assert chat_client.agent_description is None + assert client.agent_name is None + assert client.agent_description is None async def test_azure_ai_chat_client_prepare_options_with_messages(mock_agents_client: MagicMock) -> None: """Test _prepare_options with different message types.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) # Test with system message (becomes instruction) messages = [ - ChatMessage(role="system", text="You are a helpful assistant"), - ChatMessage(role="user", text="Hello"), + Message(role="system", text="You are a helpful assistant"), + Message(role="user", text="Hello"), ] - run_options, _ = await chat_client._prepare_options(messages, {}) # type: ignore + run_options, _ = await client._prepare_options(messages, {}) # type: ignore assert "instructions" in run_options assert "You are a helpful assistant" in run_options["instructions"] @@ -485,15 +481,15 @@ async def test_azure_ai_chat_client_prepare_options_with_instructions_from_optio This verifies that agent instructions set via as_agent(instructions=...) are properly included in the API call. """ - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") mock_agents_client.get_agent = AsyncMock(return_value=None) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options: ChatOptions = { "instructions": "You are a thoughtful reviewer. Give brief feedback.", } - run_options, _ = await chat_client._prepare_options(messages, chat_options) # type: ignore + run_options, _ = await client._prepare_options(messages, chat_options) # type: ignore assert "instructions" in run_options assert "reviewer" in run_options["instructions"].lower() @@ -507,18 +503,18 @@ async def test_azure_ai_chat_client_prepare_options_merges_instructions_from_mes When instructions come from both system/developer messages AND from options, both should be included in the final instructions. """ - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") mock_agents_client.get_agent = AsyncMock(return_value=None) messages = [ - ChatMessage(role="system", text="Context: You are reviewing marketing copy."), - ChatMessage(role="user", text="Review this tagline"), + Message(role="system", text="Context: You are reviewing marketing copy."), + Message(role="user", text="Review this tagline"), ] chat_options: ChatOptions = { "instructions": "Be concise and constructive in your feedback.", } - run_options, _ = await chat_client._prepare_options(messages, chat_options) # type: ignore + run_options, _ = await client._prepare_options(messages, chat_options) # type: ignore assert "instructions" in run_options instructions_text = run_options["instructions"] @@ -529,16 +525,16 @@ async def test_azure_ai_chat_client_prepare_options_merges_instructions_from_mes async def test_azure_ai_chat_client_inner_get_response(mock_agents_client: MagicMock) -> None: """Test _inner_get_response method.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") async def mock_streaming_response(): yield ChatResponseUpdate(role="assistant", contents=[Content.from_text("Hello back")]) with ( - patch.object(chat_client, "_inner_get_response", return_value=mock_streaming_response()), + patch.object(client, "_inner_get_response", return_value=mock_streaming_response()), patch("agent_framework.ChatResponse.from_update_generator") as mock_from_generator, ): - mock_response = ChatResponse(messages=[ChatMessage(role="assistant", text="Hello back")]) + mock_response = ChatResponse(messages=[Message(role="assistant", text="Hello back")]) mock_from_generator.return_value = mock_response result = await ChatResponse.from_update_generator(mock_streaming_response()) @@ -552,7 +548,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_run_options( ) -> None: """Test _get_agent_id_or_create with run_options containing tools and instructions.""" azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]) - chat_client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) + client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) run_options = { "tools": [{"type": "function", "function": {"name": "test_tool"}}], @@ -561,7 +557,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_run_options( "model": azure_ai_settings.model_deployment_name, } - agent_id = await chat_client._get_agent_id_or_create(run_options) # type: ignore + agent_id = await client._get_agent_id_or_create(run_options) # type: ignore assert agent_id == "test-agent-id" # Verify create_agent was called with run_options parameters @@ -574,7 +570,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_run_options( async def test_azure_ai_chat_client_prepare_thread_cancels_active_run(mock_agents_client: MagicMock) -> None: """Test _prepare_thread cancels active thread run when provided.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") mock_thread_run = MagicMock() mock_thread_run.id = "run_123" @@ -582,7 +578,7 @@ async def test_azure_ai_chat_client_prepare_thread_cancels_active_run(mock_agent run_options = {"additional_messages": []} # type: ignore - result = await chat_client._prepare_thread("test-thread", mock_thread_run, run_options) # type: ignore + result = await client._prepare_thread("test-thread", mock_thread_run, run_options) # type: ignore assert result == "test-thread" mock_agents_client.runs.cancel.assert_called_once_with("test-thread", "run_123") @@ -590,7 +586,7 @@ async def test_azure_ai_chat_client_prepare_thread_cancels_active_run(mock_agent def test_azure_ai_chat_client_parse_function_calls_from_azure_ai_basic(mock_agents_client: MagicMock) -> None: """Test _parse_function_calls_from_azure_ai with basic function call.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) mock_tool_call = MagicMock(spec=RequiredFunctionToolCall) mock_tool_call.id = "call_123" @@ -603,7 +599,7 @@ def test_azure_ai_chat_client_parse_function_calls_from_azure_ai_basic(mock_agen mock_event_data = MagicMock(spec=ThreadRun) mock_event_data.required_action = mock_submit_action - result = chat_client._parse_function_calls_from_azure_ai(mock_event_data, "response_123") # type: ignore + result = client._parse_function_calls_from_azure_ai(mock_event_data, "response_123") # type: ignore assert len(result) == 1 assert result[0].type == "function_call" @@ -615,12 +611,12 @@ def test_azure_ai_chat_client_parse_function_calls_from_azure_ai_no_submit_actio mock_agents_client: MagicMock, ) -> None: """Test _parse_function_calls_from_azure_ai when required_action is not SubmitToolOutputsAction.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) mock_event_data = MagicMock(spec=ThreadRun) mock_event_data.required_action = MagicMock() - result = chat_client._parse_function_calls_from_azure_ai(mock_event_data, "response_123") # type: ignore + result = client._parse_function_calls_from_azure_ai(mock_event_data, "response_123") # type: ignore assert result == [] @@ -629,7 +625,7 @@ def test_azure_ai_chat_client_parse_function_calls_from_azure_ai_non_function_to mock_agents_client: MagicMock, ) -> None: """Test _parse_function_calls_from_azure_ai with non-function tool call.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) mock_tool_call = MagicMock() @@ -639,7 +635,7 @@ def test_azure_ai_chat_client_parse_function_calls_from_azure_ai_non_function_to mock_event_data = MagicMock(spec=ThreadRun) mock_event_data.required_action = mock_submit_action - result = chat_client._parse_function_calls_from_azure_ai(mock_event_data, "response_123") # type: ignore + result = client._parse_function_calls_from_azure_ai(mock_event_data, "response_123") # type: ignore assert result == [] @@ -648,11 +644,11 @@ async def test_azure_ai_chat_client_prepare_options_with_none_tool_choice( mock_agents_client: MagicMock, ) -> None: """Test _prepare_options with tool_choice set to 'none'.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) chat_options: ChatOptions = {"tool_choice": "none"} - run_options, _ = await chat_client._prepare_options([], chat_options) # type: ignore + run_options, _ = await client._prepare_options([], chat_options) # type: ignore assert run_options["tool_choice"] == AgentsToolChoiceOptionMode.NONE @@ -661,11 +657,11 @@ async def test_azure_ai_chat_client_prepare_options_with_auto_tool_choice( mock_agents_client: MagicMock, ) -> None: """Test _prepare_options with tool_choice set to 'auto'.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) chat_options = {"tool_choice": "auto"} - run_options, _ = await chat_client._prepare_options([], chat_options) # type: ignore + run_options, _ = await client._prepare_options([], chat_options) # type: ignore assert run_options["tool_choice"] == AgentsToolChoiceOptionMode.AUTO @@ -674,16 +670,16 @@ async def test_azure_ai_chat_client_prepare_options_tool_choice_required_specifi mock_agents_client: MagicMock, ) -> None: """Test _prepare_options with required tool_choice specifying a specific function name.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) required_tool_mode = {"mode": "required", "required_function_name": "specific_function_name"} dict_tool = {"type": "function", "function": {"name": "test_function"}} chat_options = {"tools": [dict_tool], "tool_choice": required_tool_mode} - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] - run_options, _ = await chat_client._prepare_options(messages, chat_options) # type: ignore + run_options, _ = await client._prepare_options(messages, chat_options) # type: ignore # Verify tool_choice is set to the specific named function assert "tool_choice" in run_options @@ -697,14 +693,14 @@ async def test_azure_ai_chat_client_prepare_options_with_response_format( mock_agents_client: MagicMock, ) -> None: """Test _prepare_options with response_format configured.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) class TestResponseModel(BaseModel): name: str = Field(description="Test name") chat_options: ChatOptions = {"response_format": TestResponseModel} - run_options, _ = await chat_client._prepare_options([], chat_options) # type: ignore + run_options, _ = await client._prepare_options([], chat_options) # type: ignore assert "response_format" in run_options response_format = run_options["response_format"] @@ -714,155 +710,138 @@ async def test_azure_ai_chat_client_prepare_options_with_response_format( def test_azure_ai_chat_client_service_url_method(mock_agents_client: MagicMock) -> None: """Test service_url method returns endpoint.""" mock_agents_client._config.endpoint = "https://test-endpoint.com/" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) - url = chat_client.service_url() + url = client.service_url() assert url == "https://test-endpoint.com/" async def test_azure_ai_chat_client_prepare_options_mcp_never_require(mock_agents_client: MagicMock) -> None: - """Test _prepare_options with HostedMCPTool having never_require approval mode.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + """Test _prepare_options with MCP dict tool having never_require approval mode.""" + client = create_test_azure_ai_chat_client(mock_agents_client) - mcp_tool = HostedMCPTool(name="Test MCP Tool", url="https://example.com/mcp", approval_mode="never_require") + # Create MCP tool with approval_mode parameter + mcp_tool = AzureAIAgentClient.get_mcp_tool( + name="Test MCP Tool", url="https://example.com/mcp", approval_mode="never_require" + ) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options: ChatOptions = {"tools": [mcp_tool], "tool_choice": "auto"} - with patch("agent_framework_azure_ai._shared.McpTool") as mock_mcp_tool_class: - mock_mcp_tool_instance = MagicMock() - mock_mcp_tool_instance.definitions = [{"type": "mcp", "name": "test_mcp"}] - mock_mcp_tool_class.return_value = mock_mcp_tool_instance + run_options, _ = await client._prepare_options(messages, chat_options) # type: ignore - run_options, _ = await chat_client._prepare_options(messages, chat_options) # type: ignore + # Verify tool_resources is created with correct MCP approval structure + assert "tool_resources" in run_options, f"Expected 'tool_resources' in run_options keys: {list(run_options.keys())}" + assert "mcp" in run_options["tool_resources"] + assert len(run_options["tool_resources"]["mcp"]) == 1 - # Verify tool_resources is created with correct MCP approval structure - assert "tool_resources" in run_options, ( - f"Expected 'tool_resources' in run_options keys: {list(run_options.keys())}" - ) - assert "mcp" in run_options["tool_resources"] - assert len(run_options["tool_resources"]["mcp"]) == 1 - - mcp_resource = run_options["tool_resources"]["mcp"][0] - assert mcp_resource["server_label"] == "Test_MCP_Tool" - assert mcp_resource["require_approval"] == "never" + mcp_resource = run_options["tool_resources"]["mcp"][0] + assert mcp_resource["server_label"] == "Test_MCP_Tool" + assert mcp_resource["require_approval"] == "never" async def test_azure_ai_chat_client_prepare_options_mcp_with_headers(mock_agents_client: MagicMock) -> None: - """Test _prepare_options with HostedMCPTool having headers.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + """Test _prepare_options with MCP dict tool having headers.""" + client = create_test_azure_ai_chat_client(mock_agents_client) - # Test with headers + # Test with headers - create MCP tool with all options headers = {"Authorization": "Bearer DUMMY_TOKEN", "X-API-Key": "DUMMY_KEY"} - mcp_tool = HostedMCPTool( - name="Test MCP Tool", url="https://example.com/mcp", headers=headers, approval_mode="never_require" + mcp_tool = AzureAIAgentClient.get_mcp_tool( + name="Test MCP Tool", + url="https://example.com/mcp", + headers=headers, + approval_mode="never_require", ) - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] chat_options: ChatOptions = {"tools": [mcp_tool], "tool_choice": "auto"} - with patch("agent_framework_azure_ai._shared.McpTool") as mock_mcp_tool_class: - mock_mcp_tool_instance = MagicMock() - mock_mcp_tool_instance.definitions = [{"type": "mcp", "name": "test_mcp"}] - mock_mcp_tool_class.return_value = mock_mcp_tool_instance + run_options, _ = await client._prepare_options(messages, chat_options) # type: ignore - run_options, _ = await chat_client._prepare_options(messages, chat_options) # type: ignore + # Verify tool_resources is created with headers + assert "tool_resources" in run_options + assert "mcp" in run_options["tool_resources"] + assert len(run_options["tool_resources"]["mcp"]) == 1 - # Verify tool_resources is created with headers - assert "tool_resources" in run_options - assert "mcp" in run_options["tool_resources"] - assert len(run_options["tool_resources"]["mcp"]) == 1 - - mcp_resource = run_options["tool_resources"]["mcp"][0] - assert mcp_resource["server_label"] == "Test_MCP_Tool" - assert mcp_resource["require_approval"] == "never" - assert mcp_resource["headers"] == headers + mcp_resource = run_options["tool_resources"]["mcp"][0] + assert mcp_resource["server_label"] == "Test_MCP_Tool" + assert mcp_resource["require_approval"] == "never" + assert mcp_resource["headers"] == headers async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_bing_grounding( mock_agents_client: MagicMock, ) -> None: - """Test _prepare_tools_for_azure_ai with HostedWebSearchTool using Bing Grounding.""" + """Test _prepare_tools_for_azure_ai with BingGroundingTool from get_web_search_tool().""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") - web_search_tool = HostedWebSearchTool( - additional_properties={ - "connection_id": "test-connection-id", - "count": 5, - "freshness": "Day", - "market": "en-US", - "set_lang": "en", - } - ) - - # Mock BingGroundingTool + # Mock BingGroundingTool to avoid SDK validation of connection ID with patch("agent_framework_azure_ai._chat_client.BingGroundingTool") as mock_bing_grounding: mock_bing_tool = MagicMock() mock_bing_tool.definitions = [{"type": "bing_grounding"}] mock_bing_grounding.return_value = mock_bing_tool - result = await chat_client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore + # get_web_search_tool now returns a BingGroundingTool directly + web_search_tool = client.get_web_search_tool(bing_connection_id="test-connection-id") + # Verify the factory method created the tool with correct args + mock_bing_grounding.assert_called_once_with(connection_id="test-connection-id") + + result = await client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore + + # BingGroundingTool.definitions should be extended into result assert len(result) == 1 assert result[0] == {"type": "bing_grounding"} - call_args = mock_bing_grounding.call_args[1] - assert call_args["count"] == 5 - assert call_args["freshness"] == "Day" - assert call_args["market"] == "en-US" - assert call_args["set_lang"] == "en" - assert "connection_id" in call_args async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_bing_grounding_with_connection_id( mock_agents_client: MagicMock, ) -> None: - """Test _prepare_tools_... with HostedWebSearchTool using Bing Grounding with connection_id (no HTTP call).""" + """Test _prepare_tools_for_azure_ai with BingGroundingTool using explicit connection_id.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") - web_search_tool = HostedWebSearchTool( - additional_properties={ - "connection_id": "direct-connection-id", - "count": 3, - } - ) - - # Mock BingGroundingTool + # Mock BingGroundingTool to avoid SDK validation of connection ID with patch("agent_framework_azure_ai._chat_client.BingGroundingTool") as mock_bing_grounding: mock_bing_tool = MagicMock() mock_bing_tool.definitions = [{"type": "bing_grounding"}] mock_bing_grounding.return_value = mock_bing_tool - result = await chat_client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore + web_search_tool = client.get_web_search_tool(bing_connection_id="direct-connection-id") + + mock_bing_grounding.assert_called_once_with(connection_id="direct-connection-id") + + result = await client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore assert len(result) == 1 assert result[0] == {"type": "bing_grounding"} - mock_bing_grounding.assert_called_once_with(connection_id="direct-connection-id", count=3) async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_custom_bing( mock_agents_client: MagicMock, ) -> None: - """Test _prepare_tools_for_azure_ai with HostedWebSearchTool using Custom Bing Search.""" + """Test _prepare_tools_for_azure_ai with BingCustomSearchTool from get_web_search_tool().""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") - web_search_tool = HostedWebSearchTool( - additional_properties={ - "custom_connection_id": "custom-connection-id", - "custom_instance_name": "custom-instance", - "count": 10, - } - ) - - # Mock BingCustomSearchTool + # Mock BingCustomSearchTool to avoid SDK validation with patch("agent_framework_azure_ai._chat_client.BingCustomSearchTool") as mock_custom_bing: mock_custom_tool = MagicMock() mock_custom_tool.definitions = [{"type": "bing_custom_search"}] mock_custom_bing.return_value = mock_custom_tool - result = await chat_client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore + web_search_tool = client.get_web_search_tool( + bing_custom_connection_id="custom-connection-id", + bing_custom_instance_id="custom-instance", + ) + + mock_custom_bing.assert_called_once_with( + connection_id="custom-connection-id", + instance_name="custom-instance", + ) + + result = await client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore assert len(result) == 1 assert result[0] == {"type": "bing_custom_search"} @@ -871,40 +850,32 @@ async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_custom async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_file_search_with_vector_stores( mock_agents_client: MagicMock, ) -> None: - """Test _prepare_tools_for_azure_ai with HostedFileSearchTool using vector stores.""" + """Test _prepare_tools_for_azure_ai with FileSearchTool from get_file_search_tool().""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") - vector_store_input = Content.from_hosted_vector_store(vector_store_id="vs-123") - file_search_tool = HostedFileSearchTool(inputs=[vector_store_input]) + # get_file_search_tool() now returns a FileSearchTool instance directly + file_search_tool = client.get_file_search_tool(vector_store_ids=["vs-123"]) - # Mock FileSearchTool - with patch("agent_framework_azure_ai._chat_client.FileSearchTool") as mock_file_search: - mock_file_tool = MagicMock() - mock_file_tool.definitions = [{"type": "file_search"}] - mock_file_tool.resources = {"vector_store_ids": ["vs-123"]} - mock_file_search.return_value = mock_file_tool + run_options: dict[str, Any] = {} + result = await client._prepare_tools_for_azure_ai([file_search_tool], run_options) # type: ignore - run_options = {} - result = await chat_client._prepare_tools_for_azure_ai([file_search_tool], run_options) # type: ignore - - assert len(result) == 1 - assert result[0] == {"type": "file_search"} - assert run_options["tool_resources"] == {"vector_store_ids": ["vs-123"]} - mock_file_search.assert_called_once_with(vector_store_ids=["vs-123"]) + assert len(result) == 1 + assert result[0] == {"type": "file_search"} + assert run_options["tool_resources"] == {"file_search": {"vector_store_ids": ["vs-123"]}} async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals( mock_agents_client: MagicMock, ) -> None: """Test _create_agent_stream with tool approvals submission path.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Mock active thread run that matches the tool run ID mock_thread_run = MagicMock() mock_thread_run.thread_id = "test-thread" mock_thread_run.id = "test-run-id" - chat_client._get_active_thread_run = AsyncMock(return_value=mock_thread_run) # type: ignore + client._get_active_thread_run = AsyncMock(return_value=mock_thread_run) # type: ignore # Mock required action results with approval response that matches run ID approval_response = Content.from_function_approval_response( @@ -920,7 +891,7 @@ async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals( mock_agents_client.runs.submit_tool_outputs_stream = AsyncMock() with patch("azure.ai.agents.models.AsyncAgentEventHandler", return_value=mock_handler): - stream, final_thread_id = await chat_client._create_agent_stream( # type: ignore + stream, final_thread_id = await client._create_agent_stream( # type: ignore "test-agent", {"thread_id": "test-thread"}, [approval_response] ) @@ -938,7 +909,7 @@ async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals( async def test_azure_ai_chat_client_get_active_thread_run_with_active_run(mock_agents_client: MagicMock) -> None: """Test _get_active_thread_run when there's an active run.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Mock an active run mock_run = MagicMock() @@ -949,7 +920,7 @@ async def test_azure_ai_chat_client_get_active_thread_run_with_active_run(mock_a mock_agents_client.runs.list = mock_list_runs - result = await chat_client._get_active_thread_run("thread-123") # type: ignore + result = await client._get_active_thread_run("thread-123") # type: ignore assert result == mock_run @@ -957,7 +928,7 @@ async def test_azure_ai_chat_client_get_active_thread_run_with_active_run(mock_a async def test_azure_ai_chat_client_get_active_thread_run_no_active_run(mock_agents_client: MagicMock) -> None: """Test _get_active_thread_run when there's no active run.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Mock a completed run (not active) mock_run = MagicMock() @@ -968,16 +939,16 @@ async def test_azure_ai_chat_client_get_active_thread_run_no_active_run(mock_age mock_agents_client.runs.list = mock_list_runs - result = await chat_client._get_active_thread_run("thread-123") # type: ignore + result = await client._get_active_thread_run("thread-123") # type: ignore assert result is None async def test_azure_ai_chat_client_get_active_thread_run_no_thread(mock_agents_client: MagicMock) -> None: """Test _get_active_thread_run with None thread_id.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") - result = await chat_client._get_active_thread_run(None) # type: ignore + result = await client._get_active_thread_run(None) # type: ignore assert result is None # Should not call list since thread_id is None @@ -986,14 +957,14 @@ async def test_azure_ai_chat_client_get_active_thread_run_no_thread(mock_agents_ async def test_azure_ai_chat_client_service_url(mock_agents_client: MagicMock) -> None: """Test service_url method.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Mock the config endpoint mock_config = MagicMock() mock_config.endpoint = "https://test-endpoint.com/" mock_agents_client._config = mock_config - result = chat_client.service_url() + result = client.service_url() assert result == "https://test-endpoint.com/" @@ -1002,12 +973,12 @@ async def test_azure_ai_chat_client_prepare_tool_outputs_for_azure_tool_result( mock_agents_client: MagicMock, ) -> None: """Test _prepare_tool_outputs_for_azure_ai with FunctionResultContent.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Test with simple result function_result = Content.from_function_result(call_id='["run_123", "call_456"]', result="Simple result") - run_id, tool_outputs, tool_approvals = chat_client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore + run_id, tool_outputs, tool_approvals = client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore assert run_id == "run_123" assert tool_approvals is None @@ -1020,25 +991,25 @@ async def test_azure_ai_chat_client_prepare_tool_outputs_for_azure_tool_result( async def test_azure_ai_chat_client_convert_required_action_invalid_call_id(mock_agents_client: MagicMock) -> None: """Test _prepare_tool_outputs_for_azure_ai with invalid call_id format.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Invalid call_id format - should raise JSONDecodeError function_result = Content.from_function_result(call_id="invalid_json", result="result") with pytest.raises(json.JSONDecodeError): - chat_client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore + client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore async def test_azure_ai_chat_client_convert_required_action_invalid_structure( mock_agents_client: MagicMock, ) -> None: """Test _prepare_tool_outputs_for_azure_ai with invalid call_id structure.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Valid JSON but invalid structure (missing second element) function_result = Content.from_function_result(call_id='["run_123"]', result="result") - run_id, tool_outputs, tool_approvals = chat_client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore + run_id, tool_outputs, tool_approvals = client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore # Should return None values when structure is invalid assert run_id is None @@ -1056,13 +1027,13 @@ async def test_azure_ai_chat_client_convert_required_action_serde_model_results( self.name = name self.value = value - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Test with BaseModel result mock_result = MockResult(name="test", value=42) function_result = Content.from_function_result(call_id='["run_123", "call_456"]', result=mock_result) - run_id, tool_outputs, tool_approvals = chat_client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore + run_id, tool_outputs, tool_approvals = client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore assert run_id == "run_123" assert tool_approvals is None @@ -1083,14 +1054,14 @@ async def test_azure_ai_chat_client_convert_required_action_multiple_results( def __init__(self, data: str): self.data = data - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Test with multiple results - mix of BaseModel and regular objects mock_basemodel = MockResult(data="model_data") results_list = [mock_basemodel, {"key": "value"}, "string_result"] function_result = Content.from_function_result(call_id='["run_123", "call_456"]', result=results_list) - run_id, tool_outputs, tool_approvals = chat_client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore + run_id, tool_outputs, tool_approvals = client._prepare_tool_outputs_for_azure_ai([function_result]) # type: ignore assert run_id == "run_123" assert tool_outputs is not None @@ -1111,7 +1082,7 @@ async def test_azure_ai_chat_client_convert_required_action_approval_response( mock_agents_client: MagicMock, ) -> None: """Test _prepare_tool_outputs_for_azure_ai with FunctionApprovalResponseContent.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Test with approval response - need to provide required fields approval_response = Content.from_function_approval_response( @@ -1122,7 +1093,7 @@ async def test_azure_ai_chat_client_convert_required_action_approval_response( approved=True, ) - run_id, tool_outputs, tool_approvals = chat_client._prepare_tool_outputs_for_azure_ai([approval_response]) # type: ignore + run_id, tool_outputs, tool_approvals = client._prepare_tool_outputs_for_azure_ai([approval_response]) # type: ignore assert run_id == "run_123" assert tool_outputs is None @@ -1136,7 +1107,7 @@ async def test_azure_ai_chat_client_parse_function_calls_from_azure_ai_approval_ mock_agents_client: MagicMock, ) -> None: """Test _parse_function_calls_from_azure_ai with approval action.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Mock SubmitToolApprovalAction with RequiredMcpToolCall mock_tool_call = MagicMock(spec=RequiredMcpToolCall) @@ -1150,7 +1121,7 @@ async def test_azure_ai_chat_client_parse_function_calls_from_azure_ai_approval_ mock_event_data = MagicMock(spec=ThreadRun) mock_event_data.required_action = mock_approval_action - result = chat_client._parse_function_calls_from_azure_ai(mock_event_data, "response_123") # type: ignore + result = client._parse_function_calls_from_azure_ai(mock_event_data, "response_123") # type: ignore assert len(result) == 1 assert result[0].type == "function_approval_request" @@ -1164,12 +1135,12 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_agent_name( ) -> None: """Test _get_agent_id_or_create uses default name when no agent_name set.""" azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]) - chat_client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) + client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) # Ensure agent_name is None to test the default - chat_client.agent_name = None # type: ignore + client.agent_name = None # type: ignore - agent_id = await chat_client._get_agent_id_or_create(run_options={"model": azure_ai_settings.model_deployment_name}) # type: ignore + agent_id = await client._get_agent_id_or_create(run_options={"model": azure_ai_settings.model_deployment_name}) # type: ignore assert agent_id == "test-agent-id" # Verify create_agent was called with default "UnnamedAgent" @@ -1183,12 +1154,12 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_response_format( ) -> None: """Test _get_agent_id_or_create with response_format in run_options.""" azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]) - chat_client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) + client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) # Test with response_format in run_options run_options = {"response_format": {"type": "json_object"}, "model": azure_ai_settings.model_deployment_name} - agent_id = await chat_client._get_agent_id_or_create(run_options) # type: ignore + agent_id = await client._get_agent_id_or_create(run_options) # type: ignore assert agent_id == "test-agent-id" # Verify create_agent was called with response_format @@ -1202,7 +1173,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_tool_resources( ) -> None: """Test _get_agent_id_or_create with tool_resources in run_options.""" azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]) - chat_client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) + client = create_test_azure_ai_chat_client(mock_agents_client, azure_ai_settings=azure_ai_settings) # Test with tool_resources in run_options run_options = { @@ -1210,7 +1181,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_tool_resources( "model": azure_ai_settings.model_deployment_name, } - agent_id = await chat_client._get_agent_id_or_create(run_options) # type: ignore + agent_id = await client._get_agent_id_or_create(run_options) # type: ignore assert agent_id == "test-agent-id" # Verify create_agent was called with tool_resources @@ -1223,13 +1194,13 @@ async def test_azure_ai_chat_client_create_agent_stream_submit_tool_outputs( mock_agents_client: MagicMock, ) -> None: """Test _create_agent_stream with tool outputs submission path.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Mock active thread run that matches the tool run ID mock_thread_run = MagicMock() mock_thread_run.thread_id = "test-thread" mock_thread_run.id = "test-run-id" - chat_client._get_active_thread_run = AsyncMock(return_value=mock_thread_run) # type: ignore + client._get_active_thread_run = AsyncMock(return_value=mock_thread_run) # type: ignore # Mock required action results with matching run ID function_result = Content.from_function_result(call_id='["test-run-id", "test-call-id"]', result="test result") @@ -1239,7 +1210,7 @@ async def test_azure_ai_chat_client_create_agent_stream_submit_tool_outputs( mock_agents_client.runs.submit_tool_outputs_stream = AsyncMock() with patch("azure.ai.agents.models.AsyncAgentEventHandler", return_value=mock_handler): - stream, final_thread_id = await chat_client._create_agent_stream( # type: ignore + stream, final_thread_id = await client._create_agent_stream( # type: ignore agent_id="test-agent", run_options={"thread_id": "test-thread"}, required_action_results=[function_result] ) @@ -1250,7 +1221,7 @@ async def test_azure_ai_chat_client_create_agent_stream_submit_tool_outputs( def test_azure_ai_chat_client_extract_url_citations_with_citations(mock_agents_client: MagicMock) -> None: """Test _extract_url_citations with MessageDeltaChunk containing URL citations.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Create mock URL citation annotation mock_url_citation = MagicMock() @@ -1278,7 +1249,7 @@ def test_azure_ai_chat_client_extract_url_citations_with_citations(mock_agents_c mock_chunk.delta = mock_delta # Call the method with empty azure_search_tool_calls - citations = chat_client._extract_url_citations(mock_chunk, []) # type: ignore + citations = client._extract_url_citations(mock_chunk, []) # type: ignore # Verify results assert len(citations) == 1 @@ -1296,7 +1267,7 @@ def test_azure_ai_chat_client_extract_file_path_contents_with_file_path_annotati mock_agents_client: MagicMock, ) -> None: """Test _extract_file_path_contents with MessageDeltaChunk containing file path annotation.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Create mock file_path annotation mock_file_path = MagicMock() @@ -1321,7 +1292,7 @@ def test_azure_ai_chat_client_extract_file_path_contents_with_file_path_annotati mock_chunk.delta = mock_delta # Call the method - file_contents = chat_client._extract_file_path_contents(mock_chunk) + file_contents = client._extract_file_path_contents(mock_chunk) # Verify results assert len(file_contents) == 1 @@ -1333,7 +1304,7 @@ def test_azure_ai_chat_client_extract_file_path_contents_with_file_citation_anno mock_agents_client: MagicMock, ) -> None: """Test _extract_file_path_contents with MessageDeltaChunk containing file citation annotation.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Create mock file_citation annotation mock_file_citation = MagicMock() @@ -1358,7 +1329,7 @@ def test_azure_ai_chat_client_extract_file_path_contents_with_file_citation_anno mock_chunk.delta = mock_delta # Call the method - file_contents = chat_client._extract_file_path_contents(mock_chunk) + file_contents = client._extract_file_path_contents(mock_chunk) # Verify results assert len(file_contents) == 1 @@ -1370,7 +1341,7 @@ def test_azure_ai_chat_client_extract_file_path_contents_empty_annotations( mock_agents_client: MagicMock, ) -> None: """Test _extract_file_path_contents with no annotations returns empty list.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Create mock text content with no annotations mock_text = MagicMock() @@ -1388,7 +1359,7 @@ def test_azure_ai_chat_client_extract_file_path_contents_empty_annotations( mock_chunk.delta = mock_delta # Call the method - file_contents = chat_client._extract_file_path_contents(mock_chunk) + file_contents = client._extract_file_path_contents(mock_chunk) # Verify results assert len(file_contents) == 0 @@ -1407,17 +1378,17 @@ def get_weather( async def test_azure_ai_chat_client_get_response() -> None: """Test Azure AI Chat Client response.""" async with AzureAIAgentClient(credential=AzureCliCredential()) as azure_ai_chat_client: - assert isinstance(azure_ai_chat_client, ChatClientProtocol) + assert isinstance(azure_ai_chat_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] + messages: list[Message] = [] messages.append( - ChatMessage( + Message( role="user", text="The weather in Seattle is currently sunny with a high of 25°C. " "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(Message(role="user", text="What's the weather like today?")) # Test that the agents_client can be used to get a response response = await azure_ai_chat_client.get_response(messages=messages) @@ -1432,10 +1403,10 @@ async def test_azure_ai_chat_client_get_response() -> None: async def test_azure_ai_chat_client_get_response_tools() -> None: """Test Azure AI Chat Client response with tools.""" async with AzureAIAgentClient(credential=AzureCliCredential()) as azure_ai_chat_client: - assert isinstance(azure_ai_chat_client, ChatClientProtocol) + assert isinstance(azure_ai_chat_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages: list[Message] = [] + messages.append(Message(role="user", text="What's the weather like in Seattle?")) # Test that the agents_client can be used to get a response response = await azure_ai_chat_client.get_response( @@ -1453,17 +1424,17 @@ async def test_azure_ai_chat_client_get_response_tools() -> None: async def test_azure_ai_chat_client_streaming() -> None: """Test Azure AI Chat Client streaming response.""" async with AzureAIAgentClient(credential=AzureCliCredential()) as azure_ai_chat_client: - assert isinstance(azure_ai_chat_client, ChatClientProtocol) + assert isinstance(azure_ai_chat_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] + messages: list[Message] = [] messages.append( - ChatMessage( + Message( role="user", text="The weather in Seattle is currently sunny with a high of 25°C. " "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(Message(role="user", text="What's the weather like today?")) # Test that the agents_client can be used to get a response response = azure_ai_chat_client.get_response(messages=messages, stream=True) @@ -1484,10 +1455,10 @@ async def test_azure_ai_chat_client_streaming() -> None: async def test_azure_ai_chat_client_streaming_tools() -> None: """Test Azure AI Chat Client streaming response with tools.""" async with AzureAIAgentClient(credential=AzureCliCredential()) as azure_ai_chat_client: - assert isinstance(azure_ai_chat_client, ChatClientProtocol) + assert isinstance(azure_ai_chat_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages: list[Message] = [] + messages.append(Message(role="user", text="What's the weather like in Seattle?")) # Test that the agents_client can be used to get a response response = azure_ai_chat_client.get_response( @@ -1509,9 +1480,9 @@ async def test_azure_ai_chat_client_streaming_tools() -> None: @pytest.mark.flaky @skip_if_azure_ai_integration_tests_disabled async def test_azure_ai_chat_client_agent_basic_run() -> None: - """Test ChatAgent basic run functionality with AzureAIAgentClient.""" - async with ChatAgent( - chat_client=AzureAIAgentClient(credential=AzureCliCredential()), + """Test Agent basic run functionality with AzureAIAgentClient.""" + async with Agent( + client=AzureAIAgentClient(credential=AzureCliCredential()), ) as agent: # Run a simple query response = await agent.run("Hello! Please respond with 'Hello World' exactly.") @@ -1526,9 +1497,9 @@ async def test_azure_ai_chat_client_agent_basic_run() -> None: @pytest.mark.flaky @skip_if_azure_ai_integration_tests_disabled async def test_azure_ai_chat_client_agent_basic_run_streaming() -> None: - """Test ChatAgent basic streaming functionality with AzureAIAgentClient.""" - async with ChatAgent( - chat_client=AzureAIAgentClient(credential=AzureCliCredential()), + """Test Agent basic streaming functionality with AzureAIAgentClient.""" + async with Agent( + client=AzureAIAgentClient(credential=AzureCliCredential()), ) as agent: # Run streaming query full_message: str = "" @@ -1546,9 +1517,9 @@ async def test_azure_ai_chat_client_agent_basic_run_streaming() -> None: @pytest.mark.flaky @skip_if_azure_ai_integration_tests_disabled async def test_azure_ai_chat_client_agent_thread_persistence() -> None: - """Test ChatAgent thread persistence across runs with AzureAIAgentClient.""" - async with ChatAgent( - chat_client=AzureAIAgentClient(credential=AzureCliCredential()), + """Test Agent thread persistence across runs with AzureAIAgentClient.""" + async with Agent( + client=AzureAIAgentClient(credential=AzureCliCredential()), instructions="You are a helpful assistant with good memory.", ) as agent: # Create a new thread that will be reused @@ -1572,9 +1543,9 @@ async def test_azure_ai_chat_client_agent_thread_persistence() -> None: @pytest.mark.flaky @skip_if_azure_ai_integration_tests_disabled async def test_azure_ai_chat_client_agent_existing_thread_id() -> None: - """Test ChatAgent existing thread ID functionality with AzureAIAgentClient.""" - async with ChatAgent( - chat_client=AzureAIAgentClient(credential=AzureCliCredential()), + """Test Agent existing thread ID functionality with AzureAIAgentClient.""" + async with Agent( + client=AzureAIAgentClient(credential=AzureCliCredential()), instructions="You are a helpful assistant with good memory.", ) as first_agent: # Start a conversation and get the thread ID @@ -1590,8 +1561,8 @@ async def test_azure_ai_chat_client_agent_existing_thread_id() -> None: assert existing_thread_id is not None # Now continue with the same thread ID in a new agent instance - async with ChatAgent( - chat_client=AzureAIAgentClient(thread_id=existing_thread_id, credential=AzureCliCredential()), + async with Agent( + client=AzureAIAgentClient(thread_id=existing_thread_id, credential=AzureCliCredential()), instructions="You are a helpful assistant with good memory.", ) as second_agent: # Create a thread with the existing ID @@ -1610,12 +1581,12 @@ async def test_azure_ai_chat_client_agent_existing_thread_id() -> None: @pytest.mark.flaky @skip_if_azure_ai_integration_tests_disabled async def test_azure_ai_chat_client_agent_code_interpreter(): - """Test ChatAgent with code interpreter through AzureAIAgentClient.""" + """Test Agent with code interpreter through AzureAIAgentClient.""" - async with ChatAgent( - chat_client=AzureAIAgentClient(credential=AzureCliCredential()), + async with Agent( + client=AzureAIAgentClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that can write and execute Python code.", - tools=[HostedCodeInterpreterTool()], + tools=[AzureAIAgentClient.get_code_interpreter_tool()], ) as agent: # Request code execution response = await agent.run("Write Python code to calculate the factorial of 5 and show the result.") @@ -1630,7 +1601,7 @@ async def test_azure_ai_chat_client_agent_code_interpreter(): @pytest.mark.flaky @skip_if_azure_ai_integration_tests_disabled async def test_azure_ai_chat_client_agent_file_search(): - """Test ChatAgent with file search through AzureAIAgentClient.""" + """Test Agent with file search through AzureAIAgentClient.""" client = AzureAIAgentClient(credential=AzureCliCredential()) file: FileInfo | None = None @@ -1645,12 +1616,10 @@ async def test_azure_ai_chat_client_agent_file_search(): ) # 2. Create file search tool with uploaded resources - file_search_tool = HostedFileSearchTool( - inputs=[Content.from_hosted_vector_store(vector_store_id=vector_store.id)] - ) + file_search_tool = AzureAIAgentClient.get_file_search_tool(vector_store_ids=[vector_store.id]) - async with ChatAgent( - chat_client=client, + async with Agent( + client=client, instructions="You are a helpful assistant that can search through uploaded employee files.", tools=[file_search_tool], ) as agent: @@ -1679,17 +1648,17 @@ async def test_azure_ai_chat_client_agent_file_search(): @skip_if_azure_ai_integration_tests_disabled async def test_azure_ai_chat_client_agent_hosted_mcp_tool() -> None: - """Integration test for HostedMCPTool with Azure AI Agent using Microsoft Learn MCP.""" + """Integration test for MCP tool with Azure AI Agent using Microsoft Learn MCP.""" - mcp_tool = HostedMCPTool( + mcp_tool = AzureAIAgentClient.get_mcp_tool( name="Microsoft Learn MCP", url="https://learn.microsoft.com/api/mcp", description="A Microsoft Learn MCP server for documentation questions", approval_mode="never_require", ) - async with ChatAgent( - chat_client=AzureAIAgentClient(credential=AzureCliCredential()), + async with Agent( + client=AzureAIAgentClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that can help with microsoft documentation questions.", tools=[mcp_tool], ) as agent: @@ -1715,8 +1684,8 @@ async def test_azure_ai_chat_client_agent_hosted_mcp_tool() -> None: @skip_if_azure_ai_integration_tests_disabled async def test_azure_ai_chat_client_agent_level_tool_persistence(): """Test that agent-level tools persist across multiple runs with AzureAIAgentClient.""" - async with ChatAgent( - chat_client=AzureAIAgentClient(credential=AzureCliCredential()), + async with Agent( + client=AzureAIAgentClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that uses available tools.", tools=[get_weather], ) as agent: @@ -1740,8 +1709,8 @@ async def test_azure_ai_chat_client_agent_level_tool_persistence(): @skip_if_azure_ai_integration_tests_disabled async def test_azure_ai_chat_client_agent_chat_options_run_level() -> None: """Test ChatOptions parameter coverage at run level.""" - async with ChatAgent( - chat_client=AzureAIAgentClient(credential=AzureCliCredential()), + async with Agent( + client=AzureAIAgentClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", ) as agent: response = await agent.run( @@ -1764,8 +1733,8 @@ async def test_azure_ai_chat_client_agent_chat_options_run_level() -> None: @skip_if_azure_ai_integration_tests_disabled async def test_azure_ai_chat_client_agent_chat_options_agent_level() -> None: """Test ChatOptions parameter coverage agent level.""" - async with ChatAgent( - chat_client=AzureAIAgentClient(credential=AzureCliCredential()), + async with Agent( + client=AzureAIAgentClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", tools=[get_weather], default_options={ @@ -1789,59 +1758,59 @@ async def test_azure_ai_chat_client_cleanup_agent_when_enabled_and_created( mock_agents_client: MagicMock, ) -> None: """Test that agent is cleaned up when should_cleanup_agent=True and agent was created by client.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id=None, should_cleanup_agent=True) + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id=None, should_cleanup_agent=True) # Simulate agent creation - chat_client.agent_id = "created-agent-id" - chat_client._agent_created = True # type: ignore + client.agent_id = "created-agent-id" + client._agent_created = True # type: ignore - await chat_client._cleanup_agent_if_needed() # type: ignore + await client._cleanup_agent_if_needed() # type: ignore # Verify agent was deleted mock_agents_client.delete_agent.assert_called_once_with("created-agent-id") - assert chat_client.agent_id is None - assert chat_client._agent_created is False # type: ignore + assert client.agent_id is None + assert client._agent_created is False # type: ignore async def test_azure_ai_chat_client_no_cleanup_when_disabled( mock_agents_client: MagicMock, ) -> None: """Test that agent is not cleaned up when should_cleanup_agent=False.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id=None, should_cleanup_agent=False) + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id=None, should_cleanup_agent=False) # Simulate agent creation - chat_client.agent_id = "created-agent-id" - chat_client._agent_created = True + client.agent_id = "created-agent-id" + client._agent_created = True - await chat_client._cleanup_agent_if_needed() # type: ignore + await client._cleanup_agent_if_needed() # type: ignore # Verify agent was NOT deleted mock_agents_client.delete_agent.assert_not_called() - assert chat_client.agent_id == "created-agent-id" - assert chat_client._agent_created is True + assert client.agent_id == "created-agent-id" + assert client._agent_created is True async def test_azure_ai_chat_client_no_cleanup_when_agent_not_created_by_client( mock_agents_client: MagicMock, ) -> None: """Test that agent is not cleaned up when it was not created by this client instance.""" - chat_client = create_test_azure_ai_chat_client( + client = create_test_azure_ai_chat_client( mock_agents_client, agent_id="existing-agent-id", should_cleanup_agent=True ) # Agent exists but was not created by this client (_agent_created = False) - assert chat_client._agent_created is False # type: ignore + assert client._agent_created is False # type: ignore - await chat_client._cleanup_agent_if_needed() # type: ignore + await client._cleanup_agent_if_needed() # type: ignore # Verify agent was NOT deleted mock_agents_client.delete_agent.assert_not_called() - assert chat_client.agent_id == "existing-agent-id" + assert client.agent_id == "existing-agent-id" def test_azure_ai_chat_client_capture_azure_search_tool_calls(mock_agents_client: MagicMock) -> None: """Test _capture_azure_search_tool_calls method.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) # Mock Azure AI Search tool call mock_tool_call = MagicMock() @@ -1855,7 +1824,7 @@ def test_azure_ai_chat_client_capture_azure_search_tool_calls(mock_agents_client # Call the method with a list to capture tool calls azure_search_tool_calls: list[dict[str, Any]] = [] - chat_client._capture_azure_search_tool_calls(mock_step_data, azure_search_tool_calls) # type: ignore + client._capture_azure_search_tool_calls(mock_step_data, azure_search_tool_calls) # type: ignore # Verify tool call was captured assert len(azure_search_tool_calls) == 1 @@ -1869,10 +1838,10 @@ def test_azure_ai_chat_client_get_real_url_from_citation_reference_no_tool_calls mock_agents_client: MagicMock, ) -> None: """Test _get_real_url_from_citation_reference with no tool calls.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) # No tool calls - pass empty list - result = chat_client._get_real_url_from_citation_reference("doc_1", []) # type: ignore + result = client._get_real_url_from_citation_reference("doc_1", []) # type: ignore assert result == "doc_1" @@ -1880,51 +1849,51 @@ def test_azure_ai_chat_client_get_real_url_from_citation_reference_invalid_outpu mock_agents_client: MagicMock, ) -> None: """Test _get_real_url_from_citation_reference with invalid output format.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) # Tool call with invalid output format azure_search_tool_calls = [ {"id": "call_123", "type": "azure_ai_search", "azure_ai_search": {"output": "invalid_json_format"}} ] - result = chat_client._get_real_url_from_citation_reference("doc_1", azure_search_tool_calls) # type: ignore + result = client._get_real_url_from_citation_reference("doc_1", azure_search_tool_calls) # type: ignore assert result == "doc_1" async def test_azure_ai_chat_client_context_manager(mock_agents_client: MagicMock) -> None: """Test AzureAIAgentClient as async context manager.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) # Mock close method to avoid actual cleanup - chat_client.close = AsyncMock() + client.close = AsyncMock() - async with chat_client as client: - assert client is chat_client + async with client as client: + assert client is client # Verify close was called on exit - chat_client.close.assert_called_once() + client.close.assert_called_once() async def test_azure_ai_chat_client_close_method(mock_agents_client: MagicMock) -> None: """Test AzureAIAgentClient close method.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) # Mock cleanup methods - chat_client._cleanup_agent_if_needed = AsyncMock() - chat_client._close_client_if_needed = AsyncMock() + client._cleanup_agent_if_needed = AsyncMock() + client._close_client_if_needed = AsyncMock() - await chat_client.close() + await client.close() # Verify cleanup methods were called - chat_client._cleanup_agent_if_needed.assert_called_once() - chat_client._close_client_if_needed.assert_called_once() + client._cleanup_agent_if_needed.assert_called_once() + client._close_client_if_needed.assert_called_once() def test_azure_ai_chat_client_extract_url_citations_with_azure_search_enhanced_url( mock_agents_client: MagicMock, ) -> None: """Test _extract_url_citations with Azure AI Search URL enhancement.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) # Add Azure Search tool calls for URL enhancement azure_search_tool_calls = [ @@ -1961,7 +1930,7 @@ def test_azure_ai_chat_client_extract_url_citations_with_azure_search_enhanced_u mock_chunk = MagicMock(spec=MessageDeltaChunk) mock_chunk.delta = mock_delta - citations = chat_client._extract_url_citations(mock_chunk, azure_search_tool_calls) # type: ignore + citations = client._extract_url_citations(mock_chunk, azure_search_tool_calls) # type: ignore # Verify real URL was used assert len(citations) == 1 @@ -2006,7 +1975,7 @@ async def test_azure_ai_chat_client_prepare_options_with_mapping_response_format mock_agents_client: MagicMock, ) -> None: """Test _prepare_options with Mapping-based response_format (runtime JSON schema).""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) # Runtime JSON schema dict response_format_dict = { @@ -2019,7 +1988,7 @@ async def test_azure_ai_chat_client_prepare_options_with_mapping_response_format chat_options: ChatOptions = {"response_format": response_format_dict} # type: ignore[typeddict-item] - run_options, _ = await chat_client._prepare_options([], chat_options) # type: ignore + run_options, _ = await client._prepare_options([], chat_options) # type: ignore assert "response_format" in run_options # Should pass through as-is for Mapping types @@ -2030,20 +1999,20 @@ async def test_azure_ai_chat_client_prepare_options_with_invalid_response_format mock_agents_client: MagicMock, ) -> None: """Test _prepare_options with invalid response_format raises error.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) # Invalid response_format (not BaseModel or Mapping) chat_options: ChatOptions = {"response_format": "invalid_format"} # type: ignore[typeddict-item] with pytest.raises(ServiceInvalidRequestError, match="response_format must be a Pydantic BaseModel"): - await chat_client._prepare_options([], chat_options) # type: ignore + await client._prepare_options([], chat_options) # type: ignore async def test_azure_ai_chat_client_prepare_tool_definitions_with_agent_tool_resources( mock_agents_client: MagicMock, ) -> None: """Test _prepare_tool_definitions_and_resources copies tool_resources from agent definition.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Create mock agent definition with tool_resources mock_agent_definition = MagicMock() @@ -2053,7 +2022,7 @@ async def test_azure_ai_chat_client_prepare_tool_definitions_with_agent_tool_res run_options: dict[str, Any] = {} options: dict[str, Any] = {} - await chat_client._prepare_tool_definitions_and_resources(options, mock_agent_definition, run_options) # type: ignore + await client._prepare_tool_definitions_and_resources(options, mock_agent_definition, run_options) # type: ignore # Verify tool_resources was copied to run_options assert "tool_resources" in run_options @@ -2064,52 +2033,51 @@ def test_azure_ai_chat_client_prepare_mcp_resources_with_dict_approval_mode( mock_agents_client: MagicMock, ) -> None: """Test _prepare_mcp_resources with dict-based approval mode (always_require_approval).""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) - # MCP tool with dict-based approval mode - mcp_tool = HostedMCPTool( + # MCP tool with dict-based approval mode - use approval_mode parameter + mcp_tool = AzureAIAgentClient.get_mcp_tool( name="Test MCP", url="https://example.com/mcp", - approval_mode={"always_require_approval": {"tool1", "tool2"}}, + approval_mode={"always_require_approval": ["tool1", "tool2"]}, ) - result = chat_client._prepare_mcp_resources([mcp_tool]) # type: ignore + result = client._prepare_mcp_resources([mcp_tool]) # type: ignore assert len(result) == 1 assert result[0]["server_label"] == "Test_MCP" assert "require_approval" in result[0] - assert result[0]["require_approval"] == {"always": {"tool1", "tool2"}} def test_azure_ai_chat_client_prepare_mcp_resources_with_never_require_dict( mock_agents_client: MagicMock, ) -> None: """Test _prepare_mcp_resources with dict-based approval mode (never_require_approval).""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) - # MCP tool with never_require_approval dict - mcp_tool = HostedMCPTool( + # MCP tool with never require approval - use approval_mode parameter + mcp_tool = AzureAIAgentClient.get_mcp_tool( name="Test MCP", url="https://example.com/mcp", - approval_mode={"never_require_approval": {"safe_tool"}}, + approval_mode={"never_require_approval": ["safe_tool"]}, ) - result = chat_client._prepare_mcp_resources([mcp_tool]) # type: ignore + result = client._prepare_mcp_resources([mcp_tool]) # type: ignore assert len(result) == 1 - assert result[0]["require_approval"] == {"never": {"safe_tool"}} + assert "require_approval" in result[0] def test_azure_ai_chat_client_prepare_messages_with_function_result( mock_agents_client: MagicMock, ) -> None: """Test _prepare_messages extracts function_result content.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) function_result = Content.from_function_result(call_id='["run_123", "call_456"]', result="test result") - messages = [ChatMessage(role="user", contents=[function_result])] + messages = [Message(role="user", contents=[function_result])] - additional_messages, instructions, required_action_results = chat_client._prepare_messages(messages) # type: ignore + additional_messages, instructions, required_action_results = client._prepare_messages(messages) # type: ignore # function_result should be extracted, not added to additional_messages assert additional_messages is None @@ -2122,14 +2090,14 @@ def test_azure_ai_chat_client_prepare_messages_with_raw_content_block( mock_agents_client: MagicMock, ) -> None: """Test _prepare_messages handles raw MessageInputContentBlock in content.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client) + client = create_test_azure_ai_chat_client(mock_agents_client) # Create content with raw_representation that is a MessageInputContentBlock raw_block = MessageInputTextBlock(text="Raw block text") custom_content = Content(type="custom", raw_representation=raw_block) - messages = [ChatMessage(role="user", contents=[custom_content])] + messages = [Message(role="user", contents=[custom_content])] - additional_messages, instructions, required_action_results = chat_client._prepare_messages(messages) # type: ignore + additional_messages, instructions, required_action_results = client._prepare_messages(messages) # type: ignore assert additional_messages is not None assert len(additional_messages) == 1 @@ -2140,16 +2108,15 @@ def test_azure_ai_chat_client_prepare_messages_with_raw_content_block( async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_mcp_tool( mock_agents_client: MagicMock, ) -> None: - """Test _prepare_tools_for_azure_ai with HostedMCPTool.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + """Test _prepare_tools_for_azure_ai with MCP dict tool.""" + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") - mcp_tool = HostedMCPTool( + mcp_tool = AzureAIAgentClient.get_mcp_tool( name="Test MCP Server", url="https://example.com/mcp", - allowed_tools=["tool1", "tool2"], ) - tool_definitions = await chat_client._prepare_tools_for_azure_ai([mcp_tool]) # type: ignore + tool_definitions = await client._prepare_tools_for_azure_ai([mcp_tool]) # type: ignore assert len(tool_definitions) >= 1 # The McpTool.definitions property returns the tool definitions @@ -2162,12 +2129,12 @@ async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_tool_definition( mock_agents_client: MagicMock, ) -> None: """Test _prepare_tools_for_azure_ai with ToolDefinition passthrough.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Pass a ToolDefinition directly - should be passed through as-is tool_def = CodeInterpreterToolDefinition() - tool_definitions = await chat_client._prepare_tools_for_azure_ai([tool_def]) # type: ignore + tool_definitions = await client._prepare_tools_for_azure_ai([tool_def]) # type: ignore assert len(tool_definitions) == 1 assert tool_definitions[0] is tool_def @@ -2177,12 +2144,12 @@ async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_dict_passthrough( mock_agents_client: MagicMock, ) -> None: """Test _prepare_tools_for_azure_ai with dict passthrough.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") # Pass a dict tool definition - should be passed through as-is dict_tool = {"type": "function", "function": {"name": "test_func", "parameters": {}}} - tool_definitions = await chat_client._prepare_tools_for_azure_ai([dict_tool]) # type: ignore + tool_definitions = await client._prepare_tools_for_azure_ai([dict_tool]) # type: ignore assert len(tool_definitions) == 1 assert tool_definitions[0] is dict_tool @@ -2191,14 +2158,16 @@ async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_dict_passthrough( async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_unsupported_type( mock_agents_client: MagicMock, ) -> None: - """Test _prepare_tools_for_azure_ai raises error for unsupported tool type.""" - chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + """Test _prepare_tools_for_azure_ai passes through unsupported tool types.""" + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") - # Pass an unsupported tool type + # Pass an unsupported tool type - it should be passed through unchanged class UnsupportedTool: pass unsupported_tool = UnsupportedTool() - with pytest.raises(ServiceInitializationError, match="Unsupported tool type"): - await chat_client._prepare_tools_for_azure_ai([unsupported_tool]) # type: ignore + # Unsupported tools are now passed through unchanged (server will reject if invalid) + tool_definitions = await client._prepare_tools_for_azure_ai([unsupported_tool]) # type: ignore + assert len(tool_definitions) == 1 + assert tool_definitions[0] is unsupported_tool diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index 38ccfb5ad3..b4e82cbefd 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -11,17 +11,13 @@ from uuid import uuid4 import pytest from agent_framework import ( + Agent, AgentResponse, - ChatAgent, - ChatClientProtocol, - ChatMessage, ChatOptions, ChatResponse, Content, - HostedCodeInterpreterTool, - HostedFileSearchTool, - HostedMCPTool, - HostedWebSearchTool, + Message, + SupportsChatGetResponse, tool, ) from agent_framework.exceptions import ServiceInitializationError @@ -31,6 +27,7 @@ from azure.ai.projects.models import ( CodeInterpreterTool, CodeInterpreterToolAuto, FileSearchTool, + ImageGenTool, MCPTool, ResponseTextFormatConfigurationJsonSchema, WebSearchPreviewTool, @@ -88,19 +85,19 @@ async def temporary_chat_client(agent_name: str) -> AsyncIterator[AzureAIClient] """Async context manager that creates an Azure AI agent and yields an `AzureAIClient`. The underlying agent version is cleaned up automatically after use. - Tests can construct their own `ChatAgent` instances from the yielded client. + Tests can construct their own `Agent` instances from the yielded client. """ endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] async with ( AzureCliCredential() as credential, AIProjectClient(endpoint=endpoint, credential=credential) as project_client, ): - chat_client = AzureAIClient( + client = AzureAIClient( project_client=project_client, agent_name=agent_name, ) try: - yield chat_client + yield client finally: await project_client.agents.delete(agent_name=agent_name) @@ -179,7 +176,7 @@ def test_init_with_project_client(mock_project_client: MagicMock) -> None: assert client.agent_name == "test-agent" assert client.agent_version == "1.0" assert not client._should_close_client # type: ignore - assert isinstance(client, ChatClientProtocol) + assert isinstance(client, SupportsChatGetResponse) def test_init_auto_create_client( @@ -298,9 +295,9 @@ async def test_prepare_messages_for_azure_ai_with_system_messages( client = create_test_azure_ai_client(mock_project_client) messages = [ - ChatMessage(role="system", contents=[Content.from_text(text="You are a helpful assistant.")]), - ChatMessage(role="user", contents=[Content.from_text(text="Hello")]), - ChatMessage(role="assistant", contents=[Content.from_text(text="System response")]), + Message(role="system", contents=[Content.from_text(text="You are a helpful assistant.")]), + Message(role="user", contents=[Content.from_text(text="Hello")]), + Message(role="assistant", contents=[Content.from_text(text="System response")]), ] result_messages, instructions = client._prepare_messages_for_azure_ai(messages) # type: ignore @@ -318,8 +315,8 @@ async def test_prepare_messages_for_azure_ai_no_system_messages( client = create_test_azure_ai_client(mock_project_client) messages = [ - ChatMessage(role="user", contents=[Content.from_text(text="Hello")]), - ChatMessage(role="assistant", contents=[Content.from_text(text="Hi there!")]), + Message(role="user", contents=[Content.from_text(text="Hello")]), + Message(role="assistant", contents=[Content.from_text(text="Hi there!")]), ] result_messages, instructions = client._prepare_messages_for_azure_ai(messages) # type: ignore @@ -419,7 +416,7 @@ async def test_prepare_options_basic(mock_project_client: MagicMock) -> None: """Test prepare_options basic functionality.""" client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0") - messages = [ChatMessage(role="user", contents=[Content.from_text(text="Hello")])] + messages = [Message(role="user", contents=[Content.from_text(text="Hello")])] with ( patch( @@ -456,7 +453,7 @@ async def test_prepare_options_with_application_endpoint( agent_version="1", ) - messages = [ChatMessage(role="user", contents=[Content.from_text(text="Hello")])] + messages = [Message(role="user", contents=[Content.from_text(text="Hello")])] with ( patch( @@ -498,7 +495,7 @@ async def test_prepare_options_with_application_project_client( agent_version="1", ) - messages = [ChatMessage(role="user", contents=[Content.from_text(text="Hello")])] + messages = [Message(role="user", contents=[Content.from_text(text="Hello")])] with ( patch( @@ -977,7 +974,7 @@ async def test_prepare_options_excludes_response_format( """Test that prepare_options excludes response_format, text, and text_format from final run options.""" client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0") - messages = [ChatMessage(role="user", contents=[Content.from_text(text="Hello")])] + messages = [Message(role="user", contents=[Content.from_text(text="Hello")])] chat_options: ChatOptions = {} with ( @@ -1100,178 +1097,50 @@ def test_get_conversation_id_with_parsed_response_no_conversation() -> None: assert result == "resp_parsed_12345" -def test_prepare_mcp_tool_basic() -> None: - """Test _prepare_mcp_tool with basic HostedMCPTool.""" - mcp_tool = HostedMCPTool( - name="Test MCP Server", - url="https://example.com/mcp", - ) - - result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore - - assert result["server_label"] == "Test_MCP_Server" - assert result["server_url"] == "https://example.com/mcp" +# region MCP Tool Dict Tests +# These tests verify that dict-based MCP tools are processed correctly by from_azure_ai_tools -def test_prepare_mcp_tool_with_description() -> None: - """Test _prepare_mcp_tool with description.""" - mcp_tool = HostedMCPTool( - name="Test MCP", - url="https://example.com/mcp", - description="A test MCP server", - ) - - result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore - - assert result["server_description"] == "A test MCP server" - - -def test_prepare_mcp_tool_with_project_connection_id() -> None: - """Test _prepare_mcp_tool with project_connection_id in additional_properties.""" - mcp_tool = HostedMCPTool( - name="Test MCP", - url="https://example.com/mcp", - additional_properties={"project_connection_id": "conn-123"}, - ) - - result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore - - assert result["project_connection_id"] == "conn-123" - assert "headers" not in result # headers should not be set when project_connection_id is present - - -def test_prepare_mcp_tool_with_headers() -> None: - """Test _prepare_mcp_tool with headers (no project_connection_id).""" - mcp_tool = HostedMCPTool( - name="Test MCP", - url="https://example.com/mcp", - headers={"Authorization": "Bearer token123"}, - ) - - result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore - - assert result["headers"] == {"Authorization": "Bearer token123"} - - -def test_prepare_mcp_tool_with_allowed_tools() -> None: - """Test _prepare_mcp_tool with allowed_tools.""" - mcp_tool = HostedMCPTool( - name="Test MCP", - url="https://example.com/mcp", - allowed_tools=["tool1", "tool2"], - ) - - result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore - - assert set(result["allowed_tools"]) == {"tool1", "tool2"} - - -def test_prepare_mcp_tool_with_approval_mode_always_require() -> None: - """Test _prepare_mcp_tool with string approval_mode 'always_require'.""" - mcp_tool = HostedMCPTool( - name="Test MCP", - url="https://example.com/mcp", - approval_mode="always_require", - ) - - result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore - - assert result["require_approval"] == "always" - - -def test_prepare_mcp_tool_with_approval_mode_never_require() -> None: - """Test _prepare_mcp_tool with string approval_mode 'never_require'.""" - mcp_tool = HostedMCPTool( - name="Test MCP", - url="https://example.com/mcp", - approval_mode="never_require", - ) - - result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore - - assert result["require_approval"] == "never" - - -def test_prepare_mcp_tool_with_dict_approval_mode_always() -> None: - """Test _prepare_mcp_tool with dict approval_mode containing always_require_approval.""" - mcp_tool = HostedMCPTool( - name="Test MCP", - url="https://example.com/mcp", - approval_mode={"always_require_approval": {"dangerous_tool", "risky_tool"}}, - ) - - result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore - - assert "require_approval" in result - assert "always" in result["require_approval"] - assert set(result["require_approval"]["always"]["tool_names"]) == {"dangerous_tool", "risky_tool"} - - -def test_prepare_mcp_tool_with_dict_approval_mode_never() -> None: - """Test _prepare_mcp_tool with dict approval_mode containing never_require_approval.""" - mcp_tool = HostedMCPTool( - name="Test MCP", - url="https://example.com/mcp", - approval_mode={"never_require_approval": {"safe_tool"}}, - ) - - result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore - - assert "require_approval" in result - assert "never" in result["require_approval"] - assert set(result["require_approval"]["never"]["tool_names"]) == {"safe_tool"} - - -def test_from_azure_ai_tools() -> None: - """Test from_azure_ai_tools.""" - # Test MCP tool +def test_from_azure_ai_tools_mcp() -> None: + """Test from_azure_ai_tools with MCP tool.""" mcp_tool = MCPTool(server_label="test_server", server_url="http://localhost:8080") parsed_tools = from_azure_ai_tools([mcp_tool]) assert len(parsed_tools) == 1 - assert isinstance(parsed_tools[0], HostedMCPTool) - assert parsed_tools[0].name == "test server" - assert str(parsed_tools[0].url).rstrip("/") == "http://localhost:8080" + assert parsed_tools[0]["type"] == "mcp" + assert parsed_tools[0]["server_label"] == "test_server" + assert parsed_tools[0]["server_url"] == "http://localhost:8080" - # Test Code Interpreter tool + +def test_from_azure_ai_tools_code_interpreter() -> None: + """Test from_azure_ai_tools with Code Interpreter tool.""" ci_tool = CodeInterpreterTool(container=CodeInterpreterToolAuto(file_ids=["file-1"])) parsed_tools = from_azure_ai_tools([ci_tool]) assert len(parsed_tools) == 1 - assert isinstance(parsed_tools[0], HostedCodeInterpreterTool) - assert parsed_tools[0].inputs is not None - assert len(parsed_tools[0].inputs) == 1 + assert parsed_tools[0]["type"] == "code_interpreter" - tool_input = parsed_tools[0].inputs[0] - assert tool_input and tool_input.type == "hosted_file" and tool_input.file_id == "file-1" - - # Test File Search tool +def test_from_azure_ai_tools_file_search() -> None: + """Test from_azure_ai_tools with File Search tool.""" fs_tool = FileSearchTool(vector_store_ids=["vs-1"], max_num_results=5) parsed_tools = from_azure_ai_tools([fs_tool]) assert len(parsed_tools) == 1 - assert isinstance(parsed_tools[0], HostedFileSearchTool) - assert parsed_tools[0].inputs is not None - assert len(parsed_tools[0].inputs) == 1 + assert parsed_tools[0]["type"] == "file_search" + assert parsed_tools[0]["vector_store_ids"] == ["vs-1"] + assert parsed_tools[0]["max_num_results"] == 5 - tool_input = parsed_tools[0].inputs[0] - assert tool_input and tool_input.type == "hosted_vector_store" and tool_input.vector_store_id == "vs-1" - assert parsed_tools[0].max_results == 5 - - # Test Web Search tool +def test_from_azure_ai_tools_web_search() -> None: + """Test from_azure_ai_tools with Web Search tool.""" ws_tool = WebSearchPreviewTool( user_location=ApproximateLocation(city="Seattle", country="US", region="WA", timezone="PST") ) parsed_tools = from_azure_ai_tools([ws_tool]) assert len(parsed_tools) == 1 - assert isinstance(parsed_tools[0], HostedWebSearchTool) - assert parsed_tools[0].additional_properties + assert parsed_tools[0]["type"] == "web_search_preview" + assert parsed_tools[0]["user_location"]["city"] == "Seattle" - user_location = parsed_tools[0].additional_properties["user_location"] - assert user_location["city"] == "Seattle" - assert user_location["country"] == "US" - assert user_location["region"] == "WA" - assert user_location["timezone"] == "PST" +# endregion # region Integration Tests @@ -1363,10 +1232,10 @@ async def test_integration_options( # Prepare test message if option_name.startswith("tool_choice"): # Use weather-related prompt for tool tests - messages = [ChatMessage(role="user", text="What is the weather in Seattle?")] + messages = [Message(role="user", text="What is the weather in Seattle?")] else: # Generic prompt for simple options - messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")] + messages = [Message(role="user", text="Say 'Hello World' briefly.")] # Build options dict options: dict[str, Any] = {option_name: option_value, "tools": [get_weather]} @@ -1480,11 +1349,11 @@ async def test_integration_agent_options( # Prepare test message if option_name.startswith("response_format"): # Use prompt that works well with structured output - messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")] - messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) + messages = [Message(role="user", text="The weather in Seattle is sunny")] + messages.append(Message(role="user", text="What is the weather in Seattle?")) else: # Generic prompt for simple options - messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")] + messages = [Message(role="user", text="Say 'Hello World' briefly.")] # Build options dict options = {option_name: option_value} @@ -1535,7 +1404,7 @@ async def test_integration_web_search() -> None: "messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.", "options": { "tool_choice": "auto", - "tools": [HostedWebSearchTool()], + "tools": [client.get_web_search_tool()], }, } if streaming: @@ -1550,17 +1419,11 @@ async def test_integration_web_search() -> None: assert "Zoey" in response.text # Test that the client will use the web search tool with location - additional_properties = { - "user_location": { - "country": "US", - "city": "Seattle", - } - } content = { "messages": "What is the current weather? Do not ask for my current location.", "options": { "tool_choice": "auto", - "tools": [HostedWebSearchTool(additional_properties=additional_properties)], + "tools": [client.get_web_search_tool(user_location={"country": "US", "city": "Seattle"})], }, } if streaming: @@ -1573,14 +1436,14 @@ async def test_integration_web_search() -> None: @pytest.mark.flaky @skip_if_azure_ai_integration_tests_disabled async def test_integration_agent_hosted_mcp_tool() -> None: - """Integration test for HostedMCPTool with Azure Response Agent using Microsoft Learn MCP.""" + """Integration test for MCP tool with Azure Response Agent using Microsoft Learn MCP.""" async with temporary_chat_client(agent_name="af-int-test-mcp") as client: response = await client.get_response( "How to create an Azure storage account using az cli?", options={ # this needs to be high enough to handle the full MCP tool response. "max_tokens": 5000, - "tools": HostedMCPTool( + "tools": client.get_mcp_tool( name="Microsoft Learn MCP", url="https://learn.microsoft.com/api/mcp", description="A Microsoft Learn MCP server for documentation questions", @@ -1597,12 +1460,12 @@ async def test_integration_agent_hosted_mcp_tool() -> None: @pytest.mark.flaky @skip_if_azure_ai_integration_tests_disabled async def test_integration_agent_hosted_code_interpreter_tool(): - """Test Azure Responses Client agent with HostedCodeInterpreterTool through AzureAIClient.""" + """Test Azure Responses Client agent with code interpreter tool through AzureAIClient.""" async with temporary_chat_client(agent_name="af-int-test-code-interpreter") as client: response = await client.get_response( "Calculate the sum of numbers from 1 to 10 using Python code.", options={ - "tools": [HostedCodeInterpreterTool()], + "tools": [client.get_code_interpreter_tool()], }, ) # Should contain calculation result (sum of 1-10 = 55) or code execution content @@ -1621,8 +1484,8 @@ async def test_integration_agent_existing_thread(): async with ( temporary_chat_client(agent_name="af-int-test-existing-thread") as client, - ChatAgent( - chat_client=client, + Agent( + client=client, instructions="You are a helpful assistant with good memory.", ) as first_agent, ): @@ -1640,8 +1503,8 @@ async def test_integration_agent_existing_thread(): if preserved_thread: async with ( temporary_chat_client(agent_name="af-int-test-existing-thread-2") as client, - ChatAgent( - chat_client=client, + Agent( + client=client, instructions="You are a helpful assistant with good memory.", ) as second_agent, ): @@ -1651,3 +1514,115 @@ async def test_integration_agent_existing_thread(): assert isinstance(second_response, AgentResponse) assert second_response.text is not None assert "photography" in second_response.text.lower() + + +# region Factory Method Tests + + +def test_get_code_interpreter_tool_basic() -> None: + """Test get_code_interpreter_tool returns CodeInterpreterTool.""" + tool = AzureAIClient.get_code_interpreter_tool() + assert isinstance(tool, CodeInterpreterTool) + + +def test_get_code_interpreter_tool_with_file_ids() -> None: + """Test get_code_interpreter_tool with file_ids.""" + tool = AzureAIClient.get_code_interpreter_tool(file_ids=["file-123", "file-456"]) + assert isinstance(tool, CodeInterpreterTool) + assert tool["container"]["file_ids"] == ["file-123", "file-456"] + + +def test_get_file_search_tool_basic() -> None: + """Test get_file_search_tool returns FileSearchTool.""" + tool = AzureAIClient.get_file_search_tool(vector_store_ids=["vs-123"]) + assert isinstance(tool, FileSearchTool) + assert tool["vector_store_ids"] == ["vs-123"] + + +def test_get_file_search_tool_with_options() -> None: + """Test get_file_search_tool with max_num_results.""" + tool = AzureAIClient.get_file_search_tool( + vector_store_ids=["vs-123"], + max_num_results=10, + ) + assert isinstance(tool, FileSearchTool) + assert tool["max_num_results"] == 10 + + +def test_get_file_search_tool_requires_vector_store_ids() -> None: + """Test get_file_search_tool raises ValueError when vector_store_ids is empty.""" + with pytest.raises(ValueError, match="vector_store_ids"): + AzureAIClient.get_file_search_tool(vector_store_ids=[]) + + +def test_get_web_search_tool_basic() -> None: + """Test get_web_search_tool returns WebSearchPreviewTool.""" + tool = AzureAIClient.get_web_search_tool() + assert isinstance(tool, WebSearchPreviewTool) + + +def test_get_web_search_tool_with_location() -> None: + """Test get_web_search_tool with user_location.""" + tool = AzureAIClient.get_web_search_tool( + user_location={"city": "Seattle", "country": "US"}, + ) + assert isinstance(tool, WebSearchPreviewTool) + assert tool.user_location is not None + assert tool.user_location.city == "Seattle" + assert tool.user_location.country == "US" + + +def test_get_web_search_tool_with_search_context_size() -> None: + """Test get_web_search_tool with search_context_size.""" + tool = AzureAIClient.get_web_search_tool(search_context_size="high") + assert isinstance(tool, WebSearchPreviewTool) + assert tool.search_context_size == "high" + + +def test_get_mcp_tool_basic() -> None: + """Test get_mcp_tool returns MCPTool.""" + tool = AzureAIClient.get_mcp_tool(name="test_mcp", url="https://example.com") + assert isinstance(tool, MCPTool) + assert tool["server_label"] == "test_mcp" + assert tool["server_url"] == "https://example.com" + + +def test_get_mcp_tool_with_description() -> None: + """Test get_mcp_tool with description.""" + tool = AzureAIClient.get_mcp_tool( + name="test_mcp", + url="https://example.com", + description="Test MCP server", + ) + assert tool["server_description"] == "Test MCP server" + + +def test_get_mcp_tool_with_project_connection_id() -> None: + """Test get_mcp_tool with project_connection_id.""" + tool = AzureAIClient.get_mcp_tool( + name="test_mcp", + project_connection_id="conn-123", + ) + assert tool["project_connection_id"] == "conn-123" + + +def test_get_image_generation_tool_basic() -> None: + """Test get_image_generation_tool returns ImageGenTool.""" + tool = AzureAIClient.get_image_generation_tool() + assert isinstance(tool, ImageGenTool) + + +def test_get_image_generation_tool_with_options() -> None: + """Test get_image_generation_tool with various options.""" + tool = AzureAIClient.get_image_generation_tool( + size="1024x1024", + quality="high", + output_format="png", + ) + assert isinstance(tool, ImageGenTool) + assert tool["size"] == "1024x1024" + assert tool["quality"] == "high" + assert tool["output_format"] == "png" + + +# endregion diff --git a/python/packages/azure-ai/tests/test_provider.py b/python/packages/azure-ai/tests/test_provider.py index c209d14fd6..8d6cb1a29a 100644 --- a/python/packages/azure-ai/tests/test_provider.py +++ b/python/packages/azure-ai/tests/test_provider.py @@ -4,7 +4,7 @@ import os from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import ChatAgent, FunctionTool +from agent_framework import Agent, FunctionTool from agent_framework._mcp import MCPTool from agent_framework.exceptions import ServiceInitializationError from azure.ai.projects.aio import AIProjectClient @@ -158,7 +158,7 @@ async def test_provider_create_agent( description="Test Agent", ) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.name == "test-agent" mock_project_client.agents.create_version.assert_called_once() @@ -192,7 +192,7 @@ async def test_provider_create_agent_with_env_model( # Call without model parameter - should use env var agent = await provider.create_agent(name="test-agent") - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) # Verify the model from env var was used call_args = mock_project_client.agents.create_version.call_args assert call_args[1]["definition"].model == azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"] @@ -322,7 +322,7 @@ async def test_provider_get_agent_with_name(mock_project_client: MagicMock) -> N agent = await provider.get_agent(name="test-agent") - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.name == "test-agent" mock_project_client.agents.get.assert_called_with(agent_name="test-agent") @@ -350,7 +350,7 @@ async def test_provider_get_agent_with_reference(mock_project_client: MagicMock) agent_reference = AgentReference(name="test-agent", version="1.0") agent = await provider.get_agent(reference=agent_reference) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.name == "test-agent" mock_project_client.agents.get_version.assert_called_with(agent_name="test-agent", agent_version="1.0") @@ -410,7 +410,7 @@ def test_provider_as_agent(mock_project_client: MagicMock) -> None: with patch("agent_framework_azure_ai._project_provider.AzureAIClient") as mock_azure_ai_client: agent = provider.as_agent(mock_agent_version) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.name == "test-agent" assert agent.description == "Test Agent" @@ -440,19 +440,17 @@ def test_provider_merge_tools_skips_function_tool_dicts(mock_project_client: Mag # Call _merge_tools with user-provided function implementation merged = provider._merge_tools(definition_tools, [mock_ai_function]) # type: ignore - # Should have 2 items: the converted HostedMCPTool and the user-provided FunctionTool + # Should have 2 items: the converted MCP dict and the user-provided FunctionTool assert len(merged) == 2 # Check that the function tool dict was NOT included (it was skipped) function_dicts = [t for t in merged if isinstance(t, dict) and t.get("type") == "function"] assert len(function_dicts) == 0 - # Check that the MCP tool was converted to HostedMCPTool - from agent_framework import HostedMCPTool - - mcp_tools = [t for t in merged if isinstance(t, HostedMCPTool)] + # Check that the MCP tool was converted to dict + mcp_tools = [t for t in merged if isinstance(t, dict) and t.get("type") == "mcp"] assert len(mcp_tools) == 1 - assert mcp_tools[0].name == "my mcp" # server_label with _ replaced by space + assert mcp_tools[0]["server_label"] == "my_mcp" # Check that the user-provided FunctionTool was included ai_functions = [t for t in merged if isinstance(t, FunctionTool)] @@ -709,7 +707,7 @@ async def test_provider_create_and_get_agent_integration() -> None: instructions="You are a helpful assistant. Always respond with 'Hello from provider!'", ) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.name == "ProviderTestAgent" # Run the agent diff --git a/python/packages/azure-ai/tests/test_shared.py b/python/packages/azure-ai/tests/test_shared.py index 1a0292287d..b6f097bf85 100644 --- a/python/packages/azure-ai/tests/test_shared.py +++ b/python/packages/azure-ai/tests/test_shared.py @@ -5,29 +5,26 @@ from unittest.mock import MagicMock, patch import pytest from agent_framework import ( - Content, FunctionTool, - HostedCodeInterpreterTool, - HostedFileSearchTool, - HostedImageGenerationTool, - HostedMCPTool, - HostedWebSearchTool, ) -from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError +from agent_framework.exceptions import ServiceInvalidRequestError from azure.ai.agents.models import CodeInterpreterToolDefinition from pydantic import BaseModel +from agent_framework_azure_ai import AzureAIAgentClient from agent_framework_azure_ai._shared import ( _convert_response_format, # type: ignore _convert_sdk_tool, # type: ignore _extract_project_connection_id, # type: ignore - _prepare_mcp_tool_for_azure_ai, # type: ignore create_text_format_config, from_azure_ai_agent_tools, from_azure_ai_tools, to_azure_ai_agent_tools, to_azure_ai_tools, ) +from agent_framework_azure_ai._shared import ( + _prepare_mcp_tool_dict_for_azure_ai as _prepare_mcp_tool_for_azure_ai, # type: ignore +) def test_extract_project_connection_id_direct() -> None: @@ -69,16 +66,15 @@ def test_to_azure_ai_agent_tools_function_tool() -> None: def test_to_azure_ai_agent_tools_code_interpreter() -> None: - """Test converting HostedCodeInterpreterTool.""" - tool = HostedCodeInterpreterTool() + """Test converting code_interpreter dict tool.""" + tool = AzureAIAgentClient.get_code_interpreter_tool() result = to_azure_ai_agent_tools([tool]) assert len(result) == 1 assert isinstance(result[0], CodeInterpreterToolDefinition) def test_to_azure_ai_agent_tools_web_search_missing_connection() -> None: - """Test HostedWebSearchTool raises without connection info.""" - tool = HostedWebSearchTool() + """Test web search tool raises without connection info.""" # Clear any environment variables that could provide connection info with patch.dict( os.environ, @@ -90,8 +86,9 @@ def test_to_azure_ai_agent_tools_web_search_missing_connection() -> None: for key in ["BING_CONNECTION_ID", "BING_CUSTOM_CONNECTION_ID", "BING_CUSTOM_INSTANCE_NAME"]: env_backup[key] = os.environ.pop(key, None) try: - with pytest.raises(ServiceInitializationError, match="Bing search tool requires"): - to_azure_ai_agent_tools([tool]) + # get_web_search_tool now raises ValueError when no connection info is available + with pytest.raises(ValueError, match="Azure AI Agents requires a Bing connection"): + AzureAIAgentClient.get_web_search_tool() finally: # Restore environment for key, value in env_backup.items(): @@ -107,13 +104,15 @@ def test_to_azure_ai_agent_tools_dict_passthrough() -> None: def test_to_azure_ai_agent_tools_unsupported_type() -> None: - """Test unsupported tool type raises error.""" + """Test unsupported tool type passes through unchanged.""" class UnsupportedTool: pass - with pytest.raises(ServiceInitializationError, match="Unsupported tool type"): - to_azure_ai_agent_tools([UnsupportedTool()]) # type: ignore + unsupported = UnsupportedTool() + result = to_azure_ai_agent_tools([unsupported]) # type: ignore + assert len(result) == 1 + assert result[0] is unsupported # Passed through unchanged def test_from_azure_ai_agent_tools_empty() -> None: @@ -127,7 +126,7 @@ def test_from_azure_ai_agent_tools_code_interpreter() -> None: tool = CodeInterpreterToolDefinition() result = from_azure_ai_agent_tools([tool]) assert len(result) == 1 - assert isinstance(result[0], HostedCodeInterpreterTool) + assert result[0] == {"type": "code_interpreter"} def test_convert_sdk_tool_code_interpreter() -> None: @@ -135,7 +134,7 @@ def test_convert_sdk_tool_code_interpreter() -> None: tool = MagicMock() tool.type = "code_interpreter" result = _convert_sdk_tool(tool) - assert isinstance(result, HostedCodeInterpreterTool) + assert result == {"type": "code_interpreter"} def test_convert_sdk_tool_function_returns_none() -> None: @@ -161,8 +160,8 @@ def test_convert_sdk_tool_file_search() -> None: tool.file_search = MagicMock() tool.file_search.vector_store_ids = ["vs-1", "vs-2"] result = _convert_sdk_tool(tool) - assert isinstance(result, HostedFileSearchTool) - assert len(result.inputs) == 2 # type: ignore + assert result["type"] == "file_search" + assert result["vector_store_ids"] == ["vs-1", "vs-2"] def test_convert_sdk_tool_bing_grounding() -> None: @@ -172,8 +171,8 @@ def test_convert_sdk_tool_bing_grounding() -> None: tool.bing_grounding = MagicMock() tool.bing_grounding.connection_id = "conn-123" result = _convert_sdk_tool(tool) - assert isinstance(result, HostedWebSearchTool) - assert result.additional_properties["connection_id"] == "conn-123" # type: ignore + assert result["type"] == "bing_grounding" + assert result["connection_id"] == "conn-123" def test_convert_sdk_tool_bing_custom_search() -> None: @@ -184,9 +183,9 @@ def test_convert_sdk_tool_bing_custom_search() -> None: tool.bing_custom_search.connection_id = "conn-123" tool.bing_custom_search.instance_name = "my-instance" result = _convert_sdk_tool(tool) - assert isinstance(result, HostedWebSearchTool) - assert result.additional_properties["custom_connection_id"] == "conn-123" # type: ignore - assert result.additional_properties["custom_instance_name"] == "my-instance" # type: ignore + assert result["type"] == "bing_custom_search" + assert result["connection_id"] == "conn-123" + assert result["instance_name"] == "my-instance" def test_to_azure_ai_tools_empty() -> None: @@ -196,14 +195,14 @@ def test_to_azure_ai_tools_empty() -> None: def test_to_azure_ai_tools_code_interpreter_with_file_ids() -> None: - """Test converting HostedCodeInterpreterTool with file inputs.""" - tool = HostedCodeInterpreterTool( - inputs=[Content.from_hosted_file(file_id="file-123")] # type: ignore - ) + """Test converting code_interpreter dict tool with file inputs.""" + tool = { + "type": "code_interpreter", + "file_ids": ["file-123"], + } result = to_azure_ai_tools([tool]) assert len(result) == 1 assert result[0]["type"] == "code_interpreter" - assert result[0]["container"]["file_ids"] == ["file-123"] def test_to_azure_ai_tools_function_tool() -> None: @@ -221,11 +220,12 @@ def test_to_azure_ai_tools_function_tool() -> None: def test_to_azure_ai_tools_file_search() -> None: - """Test converting HostedFileSearchTool.""" - tool = HostedFileSearchTool( - inputs=[Content.from_hosted_vector_store(vector_store_id="vs-123")], # type: ignore - max_results=10, - ) + """Test converting file_search dict tool.""" + tool = { + "type": "file_search", + "vector_store_ids": ["vs-123"], + "max_num_results": 10, + } result = to_azure_ai_tools([tool]) assert len(result) == 1 assert result[0]["type"] == "file_search" @@ -234,28 +234,29 @@ def test_to_azure_ai_tools_file_search() -> None: def test_to_azure_ai_tools_web_search_with_location() -> None: - """Test converting HostedWebSearchTool with user location.""" - tool = HostedWebSearchTool( - additional_properties={ - "user_location": { - "city": "Seattle", - "country": "US", - "region": "WA", - "timezone": "PST", - } - } - ) + """Test converting web_search dict tool with user location.""" + tool = { + "type": "web_search_preview", + "user_location": { + "city": "Seattle", + "country": "US", + "region": "WA", + "timezone": "PST", + }, + } result = to_azure_ai_tools([tool]) assert len(result) == 1 assert result[0]["type"] == "web_search_preview" def test_to_azure_ai_tools_image_generation() -> None: - """Test converting HostedImageGenerationTool.""" - tool = HostedImageGenerationTool( - options={"model_id": "gpt-image-1", "image_size": "1024x1024"}, - additional_properties={"quality": "high"}, - ) + """Test converting image_generation dict tool.""" + tool = { + "type": "image_generation", + "model": "gpt-image-1", + "size": "1024x1024", + "quality": "high", + } result = to_azure_ai_tools([tool]) assert len(result) == 1 assert result[0]["type"] == "image_generation" @@ -264,7 +265,7 @@ def test_to_azure_ai_tools_image_generation() -> None: def test_prepare_mcp_tool_basic() -> None: """Test basic MCP tool conversion.""" - tool = HostedMCPTool(name="my tool", url="http://localhost:8080") + tool = {"type": "mcp", "server_label": "my_tool", "server_url": "http://localhost:8080"} result = _prepare_mcp_tool_for_azure_ai(tool) assert result["server_label"] == "my_tool" assert "http://localhost:8080" in result["server_url"] @@ -272,26 +273,37 @@ def test_prepare_mcp_tool_basic() -> None: def test_prepare_mcp_tool_with_description() -> None: """Test MCP tool with description.""" - tool = HostedMCPTool(name="my tool", url="http://localhost:8080", description="My MCP server") + tool = { + "type": "mcp", + "server_label": "my_tool", + "server_url": "http://localhost:8080", + "server_description": "My MCP server", + } result = _prepare_mcp_tool_for_azure_ai(tool) assert result["server_description"] == "My MCP server" def test_prepare_mcp_tool_with_headers() -> None: """Test MCP tool with headers (no project_connection_id).""" - tool = HostedMCPTool(name="my tool", url="http://localhost:8080", headers={"X-Api-Key": "secret"}) + tool = { + "type": "mcp", + "server_label": "my_tool", + "server_url": "http://localhost:8080", + "headers": {"X-Api-Key": "secret"}, + } result = _prepare_mcp_tool_for_azure_ai(tool) assert result["headers"] == {"X-Api-Key": "secret"} def test_prepare_mcp_tool_project_connection_takes_precedence() -> None: """Test project_connection_id takes precedence over headers.""" - tool = HostedMCPTool( - name="my tool", - url="http://localhost:8080", - headers={"X-Api-Key": "secret"}, - additional_properties={"project_connection_id": "my-conn"}, - ) + tool = { + "type": "mcp", + "server_label": "my_tool", + "server_url": "http://localhost:8080", + "headers": {"X-Api-Key": "secret"}, + "project_connection_id": "my-conn", + } result = _prepare_mcp_tool_for_azure_ai(tool) assert result["project_connection_id"] == "my-conn" assert "headers" not in result @@ -299,30 +311,38 @@ def test_prepare_mcp_tool_project_connection_takes_precedence() -> None: def test_prepare_mcp_tool_approval_mode_always() -> None: """Test MCP tool with always_require approval mode.""" - tool = HostedMCPTool(name="my tool", url="http://localhost:8080", approval_mode="always_require") + tool = { + "type": "mcp", + "server_label": "my_tool", + "server_url": "http://localhost:8080", + "require_approval": "always", + } result = _prepare_mcp_tool_for_azure_ai(tool) assert result["require_approval"] == "always" def test_prepare_mcp_tool_approval_mode_never() -> None: """Test MCP tool with never_require approval mode.""" - tool = HostedMCPTool(name="my tool", url="http://localhost:8080", approval_mode="never_require") + tool = { + "type": "mcp", + "server_label": "my_tool", + "server_url": "http://localhost:8080", + "require_approval": "never", + } result = _prepare_mcp_tool_for_azure_ai(tool) assert result["require_approval"] == "never" def test_prepare_mcp_tool_approval_mode_dict() -> None: """Test MCP tool with dict approval mode.""" - tool = HostedMCPTool( - name="my tool", - url="http://localhost:8080", - approval_mode={ - "always_require_approval": {"sensitive_tool"}, - "never_require_approval": {"safe_tool"}, - }, - ) + tool = { + "type": "mcp", + "server_label": "my_tool", + "server_url": "http://localhost:8080", + "require_approval": {"always": {"tool_names": ["sensitive_tool", "dangerous_tool"]}}, + } result = _prepare_mcp_tool_for_azure_ai(tool) - # The last assignment wins in the current implementation + # The approval mode is passed through assert "require_approval" in result @@ -385,7 +405,7 @@ def test_convert_response_format_json_schema_missing_schema_raises() -> None: def test_from_azure_ai_tools_mcp_approval_mode_always() -> None: - """Test from_azure_ai_tools converts MCP require_approval='always' to approval_mode.""" + """Test from_azure_ai_tools converts MCP require_approval='always' to dict.""" tools = [ { "type": "mcp", @@ -396,12 +416,12 @@ def test_from_azure_ai_tools_mcp_approval_mode_always() -> None: ] result = from_azure_ai_tools(tools) assert len(result) == 1 - assert isinstance(result[0], HostedMCPTool) - assert result[0].approval_mode == "always_require" + assert result[0]["type"] == "mcp" + assert result[0]["require_approval"] == "always" def test_from_azure_ai_tools_mcp_approval_mode_never() -> None: - """Test from_azure_ai_tools converts MCP require_approval='never' to approval_mode.""" + """Test from_azure_ai_tools converts MCP require_approval='never' to dict.""" tools = [ { "type": "mcp", @@ -412,8 +432,8 @@ def test_from_azure_ai_tools_mcp_approval_mode_never() -> None: ] result = from_azure_ai_tools(tools) assert len(result) == 1 - assert isinstance(result[0], HostedMCPTool) - assert result[0].approval_mode == "never_require" + assert result[0]["type"] == "mcp" + assert result[0]["require_approval"] == "never" def test_from_azure_ai_tools_mcp_approval_mode_dict_always() -> None: @@ -428,8 +448,8 @@ def test_from_azure_ai_tools_mcp_approval_mode_dict_always() -> None: ] result = from_azure_ai_tools(tools) assert len(result) == 1 - assert isinstance(result[0], HostedMCPTool) - assert result[0].approval_mode == {"always_require_approval": {"sensitive_tool", "dangerous_tool"}} + assert result[0]["type"] == "mcp" + assert result[0]["require_approval"] == {"always": {"tool_names": ["sensitive_tool", "dangerous_tool"]}} def test_from_azure_ai_tools_mcp_approval_mode_dict_never() -> None: @@ -444,5 +464,5 @@ def test_from_azure_ai_tools_mcp_approval_mode_dict_never() -> None: ] result = from_azure_ai_tools(tools) assert len(result) == 1 - assert isinstance(result[0], HostedMCPTool) - assert result[0].approval_mode == {"never_require_approval": {"safe_tool"}} + assert result[0]["type"] == "mcp" + assert result[0]["require_approval"] == {"never": {"tool_names": ["safe_tool"]}} diff --git a/python/packages/azurefunctions/pyproject.toml b/python/packages/azurefunctions/pyproject.toml index 45b8bbdce9..438625dcea 100644 --- a/python/packages/azurefunctions/pyproject.toml +++ b/python/packages/azurefunctions/pyproject.toml @@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,7 +22,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "agent-framework-durabletask", "azure-functions", "azure-functions-durable", diff --git a/python/packages/azurefunctions/tests/test_app.py b/python/packages/azurefunctions/tests/test_app.py index 5a454e6217..cc68176c93 100644 --- a/python/packages/azurefunctions/tests/test_app.py +++ b/python/packages/azurefunctions/tests/test_app.py @@ -12,7 +12,7 @@ from unittest.mock import ANY, AsyncMock, Mock, patch import azure.durable_functions as df import azure.functions as func import pytest -from agent_framework import AgentResponse, ChatMessage +from agent_framework import AgentResponse, Message from agent_framework_durabletask import ( MIMETYPE_APPLICATION_JSON, MIMETYPE_TEXT_PLAIN, @@ -356,7 +356,7 @@ class TestAgentEntityOperations: """Test that entity can run agent operation.""" mock_agent = Mock() mock_agent.run = AsyncMock( - return_value=AgentResponse(messages=[ChatMessage(role="assistant", text="Test response")]) + return_value=AgentResponse(messages=[Message(role="assistant", text="Test response")]) ) entity = AgentEntity(mock_agent, state_provider=_InMemoryStateProvider(thread_id="test-conv-123")) @@ -373,9 +373,7 @@ class TestAgentEntityOperations: async def test_entity_stores_conversation_history(self) -> None: """Test that the entity stores conversation history.""" mock_agent = Mock() - mock_agent.run = AsyncMock( - return_value=AgentResponse(messages=[ChatMessage(role="assistant", text="Response 1")]) - ) + mock_agent.run = AsyncMock(return_value=AgentResponse(messages=[Message(role="assistant", text="Response 1")])) entity = AgentEntity(mock_agent, state_provider=_InMemoryStateProvider(thread_id="conv-1")) @@ -407,9 +405,7 @@ class TestAgentEntityOperations: async def test_entity_increments_message_count(self) -> None: """Test that the entity increments the message count.""" mock_agent = Mock() - mock_agent.run = AsyncMock( - return_value=AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) - ) + mock_agent.run = AsyncMock(return_value=AgentResponse(messages=[Message(role="assistant", text="Response")])) entity = AgentEntity(mock_agent, state_provider=_InMemoryStateProvider(thread_id="conv-1")) @@ -448,9 +444,7 @@ class TestAgentEntityFactory: def test_entity_function_handles_run_operation(self) -> None: """Test that the entity function handles the run operation.""" mock_agent = Mock() - mock_agent.run = AsyncMock( - return_value=AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) - ) + mock_agent.run = AsyncMock(return_value=AgentResponse(messages=[Message(role="assistant", text="Response")])) entity_function = create_agent_entity(mock_agent) @@ -475,9 +469,7 @@ class TestAgentEntityFactory: def test_entity_function_handles_run_agent_operation(self) -> None: """Test that the entity function handles the deprecated run_agent operation for backward compatibility.""" mock_agent = Mock() - mock_agent.run = AsyncMock( - return_value=AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) - ) + mock_agent.run = AsyncMock(return_value=AgentResponse(messages=[Message(role="assistant", text="Response")])) entity_function = create_agent_entity(mock_agent) diff --git a/python/packages/azurefunctions/tests/test_entities.py b/python/packages/azurefunctions/tests/test_entities.py index eb740daddb..2fdbc3463e 100644 --- a/python/packages/azurefunctions/tests/test_entities.py +++ b/python/packages/azurefunctions/tests/test_entities.py @@ -10,7 +10,7 @@ from typing import Any, TypeVar from unittest.mock import AsyncMock, Mock import pytest -from agent_framework import AgentResponse, ChatMessage +from agent_framework import AgentResponse, Message from agent_framework_azurefunctions._entities import create_agent_entity @@ -19,7 +19,7 @@ FuncT = TypeVar("FuncT", bound=Callable[..., Any]) def _agent_response(text: str | None) -> AgentResponse: """Create an AgentResponse with a single assistant message.""" - message = ChatMessage(role="assistant", text=text) if text is not None else ChatMessage(role="assistant", text="") + message = Message(role="assistant", text=text) if text is not None else Message(role="assistant", text="") return AgentResponse(messages=[message]) diff --git a/python/packages/azurefunctions/tests/test_orchestration.py b/python/packages/azurefunctions/tests/test_orchestration.py index 989d391e68..e778875887 100644 --- a/python/packages/azurefunctions/tests/test_orchestration.py +++ b/python/packages/azurefunctions/tests/test_orchestration.py @@ -6,7 +6,7 @@ from typing import Any from unittest.mock import Mock import pytest -from agent_framework import AgentResponse, ChatMessage +from agent_framework import AgentResponse, Message from agent_framework_durabletask import DurableAIAgent from azure.durable_functions.models.Task import TaskBase, TaskState @@ -136,7 +136,7 @@ class TestAgentResponseHelpers: # Simulate successful entity task completion entity_task.state = TaskState.SUCCEEDED - entity_task.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Test response")]).to_dict() + entity_task.result = AgentResponse(messages=[Message(role="assistant", text="Test response")]).to_dict() # Clear pending_tasks to simulate that parent has processed the child task.pending_tasks.clear() @@ -178,7 +178,7 @@ class TestAgentResponseHelpers: # Simulate successful entity task with JSON response entity_task.state = TaskState.SUCCEEDED - entity_task.result = AgentResponse(messages=[ChatMessage(role="assistant", text='{"answer": "42"}')]).to_dict() + entity_task.result = AgentResponse(messages=[Message(role="assistant", text='{"answer": "42"}')]).to_dict() # Clear pending_tasks to simulate that parent has processed the child task.pending_tasks.clear() diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index 1a6bf01d59..2b6deaf9bb 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -14,7 +14,6 @@ from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, BaseChatClient, ChatAndFunctionMiddlewareTypes, - ChatMessage, ChatMiddlewareLayer, ChatOptions, ChatResponse, @@ -24,8 +23,8 @@ from agent_framework import ( FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, + Message, ResponseStream, - ToolProtocol, UsageDetails, get_logger, prepare_function_call_results, @@ -325,7 +324,7 @@ class BedrockChatClient( def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], stream: bool = False, **kwargs: Any, @@ -359,7 +358,7 @@ class BedrockChatClient( def _prepare_options( self, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], **kwargs: Any, ) -> dict[str, Any]: @@ -410,7 +409,7 @@ class BedrockChatClient( return run_options def _prepare_bedrock_messages( - self, messages: Sequence[ChatMessage] + self, messages: Sequence[Message] ) -> tuple[list[dict[str, str]], list[dict[str, Any]]]: prompts: list[dict[str, str]] = [] conversation: list[dict[str, Any]] = [] @@ -482,7 +481,7 @@ class BedrockChatClient( return aligned_blocks - def _convert_message_to_content_blocks(self, message: ChatMessage) -> list[dict[str, Any]]: + def _convert_message_to_content_blocks(self, message: Message) -> list[dict[str, Any]]: blocks: list[dict[str, Any]] = [] for content in message.contents: block = self._convert_content_to_bedrock_block(content) @@ -564,7 +563,7 @@ class BedrockChatClient( return {"text": str(value)} return {"text": str(value)} - def _prepare_tools(self, tools: list[ToolProtocol | MutableMapping[str, Any]] | None) -> dict[str, Any] | None: + def _prepare_tools(self, tools: list[FunctionTool | MutableMapping[str, Any]] | None) -> dict[str, Any] | None: converted: list[dict[str, Any]] = [] if not tools: return None @@ -593,7 +592,7 @@ class BedrockChatClient( message = output.get("message", {}) content_blocks = message.get("content", []) or [] contents = self._parse_message_contents(content_blocks) - chat_message = ChatMessage(role="assistant", contents=contents, raw_representation=message) + chat_message = Message(role="assistant", contents=contents, raw_representation=message) usage_details = self._parse_usage(response.get("usage") or output.get("usage")) finish_reason = self._map_finish_reason(output.get("completionReason") or response.get("stopReason")) response_id = response.get("responseId") or message.get("id") diff --git a/python/packages/bedrock/pyproject.toml b/python/packages/bedrock/pyproject.toml index f424cbef6f..b6111cce8d 100644 --- a/python/packages/bedrock/pyproject.toml +++ b/python/packages/bedrock/pyproject.toml @@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "boto3>=1.35.0,<2.0.0", "botocore>=1.35.0,<2.0.0", ] diff --git a/python/packages/bedrock/samples/bedrock_sample.py b/python/packages/bedrock/samples/bedrock_sample.py index 15a347997d..188e6bf1da 100644 --- a/python/packages/bedrock/samples/bedrock_sample.py +++ b/python/packages/bedrock/samples/bedrock_sample.py @@ -3,7 +3,7 @@ import asyncio import logging -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework_bedrock import BedrockChatClient @@ -17,8 +17,8 @@ def get_weather(city: str) -> dict[str, str]: async def main() -> None: """Run the Bedrock sample agent, invoke the weather tool, and log the response.""" - agent = ChatAgent( - chat_client=BedrockChatClient(), + agent = Agent( + client=BedrockChatClient(), instructions="You are a concise travel assistant.", name="BedrockWeatherAgent", tool_choice="auto", diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py index d267691e71..ef896db9c3 100644 --- a/python/packages/bedrock/tests/test_bedrock_client.py +++ b/python/packages/bedrock/tests/test_bedrock_client.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing import Any import pytest -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework.exceptions import ServiceInitializationError from agent_framework_bedrock import BedrockChatClient @@ -41,8 +41,8 @@ async def test_get_response_invokes_bedrock_runtime() -> None: ) messages = [ - ChatMessage(role="system", contents=[Content.from_text(text="You are concise.")]), - ChatMessage(role="user", contents=[Content.from_text(text="hello")]), + Message(role="system", contents=[Content.from_text(text="You are concise.")]), + Message(role="user", contents=[Content.from_text(text="hello")]), ] response = await client.get_response(messages=messages, options={"max_tokens": 32}) @@ -62,7 +62,7 @@ def test_build_request_requires_non_system_messages() -> None: client=_StubBedrockRuntime(), ) - messages = [ChatMessage(role="system", contents=[Content.from_text(text="Only system text")])] + messages = [Message(role="system", contents=[Content.from_text(text="Only system text")])] with pytest.raises(ServiceInitializationError): client._prepare_options(messages, {}) diff --git a/python/packages/bedrock/tests/test_bedrock_settings.py b/python/packages/bedrock/tests/test_bedrock_settings.py index 25df37b11f..6a1956dd7c 100644 --- a/python/packages/bedrock/tests/test_bedrock_settings.py +++ b/python/packages/bedrock/tests/test_bedrock_settings.py @@ -6,10 +6,10 @@ from unittest.mock import MagicMock import pytest from agent_framework import ( - ChatMessage, ChatOptions, Content, FunctionTool, + Message, ) from pydantic import BaseModel @@ -46,7 +46,7 @@ def test_build_request_includes_tool_config() -> None: "tools": [tool], "tool_choice": {"mode": "required", "required_function_name": "get_weather"}, } - messages = [ChatMessage(role="user", contents=[Content.from_text(text="hi")])] + messages = [Message(role="user", contents=[Content.from_text(text="hi")])] request = client._prepare_options(messages, options) @@ -58,14 +58,14 @@ def test_build_request_serializes_tool_history() -> None: client = _build_client() options: ChatOptions = {} messages = [ - ChatMessage(role="user", contents=[Content.from_text(text="how's weather?")]), - ChatMessage( + Message(role="user", contents=[Content.from_text(text="how's weather?")]), + Message( role="assistant", contents=[ Content.from_function_call(call_id="call-1", name="get_weather", arguments='{"location": "SEA"}') ], ), - ChatMessage( + Message( role="tool", contents=[Content.from_function_result(call_id="call-1", result={"answer": "72F"})], ), diff --git a/python/packages/chatkit/README.md b/python/packages/chatkit/README.md index cc48016561..f52225c1d9 100644 --- a/python/packages/chatkit/README.md +++ b/python/packages/chatkit/README.md @@ -7,9 +7,9 @@ Specifically, it mirrors the [Agent SDK integration](https://github.com/openai/c - `stream_agent_response`: A helper to convert a streamed `AgentResponseUpdate` from a Microsoft Agent Framework agent that implements `SupportsAgentRun` to ChatKit events. - `ThreadItemConverter`: A extendable helper class to convert ChatKit thread items to - `ChatMessage` objects that can be consumed by an Agent Framework agent. + `Message` objects that can be consumed by an Agent Framework agent. - `simple_to_agent_input`: A helper function that uses the default implementation - of `ThreadItemConverter` to convert a ChatKit thread to a list of `ChatMessage`, + of `ThreadItemConverter` to convert a ChatKit thread to a list of `Message`, useful for getting started quickly. ## Installation @@ -63,7 +63,7 @@ from azure.identity import AzureCliCredential from fastapi import FastAPI, Request from fastapi.responses import Response, StreamingResponse -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureOpenAIChatClient from agent_framework.chatkit import simple_to_agent_input, stream_agent_response @@ -74,8 +74,8 @@ from chatkit.types import ThreadMetadata, UserMessageItem, ThreadStreamEvent from your_store import YourStore # type: ignore[import-not-found] # Replace with your Store implementation # Define your agent with tools -agent = ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), +agent = Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", tools=[], # Add your tools here ) diff --git a/python/packages/chatkit/agent_framework_chatkit/_converter.py b/python/packages/chatkit/agent_framework_chatkit/_converter.py index ca5127e8c7..5aa953e25a 100644 --- a/python/packages/chatkit/agent_framework_chatkit/_converter.py +++ b/python/packages/chatkit/agent_framework_chatkit/_converter.py @@ -9,8 +9,8 @@ import sys from collections.abc import Awaitable, Callable, Sequence from agent_framework import ( - ChatMessage, Content, + Message, ) from chatkit.types import ( AssistantMessageItem, @@ -39,7 +39,7 @@ logger = logging.getLogger(__name__) class ThreadItemConverter: - """Helper class to convert ChatKit thread items to Agent Framework ChatMessage objects. + """Helper class to convert ChatKit thread items to Agent Framework Message objects. This class provides a base implementation for converting ChatKit thread items to Agent Framework messages. It can be extended to handle attachments, @@ -64,8 +64,8 @@ class ThreadItemConverter: async def user_message_to_input( self, item: UserMessageItem, is_last_message: bool = True - ) -> ChatMessage | list[ChatMessage] | None: - """Convert a ChatKit UserMessageItem to Agent Framework ChatMessage(s). + ) -> Message | list[Message] | None: + """Convert a ChatKit UserMessageItem to Agent Framework Message(s). This method is called internally by `to_agent_input()`. Override this method to customize how user messages are converted. @@ -75,7 +75,7 @@ class ThreadItemConverter: is_last_message: Whether this is the last message in the thread (used for quoted_text handling). Returns: - A ChatMessage, list of messages, or None to skip. + A Message, list of messages, or None to skip. Note: Instead of calling this method directly, use `to_agent_input()` which handles @@ -102,19 +102,19 @@ class ThreadItemConverter: # If only text and no attachments, use text parameter for simplicity if text_content.strip() and not data_contents: - user_message = ChatMessage(role="user", text=text_content.strip()) + user_message = Message(role="user", text=text_content.strip()) else: # Build contents list with both text and attachments contents: list[Content] = [] if text_content.strip(): contents.append(Content.from_text(text=text_content.strip())) contents.extend(data_contents) - user_message = ChatMessage(role="user", contents=contents) + user_message = Message(role="user", contents=contents) # Handle quoted text if this is the last message messages = [user_message] if item.quoted_text and is_last_message: - quoted_context = ChatMessage( + quoted_context = Message( role="user", text=f"The user is referring to this in particular:\n{item.quoted_text}", ) @@ -179,10 +179,8 @@ class ThreadItemConverter: # Subclasses can override this method to provide custom handling return None - def hidden_context_to_input( - self, item: HiddenContextItem | SDKHiddenContextItem - ) -> ChatMessage | list[ChatMessage] | None: - """Convert a ChatKit HiddenContextItem or SDKHiddenContextItem to Agent Framework ChatMessage(s). + def hidden_context_to_input(self, item: HiddenContextItem | SDKHiddenContextItem) -> Message | list[Message] | None: + """Convert a ChatKit HiddenContextItem or SDKHiddenContextItem to Agent Framework Message(s). This method is called internally by `to_agent_input()`. Override this method to customize how hidden context is converted. @@ -195,7 +193,7 @@ class ThreadItemConverter: item: The ChatKit hidden context item to convert. Returns: - A ChatMessage with system role, a list of messages, or None to skip. + A Message with system role, a list of messages, or None to skip. Note: Instead of calling this method directly, use `to_agent_input()` which handles @@ -213,9 +211,9 @@ class ThreadItemConverter: content="User's email: user@example.com", ) message = converter.hidden_context_to_input(hidden_item) - # Returns: ChatMessage(role=SYSTEM, text="User's email: ...") + # Returns: Message(role=SYSTEM, text="User's email: ...") """ - return ChatMessage(role="system", text=f"{item.content}") + return Message(role="system", text=f"{item.content}") def tag_to_message_content(self, tag: UserMessageTagContent) -> Content: """Convert a ChatKit tag (@-mention) to Agent Framework content. @@ -250,8 +248,8 @@ class ThreadItemConverter: name = getattr(tag.data, "name", tag.text if hasattr(tag, "text") else "unknown") return Content.from_text(text=f"Name:{name}") - def task_to_input(self, item: TaskItem) -> ChatMessage | list[ChatMessage] | None: - """Convert a ChatKit TaskItem to Agent Framework ChatMessage(s). + def task_to_input(self, item: TaskItem) -> Message | list[Message] | None: + """Convert a ChatKit TaskItem to Agent Framework Message(s). This method is called internally by `to_agent_input()`. Override this method to customize how tasks are converted. @@ -263,7 +261,7 @@ class ThreadItemConverter: item: The ChatKit task item to convert. Returns: - A ChatMessage, a list of messages, or None to skip the task. + A Message, a list of messages, or None to skip the task. Note: Instead of calling this method directly, use `to_agent_input()` which handles @@ -294,10 +292,10 @@ class ThreadItemConverter: f"A message was displayed to the user that the following task was performed:\n\n{task_text}\n" ) - return ChatMessage(role="user", text=text) + return Message(role="user", text=text) - def workflow_to_input(self, item: WorkflowItem) -> ChatMessage | list[ChatMessage] | None: - """Convert a ChatKit WorkflowItem to Agent Framework ChatMessage(s). + def workflow_to_input(self, item: WorkflowItem) -> Message | list[Message] | None: + """Convert a ChatKit WorkflowItem to Agent Framework Message(s). This method is called internally by `to_agent_input()`. Override this method to customize how workflows are converted. @@ -336,7 +334,7 @@ class ThreadItemConverter: messages = converter.workflow_to_input(workflow_item) # Returns list of messages for each task """ - messages: list[ChatMessage] = [] + messages: list[Message] = [] for task in item.workflow.tasks: if task.type != "custom" or (not task.title and not task.content): continue @@ -349,12 +347,12 @@ class ThreadItemConverter: f"\n{task_text}\n" ) - messages.append(ChatMessage(role="user", text=text)) + messages.append(Message(role="user", text=text)) return messages if messages else None - def widget_to_input(self, item: WidgetItem) -> ChatMessage | list[ChatMessage] | None: - """Convert a ChatKit WidgetItem to Agent Framework ChatMessage(s). + def widget_to_input(self, item: WidgetItem) -> Message | list[Message] | None: + """Convert a ChatKit WidgetItem to Agent Framework Message(s). This method is called internally by `to_agent_input()`. Override this method to customize how widgets are converted. @@ -367,7 +365,7 @@ class ThreadItemConverter: item: The ChatKit widget item to convert. Returns: - A ChatMessage describing the widget, or None to skip. + A Message describing the widget, or None to skip. Note: Instead of calling this method directly, use `to_agent_input()` which handles @@ -391,13 +389,13 @@ class ThreadItemConverter: try: widget_json = item.widget.model_dump_json(exclude_unset=True, exclude_none=True) text = f"The following graphical UI widget (id: {item.id}) was displayed to the user:{widget_json}" - return ChatMessage(role="user", text=text) + return Message(role="user", text=text) except Exception: # If JSON serialization fails, skip the widget return None - async def assistant_message_to_input(self, item: AssistantMessageItem) -> ChatMessage | list[ChatMessage] | None: - """Convert a ChatKit AssistantMessageItem to Agent Framework ChatMessage(s). + async def assistant_message_to_input(self, item: AssistantMessageItem) -> Message | list[Message] | None: + """Convert a ChatKit AssistantMessageItem to Agent Framework Message(s). The default implementation extracts text from all content parts and creates an assistant message. @@ -406,7 +404,7 @@ class ThreadItemConverter: item: The ChatKit assistant message item to convert. Returns: - A ChatMessage with assistant role, or None to skip. + A Message with assistant role, or None to skip. Note: Instead of calling this method directly, use `to_agent_input()` which handles @@ -417,10 +415,10 @@ class ThreadItemConverter: if not text_parts: return None - return ChatMessage(role="assistant", text="".join(text_parts)) + return Message(role="assistant", text="".join(text_parts)) - async def client_tool_call_to_input(self, item: ClientToolCallItem) -> ChatMessage | list[ChatMessage] | None: - """Convert a ChatKit ClientToolCallItem to Agent Framework ChatMessage(s). + async def client_tool_call_to_input(self, item: ClientToolCallItem) -> Message | list[Message] | None: + """Convert a ChatKit ClientToolCallItem to Agent Framework Message(s). The default implementation converts completed tool calls into function call and result content. @@ -442,7 +440,7 @@ class ThreadItemConverter: import json # Create function call message - function_call_msg = ChatMessage( + function_call_msg = Message( role="assistant", contents=[ Content.from_function_call( @@ -454,7 +452,7 @@ class ThreadItemConverter: ) # Create function result message - function_result_msg = ChatMessage( + function_result_msg = Message( role="tool", contents=[ Content.from_function_result( @@ -466,8 +464,8 @@ class ThreadItemConverter: return [function_call_msg, function_result_msg] - async def end_of_turn_to_input(self, item: EndOfTurnItem) -> ChatMessage | list[ChatMessage] | None: - """Convert a ChatKit EndOfTurnItem to Agent Framework ChatMessage(s). + async def end_of_turn_to_input(self, item: EndOfTurnItem) -> Message | list[Message] | None: + """Convert a ChatKit EndOfTurnItem to Agent Framework Message(s). The default implementation skips end-of-turn markers as they are only UI hints. @@ -488,15 +486,15 @@ class ThreadItemConverter: self, item: ThreadItem, is_last_message: bool = True, - ) -> list[ChatMessage]: - """Internal method to convert a single ThreadItem to ChatMessage(s). + ) -> list[Message]: + """Internal method to convert a single ThreadItem to Message(s). Args: item: The thread item to convert. is_last_message: Whether this is the last item in the thread. Returns: - A list of ChatMessage objects (may be empty). + A list of Message objects (may be empty). """ match item: case UserMessageItem(): @@ -535,7 +533,7 @@ class ThreadItemConverter: async def to_agent_input( self, thread_items: Sequence[ThreadItem] | ThreadItem, - ) -> list[ChatMessage]: + ) -> list[Message]: """Convert ChatKit thread items to Agent Framework ChatMessages. This is the main entry point for converting ChatKit thread items. It handles @@ -546,7 +544,7 @@ class ThreadItemConverter: thread_items: A single ThreadItem or a sequence of ThreadItems to convert. Returns: - A list of ChatMessage objects that can be sent to an Agent Framework agent. + A list of Message objects that can be sent to an Agent Framework agent. Examples: .. code-block:: python @@ -562,14 +560,14 @@ class ThreadItemConverter: messages = await converter.to_agent_input([user_message_item, assistant_message_item, task_item]) # Use with agent - from agent_framework import ChatAgent + from agent_framework import Agent - agent = ChatAgent(...) + agent = Agent(...) response = await agent.run(messages) """ thread_items = list(thread_items) if isinstance(thread_items, Sequence) else [thread_items] - output: list[ChatMessage] = [] + output: list[Message] = [] for item in thread_items: output.extend( await self._thread_item_to_input_item( @@ -584,7 +582,7 @@ class ThreadItemConverter: _DEFAULT_CONVERTER = ThreadItemConverter() -async def simple_to_agent_input(thread_items: Sequence[ThreadItem] | ThreadItem) -> list[ChatMessage]: +async def simple_to_agent_input(thread_items: Sequence[ThreadItem] | ThreadItem) -> list[Message]: """Helper function that uses the default ThreadItemConverter. This function provides a quick way to get started with ChatKit integration @@ -594,7 +592,7 @@ async def simple_to_agent_input(thread_items: Sequence[ThreadItem] | ThreadItem) thread_items: A single ThreadItem or a sequence of ThreadItems to convert. Returns: - A list of ChatMessage objects that can be sent to an Agent Framework agent. + A list of Message objects that can be sent to an Agent Framework agent. Examples: .. code-block:: python diff --git a/python/packages/chatkit/pyproject.toml b/python/packages/chatkit/pyproject.toml index 89e95b1ab7..6bcd89360e 100644 --- a/python/packages/chatkit/pyproject.toml +++ b/python/packages/chatkit/pyproject.toml @@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,7 +22,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "openai-chatkit>=1.4.0,<2.0.0", ] diff --git a/python/packages/chatkit/tests/test_converter.py b/python/packages/chatkit/tests/test_converter.py index 71400527aa..907a1ad0a9 100644 --- a/python/packages/chatkit/tests/test_converter.py +++ b/python/packages/chatkit/tests/test_converter.py @@ -5,7 +5,7 @@ from unittest.mock import Mock import pytest -from agent_framework import ChatMessage +from agent_framework import Message from chatkit.types import UserMessageTextContent from agent_framework_chatkit import ThreadItemConverter, simple_to_agent_input @@ -43,7 +43,7 @@ class TestThreadItemConverter: result = await converter.to_agent_input(input_item) assert len(result) == 1 - assert isinstance(result[0], ChatMessage) + assert isinstance(result[0], Message) assert result[0].role == "user" assert result[0].text == "Hello, how can you help me?" @@ -110,13 +110,13 @@ class TestThreadItemConverter: assert result[0].text == "Hello world!" def test_hidden_context_to_input(self, converter): - """Test converting hidden context item to ChatMessage.""" + """Test converting hidden context item to Message.""" hidden_item = Mock() hidden_item.content = "This is hidden context information" result = converter.hidden_context_to_input(hidden_item) - assert isinstance(result, ChatMessage) + assert isinstance(result, Message) assert result.role == "system" assert result.text == "This is hidden context information" @@ -288,7 +288,7 @@ class TestThreadItemConverter: assert message.contents[1].media_type == "application/pdf" def test_task_to_input(self, converter): - """Test converting TaskItem to ChatMessage.""" + """Test converting TaskItem to Message.""" from datetime import datetime from chatkit.types import CustomTask, TaskItem @@ -302,7 +302,7 @@ class TestThreadItemConverter: ) result = converter.task_to_input(task_item) - assert isinstance(result, ChatMessage) + assert isinstance(result, Message) assert result.role == "user" assert "Analysis: Analyzed the data" in result.text assert "" in result.text @@ -347,7 +347,7 @@ class TestThreadItemConverter: result = converter.workflow_to_input(workflow_item) assert isinstance(result, list) assert len(result) == 2 - assert all(isinstance(msg, ChatMessage) for msg in result) + assert all(isinstance(msg, Message) for msg in result) assert "Step 1: First step" in result[0].text assert "Step 2: Second step" in result[1].text @@ -369,7 +369,7 @@ class TestThreadItemConverter: assert result is None def test_widget_to_input(self, converter): - """Test converting WidgetItem to ChatMessage.""" + """Test converting WidgetItem to Message.""" from datetime import datetime from chatkit.types import WidgetItem @@ -384,7 +384,7 @@ class TestThreadItemConverter: ) result = converter.widget_to_input(widget_item) - assert isinstance(result, ChatMessage) + assert isinstance(result, Message) assert result.role == "user" assert "widget_1" in result.text assert "graphical UI widget" in result.text @@ -417,6 +417,6 @@ class TestSimpleToAgentInput: result = await simple_to_agent_input(input_item) assert len(result) == 1 - assert isinstance(result[0], ChatMessage) + assert isinstance(result[0], Message) assert result[0].role == "user" assert result[0].text == "Test message" diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index f5a343d6b9..72f17f2742 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -14,11 +14,10 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, ContextProvider, FunctionTool, - ToolProtocol, + Message, get_logger, normalize_messages, ) @@ -217,11 +216,11 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): description: str | None = None, context_provider: ContextProvider | None = None, middleware: Sequence[AgentMiddlewareTypes] | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | str - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | str] | None = None, default_options: OptionsT | MutableMapping[str, Any] | None = None, env_file_path: str | None = None, @@ -242,7 +241,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): middleware: List of middleware. tools: Tools for the agent. Can be: - Strings for built-in tools (e.g., "Read", "Write", "Bash", "Glob") - - Functions or ToolProtocol instances for custom tools + - Functions for custom tools default_options: Default ClaudeAgentOptions including system_prompt, model, etc. env_file_path: Path to .env file. env_file_encoding: Encoding of .env file. @@ -288,9 +287,9 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): except ValidationError as ex: raise ServiceInitializationError("Failed to create Claude Agent settings.", ex) from ex - # Separate built-in tools (strings) from custom tools (callables/ToolProtocol) + # Separate built-in tools (strings) from custom tools (callables/FunctionTool) self._builtin_tools: list[str] = [] - self._custom_tools: list[ToolProtocol | MutableMapping[str, Any]] = [] + self._custom_tools: list[FunctionTool | MutableMapping[str, Any]] = [] self._normalize_tools(tools) self._default_options = opts @@ -299,11 +298,11 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): def _normalize_tools( self, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | str - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | str] | None, ) -> None: """Separate built-in tools (strings) from custom tools. @@ -317,7 +316,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): # Normalize to sequence if isinstance(tools, str): tools_list: Sequence[Any] = [tools] - elif isinstance(tools, (ToolProtocol, MutableMapping)) or callable(tools): + elif isinstance(tools, (FunctionTool, MutableMapping)) or callable(tools): tools_list = [tools] else: tools_list = list(tools) @@ -458,7 +457,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): def _prepare_tools( self, - tools: list[ToolProtocol | MutableMapping[str, Any]], + tools: list[FunctionTool | MutableMapping[str, Any]], ) -> tuple[Any, list[str]]: """Convert Agent Framework tools to SDK MCP server. @@ -476,7 +475,8 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): sdk_tools.append(self._function_tool_to_sdk_mcp_tool(tool)) # Claude Agent SDK convention: MCP tools use format "mcp__{server}__{tool}" tool_names.append(f"mcp__{TOOLS_MCP_SERVER_NAME}__{tool.name}") - elif isinstance(tool, ToolProtocol): + else: + # Non-FunctionTool items (e.g., dict-based hosted tools) cannot be converted to SDK MCP tools logger.debug(f"Unsupported tool type: {type(tool)}") if not sdk_tools: @@ -541,7 +541,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): if "permission_mode" in options: await self._client.set_permission_mode(options["permission_mode"]) - def _format_prompt(self, messages: list[ChatMessage] | None) -> str: + def _format_prompt(self, messages: list[Message] | None) -> str: """Format messages into a prompt string. Args: @@ -557,7 +557,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[True], thread: AgentThread | None = None, @@ -568,7 +568,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): @overload async def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[False] = ..., thread: AgentThread | None = None, @@ -578,7 +578,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -608,7 +608,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): async def _run_non_streaming( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, thread: AgentThread | None = None, options: OptionsT | MutableMapping[str, Any] | None = None, @@ -622,7 +622,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): async def _run_streaming( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, thread: AgentThread | None = None, options: OptionsT | MutableMapping[str, Any] | None = None, diff --git a/python/packages/claude/pyproject.toml b/python/packages/claude/pyproject.toml index 88dae15d01..a4a9b142c0 100644 --- a/python/packages/claude/pyproject.toml +++ b/python/packages/claude/pyproject.toml @@ -4,7 +4,7 @@ description = "Claude Agent SDK integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "claude-agent-sdk>=0.1.25", ] diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py index 3025962f26..b3d39d82ab 100644 --- a/python/packages/claude/tests/test_claude_agent.py +++ b/python/packages/claude/tests/test_claude_agent.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import AgentResponseUpdate, AgentThread, ChatMessage, Content, tool +from agent_framework import AgentResponseUpdate, AgentThread, Content, Message, tool from agent_framework_claude import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings from agent_framework_claude._agent import TOOLS_MCP_SERVER_NAME @@ -686,7 +686,7 @@ class TestFormatPrompt: def test_format_user_message(self) -> None: """Test formatting user message.""" agent = ClaudeAgent() - msg = ChatMessage( + msg = Message( role="user", contents=[Content.from_text(text="Hello")], ) @@ -697,9 +697,9 @@ class TestFormatPrompt: """Test formatting multiple messages.""" agent = ClaudeAgent() messages = [ - ChatMessage(role="user", contents=[Content.from_text(text="Hi")]), - ChatMessage(role="assistant", contents=[Content.from_text(text="Hello!")]), - ChatMessage(role="user", contents=[Content.from_text(text="How are you?")]), + Message(role="user", contents=[Content.from_text(text="Hi")]), + Message(role="assistant", contents=[Content.from_text(text="Hello!")]), + Message(role="user", contents=[Content.from_text(text="How are you?")]), ] result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage] assert "Hi" in result diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py index 40f93eee6a..7a2567e48f 100644 --- a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py @@ -11,9 +11,9 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, ContextProvider, + Message, ResponseStream, normalize_messages, ) @@ -210,7 +210,7 @@ class CopilotStudioAgent(BaseAgent): @overload def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: Literal[False] = False, thread: AgentThread | None = None, @@ -220,7 +220,7 @@ class CopilotStudioAgent(BaseAgent): @overload def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: Literal[True], thread: AgentThread | None = None, @@ -229,7 +229,7 @@ class CopilotStudioAgent(BaseAgent): def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -259,7 +259,7 @@ class CopilotStudioAgent(BaseAgent): async def _run_impl( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -274,7 +274,7 @@ class CopilotStudioAgent(BaseAgent): question = "\n".join([message.text for message in input_messages]) activities = self.client.ask_question(question, thread.service_thread_id) - response_messages: list[ChatMessage] = [] + response_messages: list[Message] = [] response_id: str | None = None response_messages = [message async for message in self._process_activities(activities, streaming=False)] @@ -284,7 +284,7 @@ class CopilotStudioAgent(BaseAgent): def _run_stream_impl( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -338,7 +338,7 @@ class CopilotStudioAgent(BaseAgent): return conversation_id - async def _process_activities(self, activities: AsyncIterable[Any], streaming: bool) -> AsyncIterable[ChatMessage]: + async def _process_activities(self, activities: AsyncIterable[Any], streaming: bool) -> AsyncIterable[Message]: """Process activities from the Copilot Studio agent. Args: @@ -347,13 +347,13 @@ class CopilotStudioAgent(BaseAgent): or non-streaming (message activities) responses. Yields: - ChatMessage objects created from the activities. + Message objects created from the activities. """ async for activity in activities: if activity.text and ( (activity.type == "message" and not streaming) or (activity.type == "typing" and streaming) ): - yield ChatMessage( + yield Message( role="assistant", contents=[Content.from_text(activity.text)], author_name=activity.from_property.name if activity.from_property else None, diff --git a/python/packages/copilotstudio/pyproject.toml b/python/packages/copilotstudio/pyproject.toml index 7e4c61bd7e..e3936e5c8d 100644 --- a/python/packages/copilotstudio/pyproject.toml +++ b/python/packages/copilotstudio/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "microsoft-agents-copilotstudio-client>=0.3.1", ] diff --git a/python/packages/copilotstudio/tests/test_copilot_agent.py b/python/packages/copilotstudio/tests/test_copilot_agent.py index cd11c7a6ef..6172f871d3 100644 --- a/python/packages/copilotstudio/tests/test_copilot_agent.py +++ b/python/packages/copilotstudio/tests/test_copilot_agent.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import MagicMock, patch import pytest -from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, ChatMessage, Content +from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, Content, Message from agent_framework.exceptions import ServiceException, ServiceInitializationError from microsoft_agents.copilotstudio.client import CopilotClient @@ -134,7 +134,7 @@ class TestCopilotStudioAgent: assert response.messages[0].role == "assistant" async def test_run_with_chat_message(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None: - """Test run method with ChatMessage.""" + """Test run method with Message.""" agent = CopilotStudioAgent(client=mock_copilot_client) conversation_activity = MagicMock() @@ -143,7 +143,7 @@ class TestCopilotStudioAgent: mock_copilot_client.start_conversation.return_value = create_async_generator([conversation_activity]) mock_copilot_client.ask_question.return_value = create_async_generator([mock_activity]) - chat_message = ChatMessage(role="user", contents=[Content.from_text("test message")]) + chat_message = Message(role="user", contents=[Content.from_text("test message")]) response = await agent.run(chat_message) assert isinstance(response, AgentResponse) diff --git a/python/packages/core/AGENTS.md b/python/packages/core/AGENTS.md index 2a308c245d..823b601b76 100644 --- a/python/packages/core/AGENTS.md +++ b/python/packages/core/AGENTS.md @@ -9,7 +9,7 @@ agent_framework/ ├── __init__.py # Public API exports ├── _agents.py # Agent implementations ├── _clients.py # Chat client base classes and protocols -├── _types.py # Core types (ChatMessage, ChatResponse, Content, etc.) +├── _types.py # Core types (Message, ChatResponse, Content, etc.) ├── _tools.py # Tool definitions and function invocation ├── _middleware.py # Middleware system for request/response interception ├── _threads.py # AgentThread and message store abstractions @@ -27,16 +27,16 @@ agent_framework/ - **`SupportsAgentRun`** - Protocol defining the agent interface - **`BaseAgent`** - Abstract base class for agents -- **`ChatAgent`** - Main agent class wrapping a chat client with tools, instructions, and middleware +- **`Agent`** - Main agent class wrapping a chat client with tools, instructions, and middleware ### Chat Clients (`_clients.py`) -- **`ChatClientProtocol`** - Protocol for chat client implementations +- **`SupportsChatGetResponse`** - Protocol for chat client implementations - **`BaseChatClient`** - Abstract base class with middleware support; subclasses implement `_inner_get_response()` and `_inner_get_streaming_response()` ### Types (`_types.py`) -- **`ChatMessage`** - Represents a chat message with role, content, and metadata +- **`Message`** - Represents a chat message with role, content, and metadata - **`ChatResponse`** - Response from a chat client containing messages and usage - **`ChatResponseUpdate`** - Streaming response update - **`AgentResponse`** / **`AgentResponseUpdate`** - Agent-level response wrappers @@ -91,11 +91,11 @@ agent_framework/ ### Creating an Agent ```python -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient -agent = ChatAgent( - chat_client=OpenAIChatClient(), +agent = Agent( + client=OpenAIChatClient(), instructions="You are helpful.", tools=[my_function], ) @@ -114,7 +114,7 @@ agent = OpenAIChatClient().as_agent( ### Middleware Pipeline ```python -from agent_framework import ChatAgent, AgentMiddleware, AgentContext +from agent_framework import Agent, AgentMiddleware, AgentContext class LoggingMiddleware(AgentMiddleware): async def process(self, context: AgentContext, call_next) -> None: @@ -122,18 +122,18 @@ class LoggingMiddleware(AgentMiddleware): await call_next(context) print(f"Output: {context.result}") -agent = ChatAgent(..., middleware=[LoggingMiddleware()]) +agent = Agent(..., middleware=[LoggingMiddleware()]) ``` ### Custom Chat Client ```python -from agent_framework import BaseChatClient, ChatResponse, ChatMessage +from agent_framework import BaseChatClient, ChatResponse, Message class MyClient(BaseChatClient): async def _inner_get_response(self, *, messages, options, **kwargs) -> ChatResponse: # Call your LLM here - return ChatResponse(messages=[ChatMessage(role="assistant", text="Hi!")]) + return ChatResponse(messages=[Message(role="assistant", text="Hi!")]) async def _inner_get_streaming_response(self, *, messages, options, **kwargs): yield ChatResponseUpdate(...) diff --git a/python/packages/core/README.md b/python/packages/core/README.md index a56badd777..b919b229e4 100644 --- a/python/packages/core/README.md +++ b/python/packages/core/README.md @@ -45,7 +45,7 @@ You can also override environment variables by explicitly passing configuration ```python from agent_framework.azure import AzureOpenAIChatClient -chat_client = AzureOpenAIChatClient( +client = AzureOpenAIChatClient( api_key="", endpoint="", deployment_name="", @@ -61,12 +61,12 @@ Create agents and invoke them directly: ```python import asyncio -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient async def main(): - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), instructions=""" 1) A robot may not injure a human being... 2) A robot must obey orders given it by human beings... @@ -90,14 +90,14 @@ You can use the chat client classes directly for advanced workflows: ```python import asyncio from agent_framework.openai import OpenAIChatClient -from agent_framework import ChatMessage, Role +from agent_framework import Message, Role async def main(): client = OpenAIChatClient() messages = [ - ChatMessage("system", ["You are a helpful assistant."]), - ChatMessage("user", ["Write a haiku about Agent Framework."]) + Message("system", ["You are a helpful assistant."]), + Message("user", ["Write a haiku about Agent Framework."]) ] response = await client.get_response(messages) @@ -123,7 +123,7 @@ import asyncio from typing import Annotated from random import randint from pydantic import Field -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient @@ -145,8 +145,8 @@ def get_menu_specials() -> str: async def main(): - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), instructions="You are a helpful assistant that can provide weather and restaurant information.", tools=[get_weather, get_menu_specials] ) @@ -169,20 +169,20 @@ Coordinate multiple agents to collaborate on complex tasks using orchestration p ```python import asyncio -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient async def main(): # Create specialized agents - writer = ChatAgent( - chat_client=OpenAIChatClient(), + writer = Agent( + client=OpenAIChatClient(), name="Writer", instructions="You are a creative content writer. Generate and refine slogans based on feedback." ) - reviewer = ChatAgent( - chat_client=OpenAIChatClient(), + reviewer = Agent( + client=OpenAIChatClient(), name="Reviewer", instructions="You are a critical reviewer. Provide detailed feedback on proposed slogans." ) diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index 58cec8f0b0..b7d8a739d5 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -28,7 +28,7 @@ from mcp.server.lowlevel import Server from mcp.shared.exceptions import McpError from pydantic import BaseModel, Field, create_model -from ._clients import BaseChatClient, ChatClientProtocol +from ._clients import BaseChatClient, SupportsChatGetResponse from ._logging import get_logger from ._mcp import LOG_LEVEL_MAPPING, MCPTool from ._memory import Context, ContextProvider @@ -38,14 +38,13 @@ from ._threads import AgentThread, ChatMessageStoreProtocol from ._tools import ( FunctionInvocationLayer, FunctionTool, - ToolProtocol, ) from ._types import ( AgentResponse, AgentResponseUpdate, - ChatMessage, ChatResponse, ChatResponseUpdate, + Message, ResponseStream, map_chat_to_agent_update, normalize_messages, @@ -157,15 +156,15 @@ def _sanitize_agent_name(agent_name: str | None) -> str | None: class _RunContext(TypedDict): thread: AgentThread - input_messages: list[ChatMessage] - thread_messages: list[ChatMessage] + input_messages: list[Message] + thread_messages: list[Message] agent_name: str chat_options: dict[str, Any] filtered_kwargs: dict[str, Any] finalize_kwargs: dict[str, Any] -__all__ = ["BareAgent", "BaseAgent", "ChatAgent", "RawChatAgent", "SupportsAgentRun"] +__all__ = ["Agent", "BaseAgent", "RawAgent", "SupportsAgentRun"] # region Agent Protocol @@ -230,7 +229,7 @@ class SupportsAgentRun(Protocol): @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[False] = ..., thread: AgentThread | None = None, @@ -242,7 +241,7 @@ class SupportsAgentRun(Protocol): @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[True], thread: AgentThread | None = None, @@ -253,7 +252,7 @@ class SupportsAgentRun(Protocol): def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -292,7 +291,7 @@ class BaseAgent(SerializationMixin): """Base class for all Agent Framework agents. This is the minimal base class without middleware or telemetry layers. - For most use cases, prefer :class:`ChatAgent` which includes all standard layers. + For most use cases, prefer :class:`Agent` which includes all standard layers. This class provides core functionality for agent implementations, including context providers, middleware support, and thread management. @@ -300,7 +299,7 @@ class BaseAgent(SerializationMixin): Note: BaseAgent cannot be instantiated directly as it doesn't implement the ``run()`` and other methods required by SupportsAgentRun. - Use a concrete implementation like ChatAgent or create a subclass. + Use a concrete implementation like Agent or create a subclass. Examples: .. code-block:: python @@ -380,8 +379,8 @@ class BaseAgent(SerializationMixin): async def _notify_thread_of_new_messages( self, thread: AgentThread, - input_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage], + input_messages: Message | Sequence[Message], + response_messages: Message | Sequence[Message], **kwargs: Any, ) -> None: """Notify the thread of new messages. @@ -394,9 +393,9 @@ class BaseAgent(SerializationMixin): response_messages: The response messages to notify about. **kwargs: Any extra arguments to pass from the agent run. """ - if isinstance(input_messages, ChatMessage) or len(input_messages) > 0: + if isinstance(input_messages, Message) or len(input_messages) > 0: await thread.on_new_messages(input_messages) - if isinstance(response_messages, ChatMessage) or len(response_messages) > 0: + if isinstance(response_messages, Message) or len(response_messages) > 0: await thread.on_new_messages(response_messages) if thread.context_provider: await thread.context_provider.invoked(input_messages, response_messages, **kwargs) @@ -459,16 +458,16 @@ class BaseAgent(SerializationMixin): Examples: .. code-block:: python - from agent_framework import ChatAgent + from agent_framework import Agent # Create an agent - agent = ChatAgent(chat_client=client, name="research-agent", description="Performs research tasks") + agent = Agent(client=client, name="research-agent", description="Performs research tasks") # Convert the agent to a tool research_tool = agent.as_tool() # Use the tool with another agent - coordinator = ChatAgent(chat_client=client, name="coordinator", tools=research_tool) + coordinator = Agent(client=client, name="coordinator", tools=research_tool) """ # Verify that self implements SupportsAgentRun if not isinstance(self, SupportsAgentRun): @@ -523,18 +522,14 @@ class BaseAgent(SerializationMixin): return agent_tool -# Backward compatibility alias -BareAgent = BaseAgent +# region Agent -# region ChatAgent - - -class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] +class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] """A Chat Client Agent without middleware or telemetry layers. This is the core chat agent implementation. For most use cases, - prefer :class:`ChatAgent` which includes all standard layers. + prefer :class:`Agent` which includes all standard layers. This is the primary agent implementation that uses a chat client to interact with language models. It supports tools, context providers, middleware, and @@ -548,12 +543,12 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] .. code-block:: python - from agent_framework import ChatAgent + from agent_framework import Agent from agent_framework.openai import OpenAIChatClient # Create a basic chat agent client = OpenAIChatClient(model_id="gpt-4") - agent = ChatAgent(chat_client=client, name="assistant", description="A helpful assistant") + agent = Agent(client=client, name="assistant", description="A helpful assistant") # Run the agent with a simple message response = await agent.run("Hello, how are you?") @@ -568,8 +563,8 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] return f"The weather in {location} is sunny." - agent = ChatAgent( - chat_client=client, + agent = Agent( + client=client, name="weather-agent", instructions="You are a weather assistant.", tools=get_weather, @@ -587,12 +582,12 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] .. code-block:: python - from agent_framework import ChatAgent + from agent_framework import Agent from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions client = OpenAIChatClient(model_id="gpt-4o") - agent: ChatAgent[OpenAIChatOptions] = ChatAgent( - chat_client=client, + agent: Agent[OpenAIChatOptions] = Agent( + client=client, name="reasoning-agent", instructions="You are a reasoning assistant.", options={ @@ -613,26 +608,27 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] def __init__( self, - chat_client: ChatClientProtocol[OptionsCoT], + client: SupportsChatGetResponse[OptionsCoT], instructions: str | None = None, *, id: str | None = None, name: str | None = None, description: str | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Any + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] | None = None, default_options: OptionsCoT | None = None, chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None, context_provider: ContextProvider | None = None, **kwargs: Any, ) -> None: - """Initialize a ChatAgent instance. + """Initialize a Agent instance. Args: - chat_client: The chat client to use for the agent. + client: The chat client to use for the agent. instructions: Optional instructions for the agent. These will be put into the messages sent to the chat client service as a system message. @@ -645,7 +641,7 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] context_provider: The context providers to include during agent invocation. middleware: List of middleware to intercept agent and function invocations. default_options: A TypedDict containing chat options. When using a typed agent like - ``ChatAgent[OpenAIChatOptions]``, this enables IDE autocomplete for + ``Agent[OpenAIChatOptions]``, this enables IDE autocomplete for provider-specific options including temperature, max_tokens, model_id, tool_choice, and provider-specific options like reasoning_effort. You can also create your own TypedDict for custom chat clients. @@ -667,7 +663,7 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] "Use conversation_id for service-managed threads or chat_message_store_factory for local storage." ) - if not isinstance(chat_client, FunctionInvocationLayer) and isinstance(chat_client, BaseChatClient): + if not isinstance(client, FunctionInvocationLayer) and isinstance(client, BaseChatClient): logger.warning( "The provided chat client does not support function invoking, this might limit agent capabilities." ) @@ -679,16 +675,16 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] context_provider=context_provider, **kwargs, ) - self.chat_client = chat_client + self.client = client self.chat_message_store_factory = chat_message_store_factory # Get tools from options or named parameter (named param takes precedence) tools_ = tools if tools is not None else opts.pop("tools", None) tools_ = cast( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None, tools_, ) @@ -698,15 +694,15 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] # We ignore the MCP Servers here and store them separately, # we add their functions to the tools list at runtime - normalized_tools: list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] = ( # type:ignore[reportUnknownVariableType] + normalized_tools: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] = ( # type:ignore[reportUnknownVariableType] [] if tools_ is None else tools_ if isinstance(tools_, list) else [tools_] # type: ignore[list-item] ) - self.mcp_tools: list[MCPTool] = [tool for tool in normalized_tools if isinstance(tool, MCPTool)] + self.mcp_tools: list[MCPTool] = [tool for tool in normalized_tools if isinstance(tool, MCPTool)] # type: ignore[misc] agent_tools = [tool for tool in normalized_tools if not isinstance(tool, MCPTool)] # Build chat options dict self.default_options: dict[str, Any] = { - "model_id": opts.pop("model_id", None) or (getattr(self.chat_client, "model_id", None)), + "model_id": opts.pop("model_id", None) or (getattr(self.client, "model_id", None)), "allow_multiple_tool_calls": opts.pop("allow_multiple_tool_calls", None), "conversation_id": conversation_id, "frequency_penalty": opts.pop("frequency_penalty", None), @@ -734,16 +730,16 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] async def __aenter__(self) -> Self: """Enter the async context manager. - If any of the chat_client or local_mcp_tools are context managers, + If any of the client or local_mcp_tools are context managers, they will be entered into the async exit stack to ensure proper cleanup. Note: This list might be extended in the future. Returns: - The ChatAgent instance. + The Agent instance. """ - for context_manager in chain([self.chat_client], self.mcp_tools): + for context_manager in chain([self.client], self.mcp_tools): if isinstance(context_manager, AbstractAsyncContextManager): await self._async_exit_stack.enter_async_context(context_manager) return self @@ -772,22 +768,23 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] should check if there is already an agent name defined, and if not set it to this value. """ - if hasattr(self.chat_client, "_update_agent_name_and_description") and callable( - self.chat_client._update_agent_name_and_description + if hasattr(self.client, "_update_agent_name_and_description") and callable( + self.client._update_agent_name_and_description ): # type: ignore[reportAttributeAccessIssue, attr-defined] - self.chat_client._update_agent_name_and_description(self.name, self.description) # type: ignore[reportAttributeAccessIssue, attr-defined] + self.client._update_agent_name_and_description(self.name, self.description) # type: ignore[reportAttributeAccessIssue, attr-defined] @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[False] = ..., thread: AgentThread | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Any + | list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] | None = None, options: ChatOptions[ResponseModelBoundT], **kwargs: Any, @@ -796,14 +793,15 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[False] = ..., thread: AgentThread | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Any + | list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] | None = None, options: OptionsCoT | ChatOptions[None] | None = None, **kwargs: Any, @@ -812,14 +810,15 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[True], thread: AgentThread | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Any + | list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] | None = None, options: OptionsCoT | ChatOptions[Any] | None = None, **kwargs: Any, @@ -827,14 +826,15 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Any + | list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] | None = None, options: OptionsCoT | ChatOptions[Any] | None = None, **kwargs: Any, @@ -855,7 +855,7 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] thread: The thread to use for the agent. tools: The tools to use for this specific run (merged with default tools). options: A TypedDict containing chat options. When using a typed agent like - ``ChatAgent[OpenAIChatOptions]``, this enables IDE autocomplete for + ``Agent[OpenAIChatOptions]``, this enables IDE autocomplete for provider-specific options including temperature, max_tokens, model_id, tool_choice, and provider-specific options like reasoning_effort. kwargs: Additional keyword arguments for the agent. @@ -876,7 +876,7 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] options=options, kwargs=kwargs, ) - response = await self.chat_client.get_response( # type: ignore[call-overload] + response = await self.client.get_response( # type: ignore[call-overload] messages=ctx["thread_messages"], stream=False, options=ctx["chat_options"], @@ -908,6 +908,7 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] usage_details=response.usage_details, value=response.value, response_format=response_format, + continuation_token=response.continuation_token, raw_representation=response, additional_properties=response.additional_properties, ) @@ -947,7 +948,7 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] kwargs=kwargs, ) ctx: _RunContext = ctx_holder["ctx"] # type: ignore[assignment] # Safe: we just assigned it - return self.chat_client.get_response( # type: ignore[call-overload, no-any-return] + return self.client.get_response( # type: ignore[call-overload, no-any-return] messages=ctx["thread_messages"], stream=True, options=ctx["chat_options"], @@ -982,12 +983,13 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] async def _prepare_run_context( self, *, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None, + messages: str | Message | Sequence[str | Message] | None, thread: AgentThread | None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Any + | list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] | None, options: Mapping[str, Any] | None, kwargs: dict[str, Any], @@ -1003,13 +1005,13 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] ) # Normalize tools - normalized_tools: list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] = ( + normalized_tools: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] = ( [] if tools_ is None else tools_ if isinstance(tools_, list) else [tools_] ) agent_name = self._get_agent_name() # Resolve final tool list (runtime provided tools + local MCP server tools) - final_tools: list[ToolProtocol | Callable[..., Any] | dict[str, Any]] = [] + final_tools: list[FunctionTool | Callable[..., Any] | dict[str, Any] | Any] = [] for tool in normalized_tools: if isinstance(tool, MCPTool): if not tool.is_connected: @@ -1070,7 +1072,7 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] response: ChatResponse, agent_name: str, thread: AgentThread, - input_messages: list[ChatMessage], + input_messages: list[Message], kwargs: dict[str, Any], ) -> None: """Finalize response by updating thread and setting author names. @@ -1290,9 +1292,9 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] self, *, thread: AgentThread | None, - input_messages: list[ChatMessage] | None = None, + input_messages: list[Message] | None = None, **kwargs: Any, - ) -> tuple[AgentThread, dict[str, Any], list[ChatMessage]]: + ) -> tuple[AgentThread, dict[str, Any], list[Message]]: """Prepare the thread and messages for agent execution. This method prepares the conversation thread, merges context provider data, @@ -1328,7 +1330,7 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] thread = thread or self.get_new_thread() if thread.service_thread_id and thread.context_provider: await thread.context_provider.thread_created(thread.service_thread_id) - thread_messages: list[ChatMessage] = [] + thread_messages: list[Message] = [] if thread.message_store: thread_messages.extend(await thread.message_store.list_messages() or []) context: Context | None = None @@ -1372,10 +1374,10 @@ class RawChatAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] return self.name or "UnnamedAgent" -class ChatAgent( +class Agent( AgentTelemetryLayer, AgentMiddlewareLayer, - RawChatAgent[OptionsCoT], + RawAgent[OptionsCoT], Generic[OptionsCoT], ): """A Chat Client Agent with middleware, telemetry, and full layer support. @@ -1384,21 +1386,22 @@ class ChatAgent( - Agent middleware support for request/response interception - OpenTelemetry-based telemetry for observability - For a minimal implementation without these features, use :class:`RawChatAgent`. + For a minimal implementation without these features, use :class:`RawAgent`. """ def __init__( self, - chat_client: ChatClientProtocol[OptionsCoT], + client: SupportsChatGetResponse[OptionsCoT], instructions: str | None = None, *, id: str | None = None, name: str | None = None, description: str | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Any + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] | None = None, default_options: OptionsCoT | None = None, chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None, @@ -1406,9 +1409,9 @@ class ChatAgent( middleware: Sequence[MiddlewareTypes] | None = None, **kwargs: Any, ) -> None: - """Initialize a ChatAgent instance.""" + """Initialize a Agent instance.""" super().__init__( - chat_client=chat_client, + client=client, instructions=instructions, id=id, name=name, diff --git a/python/packages/core/agent_framework/_clients.py b/python/packages/core/agent_framework/_clients.py index 1893f27f80..0c3523698e 100644 --- a/python/packages/core/agent_framework/_clients.py +++ b/python/packages/core/agent_framework/_clients.py @@ -33,12 +33,12 @@ from ._serialization import SerializationMixin from ._threads import ChatMessageStoreProtocol from ._tools import ( FunctionInvocationConfiguration, - ToolProtocol, + FunctionTool, ) from ._types import ( - ChatMessage, ChatResponse, ChatResponseUpdate, + Message, ResponseStream, prepare_messages, validate_chat_options, @@ -51,7 +51,7 @@ else: if TYPE_CHECKING: - from ._agents import ChatAgent + from ._agents import Agent from ._middleware import ( MiddlewareTypes, ) @@ -67,11 +67,16 @@ logger = get_logger() __all__ = [ "BaseChatClient", - "ChatClientProtocol", + "SupportsChatGetResponse", + "SupportsCodeInterpreterTool", + "SupportsFileSearchTool", + "SupportsImageGenerationTool", + "SupportsMCPTool", + "SupportsWebSearchTool", ] -# region ChatClientProtocol Protocol +# region SupportsChatGetResponse Protocol # Contravariant for the Protocol OptionsContraT = TypeVar( @@ -86,7 +91,7 @@ ResponseModelBoundT = TypeVar("ResponseModelBoundT", bound=BaseModel) @runtime_checkable -class ChatClientProtocol(Protocol[OptionsContraT]): +class SupportsChatGetResponse(Protocol[OptionsContraT]): """A protocol for a chat client that can generate responses. This protocol defines the interface that all chat clients must implement, @@ -103,7 +108,7 @@ class ChatClientProtocol(Protocol[OptionsContraT]): Examples: .. code-block:: python - from agent_framework import ChatClientProtocol, ChatResponse, ChatMessage + from agent_framework import SupportsChatGetResponse, ChatResponse, Message # Any class implementing the required methods is compatible @@ -128,7 +133,7 @@ class ChatClientProtocol(Protocol[OptionsContraT]): # Verify the instance satisfies the protocol client = CustomChatClient() - assert isinstance(client, ChatClientProtocol) + assert isinstance(client, SupportsChatGetResponse) """ additional_properties: dict[str, Any] @@ -136,7 +141,7 @@ class ChatClientProtocol(Protocol[OptionsContraT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: ChatOptions[ResponseModelBoundT], @@ -146,7 +151,7 @@ class ChatClientProtocol(Protocol[OptionsContraT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: OptionsContraT | ChatOptions[None] | None = None, @@ -156,7 +161,7 @@ class ChatClientProtocol(Protocol[OptionsContraT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[True], options: OptionsContraT | ChatOptions[Any] | None = None, @@ -165,7 +170,7 @@ class ChatClientProtocol(Protocol[OptionsContraT]): def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: bool = False, options: OptionsContraT | ChatOptions[Any] | None = None, @@ -226,7 +231,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): Examples: .. code-block:: python - from agent_framework import BaseChatClient, ChatResponse, ChatMessage + from agent_framework import BaseChatClient, ChatResponse, Message from collections.abc import AsyncIterable @@ -243,7 +248,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): else: # Non-streaming implementation return ChatResponse( - messages=[ChatMessage(role="assistant", text="Hello!")], response_id="custom-response" + messages=[Message(role="assistant", text="Hello!")], response_id="custom-response" ) @@ -338,7 +343,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], stream: bool, options: Mapping[str, Any], **kwargs: Any, @@ -365,7 +370,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: ChatOptions[ResponseModelBoundT], @@ -375,7 +380,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: OptionsCoT | ChatOptions[None] | None = None, @@ -385,7 +390,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[True], options: OptionsCoT | ChatOptions[Any] | None = None, @@ -394,7 +399,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: bool = False, options: OptionsCoT | ChatOptions[Any] | None = None, @@ -437,10 +442,10 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): name: str | None = None, description: str | None = None, instructions: str | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, default_options: OptionsCoT | Mapping[str, Any] | None = None, chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None, @@ -448,10 +453,10 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): middleware: Sequence[MiddlewareTypes] | None = None, function_invocation_configuration: FunctionInvocationConfiguration | None = None, **kwargs: Any, - ) -> ChatAgent[OptionsCoT]: - """Create a ChatAgent with this client. + ) -> Agent[OptionsCoT]: + """Create a Agent with this client. - This is a convenience method that creates a ChatAgent instance with this + This is a convenience method that creates a Agent instance with this chat client already configured. Keyword Args: @@ -474,7 +479,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): kwargs: Any additional keyword arguments. Will be stored as ``additional_properties``. Returns: - A ChatAgent instance configured with this chat client. + A Agent instance configured with this chat client. Examples: .. code-block:: python @@ -494,10 +499,10 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): # Run the agent response = await agent.run("Hello!") """ - from ._agents import ChatAgent + from ._agents import Agent - return ChatAgent( - chat_client=self, + return Agent( + client=self, id=id, name=name, description=description, @@ -510,3 +515,163 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): function_invocation_configuration=function_invocation_configuration, **kwargs, ) + + +# endregion + + +# region Tool Support Protocols + + +@runtime_checkable +class SupportsCodeInterpreterTool(Protocol): + """Protocol for clients that support code interpreter tools. + + This protocol enables runtime checking to determine if a client + supports code interpreter functionality. + + Examples: + .. code-block:: python + + from agent_framework import SupportsCodeInterpreterTool + + if isinstance(client, SupportsCodeInterpreterTool): + tool = client.get_code_interpreter_tool() + agent = ChatAgent(client, tools=[tool]) + """ + + @staticmethod + def get_code_interpreter_tool(**kwargs: Any) -> Any: + """Create a code interpreter tool configuration. + + Keyword Args: + **kwargs: Provider-specific configuration options. + + Returns: + A tool configuration ready to pass to ChatAgent. + """ + ... + + +@runtime_checkable +class SupportsWebSearchTool(Protocol): + """Protocol for clients that support web search tools. + + This protocol enables runtime checking to determine if a client + supports web search functionality. + + Examples: + .. code-block:: python + + from agent_framework import SupportsWebSearchTool + + if isinstance(client, SupportsWebSearchTool): + tool = client.get_web_search_tool() + agent = ChatAgent(client, tools=[tool]) + """ + + @staticmethod + def get_web_search_tool(**kwargs: Any) -> Any: + """Create a web search tool configuration. + + Keyword Args: + **kwargs: Provider-specific configuration options. + + Returns: + A tool configuration ready to pass to ChatAgent. + """ + ... + + +@runtime_checkable +class SupportsImageGenerationTool(Protocol): + """Protocol for clients that support image generation tools. + + This protocol enables runtime checking to determine if a client + supports image generation functionality. + + Examples: + .. code-block:: python + + from agent_framework import SupportsImageGenerationTool + + if isinstance(client, SupportsImageGenerationTool): + tool = client.get_image_generation_tool() + agent = ChatAgent(client, tools=[tool]) + """ + + @staticmethod + def get_image_generation_tool(**kwargs: Any) -> Any: + """Create an image generation tool configuration. + + Keyword Args: + **kwargs: Provider-specific configuration options. + + Returns: + A tool configuration ready to pass to ChatAgent. + """ + ... + + +@runtime_checkable +class SupportsMCPTool(Protocol): + """Protocol for clients that support MCP (Model Context Protocol) tools. + + This protocol enables runtime checking to determine if a client + supports MCP server connections. + + Examples: + .. code-block:: python + + from agent_framework import SupportsMCPTool + + if isinstance(client, SupportsMCPTool): + tool = client.get_mcp_tool(name="my_mcp", url="https://...") + agent = ChatAgent(client, tools=[tool]) + """ + + @staticmethod + def get_mcp_tool(**kwargs: Any) -> Any: + """Create an MCP tool configuration. + + Keyword Args: + **kwargs: Provider-specific configuration options including + name and url for the MCP server. + + Returns: + A tool configuration ready to pass to ChatAgent. + """ + ... + + +@runtime_checkable +class SupportsFileSearchTool(Protocol): + """Protocol for clients that support file search tools. + + This protocol enables runtime checking to determine if a client + supports file search functionality with vector stores. + + Examples: + .. code-block:: python + + from agent_framework import SupportsFileSearchTool + + if isinstance(client, SupportsFileSearchTool): + tool = client.get_file_search_tool(vector_store_ids=["vs_123"]) + agent = ChatAgent(client, tools=[tool]) + """ + + @staticmethod + def get_file_search_tool(**kwargs: Any) -> Any: + """Create a file search tool configuration. + + Keyword Args: + **kwargs: Provider-specific configuration options. + + Returns: + A tool configuration ready to pass to ChatAgent. + """ + ... + + +# endregion diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index d716aa0c94..a56e7f14db 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -12,7 +12,7 @@ from collections.abc import Callable, Collection, Sequence from contextlib import AsyncExitStack, _AsyncGeneratorContextManager # type: ignore from datetime import timedelta from functools import partial -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, TypedDict import httpx from anyio import ClosedResourceError @@ -28,12 +28,11 @@ from pydantic import BaseModel, create_model from ._tools import ( FunctionTool, - HostedMCPSpecificApproval, _build_pydantic_model_from_json_schema, ) from ._types import ( - ChatMessage, Content, + Message, ) from .exceptions import ToolException, ToolExecutionException @@ -43,7 +42,22 @@ else: from typing_extensions import Self # pragma: no cover if TYPE_CHECKING: - from ._clients import ChatClientProtocol + from ._clients import SupportsChatGetResponse + + +class MCPSpecificApproval(TypedDict, total=False): + """Represents the specific approval mode for an MCP tool. + + When using this mode, the user must specify which tools always or never require approval. + + Attributes: + always_require_approval: A sequence of tool names that always require approval. + never_require_approval: A sequence of tool names that never require approval. + """ + + always_require_approval: Collection[str] | None + never_require_approval: Collection[str] | None + logger = logging.getLogger(__name__) @@ -69,9 +83,9 @@ __all__ = [ def _parse_message_from_mcp( mcp_type: types.PromptMessage | types.SamplingMessage, -) -> ChatMessage: +) -> Message: """Parse an MCP container type into an Agent Framework type.""" - return ChatMessage( + return Message( role=mcp_type.role, contents=_parse_content_from_mcp(mcp_type.content), raw_representation=mcp_type, @@ -256,9 +270,9 @@ def _prepare_content_for_mcp( def _prepare_message_for_mcp( - content: ChatMessage, + content: Message, ) -> list[types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink]: - """Prepare a ChatMessage for MCP format.""" + """Prepare a Message for MCP format.""" messages: list[ types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink ] = [] @@ -327,7 +341,7 @@ class MCPTool: self, name: str, description: str | None = None, - approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None, + approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None, allowed_tools: Collection[str] | None = None, load_tools: bool = True, parse_tool_results: Literal[True] | Callable[[types.CallToolResult], Any] | None = True, @@ -335,7 +349,7 @@ class MCPTool: parse_prompt_results: Literal[True] | Callable[[types.GetPromptResult], Any] | None = True, session: ClientSession | None = None, request_timeout: int | None = None, - chat_client: ChatClientProtocol | None = None, + client: SupportsChatGetResponse | None = None, additional_properties: dict[str, Any] | None = None, ) -> None: """Initialize the MCP Tool base. @@ -356,7 +370,7 @@ class MCPTool: self._exit_stack = AsyncExitStack() self.session = session self.request_timeout = request_timeout - self.chat_client = chat_client + self.client = client self._functions: list[FunctionTool[Any, Any]] = [] self.is_connected: bool = False self._tools_loaded: bool = False @@ -507,17 +521,17 @@ class MCPTool: Returns: Either a CreateMessageResult with the generated message or ErrorData if generation fails. """ - if not self.chat_client: + if not self.client: return types.ErrorData( code=types.INTERNAL_ERROR, message="No chat client available. Please set a chat client.", ) logger.debug("Sampling callback called with params: %s", params) - messages: list[ChatMessage] = [] + messages: list[Message] = [] for msg in params.messages: messages.append(_parse_message_from_mcp(msg)) try: - response = await self.chat_client.get_response( + response = await self.client.get_response( messages, temperature=params.temperature, max_tokens=params.maxTokens, @@ -634,7 +648,7 @@ class MCPTool: input_model = _get_input_model_from_mcp_prompt(prompt) approval_mode = self._determine_approval_mode(local_name) - func: FunctionTool[BaseModel, list[ChatMessage] | Any | types.GetPromptResult] = FunctionTool( + func: FunctionTool[BaseModel, list[Message] | Any | types.GetPromptResult] = FunctionTool( func=partial(self.get_prompt, prompt.name), name=local_name, description=prompt.description or "", @@ -801,7 +815,7 @@ class MCPTool: raise ToolExecutionException(f"Failed to call tool '{tool_name}'.", inner_exception=ex) from ex raise ToolExecutionException(f"Failed to call tool '{tool_name}' after retries.") - async def get_prompt(self, prompt_name: str, **kwargs: Any) -> list[ChatMessage] | Any | types.GetPromptResult: + async def get_prompt(self, prompt_name: str, **kwargs: Any) -> list[Message] | Any | types.GetPromptResult: """Call a prompt with the given arguments. Args: @@ -909,7 +923,7 @@ class MCPStdioTool(MCPTool): Examples: .. code-block:: python - from agent_framework import MCPStdioTool, ChatAgent + from agent_framework import MCPStdioTool, Agent # Create an MCP stdio tool mcp_tool = MCPStdioTool( @@ -921,7 +935,7 @@ class MCPStdioTool(MCPTool): # Use with a chat agent async with mcp_tool: - agent = ChatAgent(chat_client=client, name="assistant", tools=mcp_tool) + agent = Agent(client=client, name="assistant", tools=mcp_tool) response = await agent.run("List files in the directory") """ @@ -937,12 +951,12 @@ class MCPStdioTool(MCPTool): request_timeout: int | None = None, session: ClientSession | None = None, description: str | None = None, - approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None, + approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None, allowed_tools: Collection[str] | None = None, args: list[str] | None = None, env: dict[str, str] | None = None, encoding: str | None = None, - chat_client: ChatClientProtocol | None = None, + client: SupportsChatGetResponse | None = None, additional_properties: dict[str, Any] | None = None, **kwargs: Any, ) -> None: @@ -982,7 +996,7 @@ class MCPStdioTool(MCPTool): args: The arguments to pass to the command. env: The environment variables to set for the command. encoding: The encoding to use for the command output. - chat_client: The chat client to use for sampling. + client: The chat client to use for sampling. kwargs: Any extra arguments to pass to the stdio client. """ super().__init__( @@ -992,7 +1006,7 @@ class MCPStdioTool(MCPTool): allowed_tools=allowed_tools, additional_properties=additional_properties, session=session, - chat_client=chat_client, + client=client, load_tools=load_tools, parse_tool_results=parse_tool_results, load_prompts=load_prompts, @@ -1031,7 +1045,7 @@ class MCPStreamableHTTPTool(MCPTool): Examples: .. code-block:: python - from agent_framework import MCPStreamableHTTPTool, ChatAgent + from agent_framework import MCPStreamableHTTPTool, Agent # Create an MCP HTTP tool mcp_tool = MCPStreamableHTTPTool( @@ -1042,7 +1056,7 @@ class MCPStreamableHTTPTool(MCPTool): # Use with a chat agent async with mcp_tool: - agent = ChatAgent(chat_client=client, name="assistant", tools=mcp_tool) + agent = Agent(client=client, name="assistant", tools=mcp_tool) response = await agent.run("Fetch data from the API") """ @@ -1058,10 +1072,10 @@ class MCPStreamableHTTPTool(MCPTool): request_timeout: int | None = None, session: ClientSession | None = None, description: str | None = None, - approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None, + approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None, allowed_tools: Collection[str] | None = None, terminate_on_close: bool | None = None, - chat_client: ChatClientProtocol | None = None, + client: SupportsChatGetResponse | None = None, additional_properties: dict[str, Any] | None = None, http_client: httpx.AsyncClient | None = None, **kwargs: Any, @@ -1101,7 +1115,7 @@ class MCPStreamableHTTPTool(MCPTool): allowed_tools: A list of tools that are allowed to use this tool. additional_properties: Additional properties. terminate_on_close: Close the transport when the MCP client is terminated. - chat_client: The chat client to use for sampling. + client: The chat client to use for sampling. http_client: Optional httpx.AsyncClient to use. If not provided, the ``streamable_http_client`` API will create and manage a default client. To configure headers, timeouts, or other HTTP client settings, create @@ -1115,7 +1129,7 @@ class MCPStreamableHTTPTool(MCPTool): allowed_tools=allowed_tools, additional_properties=additional_properties, session=session, - chat_client=chat_client, + client=client, load_tools=load_tools, parse_tool_results=parse_tool_results, load_prompts=load_prompts, @@ -1148,7 +1162,7 @@ class MCPWebsocketTool(MCPTool): Examples: .. code-block:: python - from agent_framework import MCPWebsocketTool, ChatAgent + from agent_framework import MCPWebsocketTool, Agent # Create an MCP WebSocket tool mcp_tool = MCPWebsocketTool( @@ -1157,7 +1171,7 @@ class MCPWebsocketTool(MCPTool): # Use with a chat agent async with mcp_tool: - agent = ChatAgent(chat_client=client, name="assistant", tools=mcp_tool) + agent = Agent(client=client, name="assistant", tools=mcp_tool) response = await agent.run("Connect to the real-time service") """ @@ -1173,9 +1187,9 @@ class MCPWebsocketTool(MCPTool): request_timeout: int | None = None, session: ClientSession | None = None, description: str | None = None, - approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None, + approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None, allowed_tools: Collection[str] | None = None, - chat_client: ChatClientProtocol | None = None, + client: SupportsChatGetResponse | None = None, additional_properties: dict[str, Any] | None = None, **kwargs: Any, ) -> None: @@ -1213,7 +1227,7 @@ class MCPWebsocketTool(MCPTool): A tool should not be listed in both, if so, it will require approval. allowed_tools: A list of tools that are allowed to use this tool. additional_properties: Additional properties. - chat_client: The chat client to use for sampling. + client: The chat client to use for sampling. kwargs: Any extra arguments to pass to the WebSocket client. """ super().__init__( @@ -1223,7 +1237,7 @@ class MCPWebsocketTool(MCPTool): allowed_tools=allowed_tools, additional_properties=additional_properties, session=session, - chat_client=chat_client, + client=client, load_tools=load_tools, parse_tool_results=parse_tool_results, load_prompts=load_prompts, diff --git a/python/packages/core/agent_framework/_memory.py b/python/packages/core/agent_framework/_memory.py index 465bc1ffec..f6c2bd6403 100644 --- a/python/packages/core/agent_framework/_memory.py +++ b/python/packages/core/agent_framework/_memory.py @@ -8,10 +8,10 @@ from collections.abc import MutableSequence, Sequence from types import TracebackType from typing import TYPE_CHECKING, Any, Final -from ._types import ChatMessage +from ._types import Message if TYPE_CHECKING: - from ._tools import ToolProtocol + from ._tools import FunctionTool if sys.version_info >= (3, 11): from typing import Self # pragma: no cover @@ -34,12 +34,12 @@ class Context: Examples: .. code-block:: python - from agent_framework import Context, ChatMessage + from agent_framework import Context, Message # Create context with instructions context = Context( instructions="Use a professional tone when responding.", - messages=[ChatMessage(content="Previous context", role="user")], + messages=[Message(content="Previous context", role="user")], tools=[my_tool], ) @@ -51,8 +51,8 @@ class Context: def __init__( self, instructions: str | None = None, - messages: Sequence[ChatMessage] | None = None, - tools: Sequence[ToolProtocol] | None = None, + messages: Sequence[Message] | None = None, + tools: Sequence[FunctionTool] | None = None, ): """Create a new Context object. @@ -62,8 +62,8 @@ class Context: tools: The list of tools to provide to this run. """ self.instructions = instructions - self.messages: Sequence[ChatMessage] = messages or [] - self.tools: Sequence[ToolProtocol] = tools or [] + self.messages: Sequence[Message] = messages or [] + self.tools: Sequence[FunctionTool] = tools or [] # region ContextProvider @@ -85,7 +85,7 @@ class ContextProvider(ABC): Examples: .. code-block:: python - from agent_framework import ContextProvider, Context, ChatMessage + from agent_framework import ContextProvider, Context, Message class CustomContextProvider(ContextProvider): @@ -96,7 +96,7 @@ class ContextProvider(ABC): # Use with a chat agent async with CustomContextProvider() as provider: - agent = ChatAgent(chat_client=client, name="assistant", context_provider=provider) + agent = Agent(client=client, name="assistant", context_provider=provider) """ # Default prompt to be used by all context providers when assembling memories/instructions @@ -116,8 +116,8 @@ class ContextProvider(ABC): async def invoked( self, - request_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage] | None = None, + request_messages: Message | Sequence[Message], + response_messages: Message | Sequence[Message] | None = None, invoke_exception: Exception | None = None, **kwargs: Any, ) -> None: @@ -136,7 +136,7 @@ class ContextProvider(ABC): pass @abstractmethod - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: """Called just before the model/agent is invoked. Implementers can load any additional context required at this time, diff --git a/python/packages/core/agent_framework/_middleware.py b/python/packages/core/agent_framework/_middleware.py index 8d63aa2eba..ac6630a03f 100644 --- a/python/packages/core/agent_framework/_middleware.py +++ b/python/packages/core/agent_framework/_middleware.py @@ -10,13 +10,13 @@ from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequenc from enum import Enum from typing import TYPE_CHECKING, Any, Generic, Literal, TypeAlias, overload -from ._clients import ChatClientProtocol +from ._clients import SupportsChatGetResponse from ._types import ( AgentResponse, AgentResponseUpdate, - ChatMessage, ChatResponse, ChatResponseUpdate, + Message, ResponseStream, prepare_messages, ) @@ -35,7 +35,7 @@ if TYPE_CHECKING: from pydantic import BaseModel from ._agents import SupportsAgentRun - from ._clients import ChatClientProtocol + from ._clients import SupportsChatGetResponse from ._threads import AgentThread from ._tools import FunctionTool from ._types import ChatOptions, ChatResponse, ChatResponseUpdate @@ -155,7 +155,7 @@ class AgentContext: self, *, agent: SupportsAgentRun, - messages: list[ChatMessage], + messages: list[Message], thread: AgentThread | None = None, options: Mapping[str, Any] | None = None, stream: bool = False, @@ -263,7 +263,7 @@ class ChatContext: about the chat request. Attributes: - chat_client: The chat client being invoked. + client: The chat client being invoked. messages: The messages being sent to the chat client. options: The options for the chat request as a dict. stream: Whether this is a streaming invocation. @@ -302,8 +302,8 @@ class ChatContext: def __init__( self, - chat_client: ChatClientProtocol, - messages: Sequence[ChatMessage], + client: SupportsChatGetResponse, + messages: Sequence[Message], options: Mapping[str, Any] | None, stream: bool = False, metadata: Mapping[str, Any] | None = None, @@ -319,7 +319,7 @@ class ChatContext: """Initialize the ChatContext. Args: - chat_client: The chat client being invoked. + client: The chat client being invoked. messages: The messages being sent to the chat client. options: The options for the chat request as a dict. stream: Whether this is a streaming invocation. @@ -330,7 +330,7 @@ class ChatContext: stream_result_hooks: Result hooks to apply to the finalized streaming response. stream_cleanup_hooks: Cleanup hooks to run after streaming completes. """ - self.chat_client = chat_client + self.client = client self.messages = messages self.options = options self.stream = stream @@ -356,7 +356,7 @@ class AgentMiddleware(ABC): Examples: .. code-block:: python - from agent_framework import AgentMiddleware, AgentContext, ChatAgent + from agent_framework import AgentMiddleware, AgentContext, Agent class RetryMiddleware(AgentMiddleware): @@ -372,7 +372,7 @@ class AgentMiddleware(ABC): # Use with an agent - agent = ChatAgent(chat_client=client, name="assistant", middleware=[RetryMiddleware()]) + agent = Agent(client=client, name="assistant", middleware=[RetryMiddleware()]) """ @abstractmethod @@ -415,7 +415,7 @@ class FunctionMiddleware(ABC): Examples: .. code-block:: python - from agent_framework import FunctionMiddleware, FunctionInvocationContext, ChatAgent + from agent_framework import FunctionMiddleware, FunctionInvocationContext, Agent class CachingMiddleware(FunctionMiddleware): @@ -439,7 +439,7 @@ class FunctionMiddleware(ABC): # Use with an agent - agent = ChatAgent(chat_client=client, name="assistant", middleware=[CachingMiddleware()]) + agent = Agent(client=client, name="assistant", middleware=[CachingMiddleware()]) """ @abstractmethod @@ -479,7 +479,7 @@ class ChatMiddleware(ABC): Examples: .. code-block:: python - from agent_framework import ChatMiddleware, ChatContext, ChatAgent + from agent_framework import ChatMiddleware, ChatContext, Agent class SystemPromptMiddleware(ChatMiddleware): @@ -488,17 +488,17 @@ class ChatMiddleware(ABC): async def process(self, context: ChatContext, call_next): # Add system prompt to messages - from agent_framework import ChatMessage + from agent_framework import Message - context.messages.insert(0, ChatMessage(role="system", text=self.system_prompt)) + context.messages.insert(0, Message(role="system", text=self.system_prompt)) # Continue execution await call_next(context) # Use with an agent - agent = ChatAgent( - chat_client=client, + agent = Agent( + client=client, name="assistant", middleware=[SystemPromptMiddleware("You are a helpful assistant.")], ) @@ -572,7 +572,7 @@ def agent_middleware(func: AgentMiddlewareCallable) -> AgentMiddlewareCallable: Examples: .. code-block:: python - from agent_framework import agent_middleware, AgentContext, ChatAgent + from agent_framework import agent_middleware, AgentContext, Agent @agent_middleware @@ -583,7 +583,7 @@ def agent_middleware(func: AgentMiddlewareCallable) -> AgentMiddlewareCallable: # Use with an agent - agent = ChatAgent(chat_client=client, name="assistant", middleware=[logging_middleware]) + agent = Agent(client=client, name="assistant", middleware=[logging_middleware]) """ # Add marker attribute to identify this as agent middleware func._middleware_type: MiddlewareType = MiddlewareType.AGENT # type: ignore @@ -605,7 +605,7 @@ def function_middleware(func: FunctionMiddlewareCallable) -> FunctionMiddlewareC Examples: .. code-block:: python - from agent_framework import function_middleware, FunctionInvocationContext, ChatAgent + from agent_framework import function_middleware, FunctionInvocationContext, Agent @function_middleware @@ -616,7 +616,7 @@ def function_middleware(func: FunctionMiddlewareCallable) -> FunctionMiddlewareC # Use with an agent - agent = ChatAgent(chat_client=client, name="assistant", middleware=[logging_middleware]) + agent = Agent(client=client, name="assistant", middleware=[logging_middleware]) """ # Add marker attribute to identify this as function middleware func._middleware_type: MiddlewareType = MiddlewareType.FUNCTION # type: ignore @@ -638,7 +638,7 @@ def chat_middleware(func: ChatMiddlewareCallable) -> ChatMiddlewareCallable: Examples: .. code-block:: python - from agent_framework import chat_middleware, ChatContext, ChatAgent + from agent_framework import chat_middleware, ChatContext, Agent @chat_middleware @@ -649,7 +649,7 @@ def chat_middleware(func: ChatMiddlewareCallable) -> ChatMiddlewareCallable: # Use with an agent - agent = ChatAgent(chat_client=client, name="assistant", middleware=[logging_middleware]) + agent = Agent(client=client, name="assistant", middleware=[logging_middleware]) """ # Add marker attribute to identify this as chat middleware func._middleware_type: MiddlewareType = MiddlewareType.CHAT # type: ignore @@ -980,7 +980,7 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: ChatOptions[ResponseModelBoundT], @@ -990,7 +990,7 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: OptionsCoT | ChatOptions[None] | None = None, @@ -1000,7 +1000,7 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[True], options: OptionsCoT | ChatOptions[Any] | None = None, @@ -1009,7 +1009,7 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]): def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: bool = False, options: OptionsCoT | ChatOptions[Any] | None = None, @@ -1035,7 +1035,7 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]): ) context = ChatContext( - chat_client=self, # type: ignore[arg-type] + client=self, # type: ignore[arg-type] messages=prepare_messages(messages), options=options, stream=stream, @@ -1090,14 +1090,14 @@ class AgentMiddlewareLayer: self.agent_middleware = middleware_list["agent"] # Pass middleware to super so BaseAgent can store it for dynamic rebuild super().__init__(*args, middleware=middleware, **kwargs) # type: ignore[call-arg] - # Note: We intentionally don't extend chat_client's middleware lists here. + # Note: We intentionally don't extend client's middleware lists here. # Chat and function middleware is passed to the chat client at runtime via kwargs # in AgentMiddlewareLayer.run(), where it's properly combined with run-level middleware. @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[False] = ..., thread: AgentThread | None = None, @@ -1109,7 +1109,7 @@ class AgentMiddlewareLayer: @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[False] = ..., thread: AgentThread | None = None, @@ -1121,7 +1121,7 @@ class AgentMiddlewareLayer: @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[True], thread: AgentThread | None = None, @@ -1132,7 +1132,7 @@ class AgentMiddlewareLayer: def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, diff --git a/python/packages/core/agent_framework/_serialization.py b/python/packages/core/agent_framework/_serialization.py index 44de6ef848..8588e0be5a 100644 --- a/python/packages/core/agent_framework/_serialization.py +++ b/python/packages/core/agent_framework/_serialization.py @@ -31,16 +31,16 @@ class SerializationProtocol(Protocol): ensuring consistent behavior across the framework. Examples: - The framework's ``ChatMessage`` class demonstrates the protocol in action: + The framework's ``Message`` class demonstrates the protocol in action: .. code-block:: python - from agent_framework import ChatMessage + from agent_framework import Message from agent_framework._serialization import SerializationProtocol - # ChatMessage implements SerializationProtocol via SerializationMixin - user_msg = ChatMessage(role="user", text="What's the weather like today?") + # Message implements SerializationProtocol via SerializationMixin + user_msg = Message(role="user", text="What's the weather like today?") # Serialize to dictionary - automatic type identification and nested serialization msg_dict = user_msg.to_dict() @@ -52,8 +52,8 @@ class SerializationProtocol(Protocol): # "additional_properties": {} # } - # Deserialize back to ChatMessage instance - automatic type reconstruction - restored_msg = ChatMessage.from_dict(msg_dict) + # Deserialize back to Message instance - automatic type reconstruction + restored_msg = Message.from_dict(msg_dict) print(restored_msg.text) # "What's the weather like today?" print(restored_msg.role) # "user" @@ -170,15 +170,15 @@ class SerializationMixin: .. code-block:: python - from agent_framework import ChatMessage + from agent_framework import Message from agent_framework._threads import AgentThreadState, ChatMessageStoreState - # ChatMessageStoreState handles nested ChatMessage serialization + # ChatMessageStoreState handles nested Message serialization store_state = ChatMessageStoreState( messages=[ - ChatMessage(role="user", text="Hello agent"), - ChatMessage(role="assistant", text="Hi! How can I help?"), + Message(role="user", text="Hello agent"), + Message(role="assistant", text="Hi! How can I help?"), ] ) @@ -443,7 +443,7 @@ class SerializationMixin: dependencies = {"open_ai_chat_client": {"client": openai_client}} # The chat client is reconstructed with the OpenAI client injected - chat_client = OpenAIChatClient.from_dict(client_data, dependencies=dependencies) + client = OpenAIChatClient.from_dict(client_data, dependencies=dependencies) # Now ready to make API calls with the injected client **Function Injection for Tools** - FunctionTool runtime dependency: diff --git a/python/packages/core/agent_framework/_sessions.py b/python/packages/core/agent_framework/_sessions.py new file mode 100644 index 0000000000..015248d844 --- /dev/null +++ b/python/packages/core/agent_framework/_sessions.py @@ -0,0 +1,521 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Unified context management types for the agent framework. + +This module provides the core types for the context provider pipeline: +- SessionContext: Per-invocation state passed through providers +- BaseContextProvider: Base class for context providers (renamed to ContextProvider in PR2) +- BaseHistoryProvider: Base class for history storage providers (renamed to HistoryProvider in PR2) +- AgentSession: Lightweight session state container +- InMemoryHistoryProvider: Built-in in-memory history provider +""" + +from __future__ import annotations + +import copy +import uuid +from abc import abstractmethod +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +from ._types import AgentResponse, Message + +if TYPE_CHECKING: + from ._agents import SupportsAgentRun + + +__all__ = [ + "AgentSession", + "BaseContextProvider", + "BaseHistoryProvider", + "InMemoryHistoryProvider", + "SessionContext", +] + + +# Registry of known types for state deserialization +_STATE_TYPE_REGISTRY: dict[str, type] = {} + + +def _register_state_type(cls: type) -> None: + """Register a type for automatic deserialization in session state.""" + type_id: str = getattr(cls, "_get_type_identifier", lambda: cls.__name__.lower())() + _STATE_TYPE_REGISTRY[type_id] = cls + + +def _serialize_value(value: Any) -> Any: + """Serialize a single value, handling objects with to_dict().""" + if hasattr(value, "to_dict") and callable(value.to_dict): + return value.to_dict() # pyright: ignore[reportUnknownMemberType] + if isinstance(value, list): + return [_serialize_value(item) for item in value] # pyright: ignore[reportUnknownVariableType] + if isinstance(value, dict): + return {str(k): _serialize_value(v) for k, v in value.items()} # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType] + return value + + +def _deserialize_value(value: Any) -> Any: + """Deserialize a single value, restoring registered types.""" + if isinstance(value, dict) and "type" in value: + type_id = str(value["type"]) # pyright: ignore[reportUnknownArgumentType] + cls = _STATE_TYPE_REGISTRY.get(type_id) + if cls is not None and hasattr(cls, "from_dict"): + return cls.from_dict(value) # type: ignore[union-attr] + if isinstance(value, list): + return [_deserialize_value(item) for item in value] # pyright: ignore[reportUnknownVariableType] + if isinstance(value, dict): + return {str(k): _deserialize_value(v) for k, v in value.items()} # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType] + return value + + +def _serialize_state(state: dict[str, Any]) -> dict[str, Any]: + """Deep-serialize a state dict, converting SerializationProtocol objects to dicts.""" + return {k: _serialize_value(v) for k, v in state.items()} + + +def _deserialize_state(state: dict[str, Any]) -> dict[str, Any]: + """Deep-deserialize a state dict, restoring SerializationProtocol objects.""" + return {k: _deserialize_value(v) for k, v in state.items()} + + +# Register known types +_register_state_type(Message) + + +class SessionContext: + """Per-invocation state passed through the context provider pipeline. + + Created fresh for each agent.run() call. Providers read from and write to + the mutable fields to add context before invocation and process responses after. + + Attributes: + session_id: The ID of the current session. + service_session_id: Service-managed session ID (if present, service handles storage). + input_messages: The new messages being sent to the agent (set by caller). + context_messages: Dict mapping source_id -> messages added by that provider. + Maintains insertion order (provider execution order). + instructions: Additional instructions added by providers. + tools: Additional tools added by providers. + response: After invocation, contains the full AgentResponse, should not be changed. + options: Options passed to agent.run() - read-only, for reflection only. + metadata: Shared metadata dictionary for cross-provider communication. + """ + + def __init__( + self, + *, + session_id: str | None = None, + service_session_id: str | None = None, + input_messages: list[Message], + context_messages: dict[str, list[Message]] | None = None, + instructions: list[str] | None = None, + tools: list[Any] | None = None, + options: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + ): + """Initialize the session context. + + Args: + session_id: The ID of the current session. + service_session_id: Service-managed session ID. + input_messages: The new messages being sent to the agent. + context_messages: Pre-populated context messages by source. + instructions: Pre-populated instructions. + tools: Pre-populated tools. + options: Options from agent.run() - read-only for providers. + metadata: Shared metadata for cross-provider communication. + """ + self.session_id = session_id + self.service_session_id = service_session_id + self.input_messages = input_messages + self.context_messages: dict[str, list[Message]] = context_messages or {} + self.instructions: list[str] = instructions or [] + self.tools: list[Any] = tools or [] + self._response: AgentResponse | None = None + self.options: dict[str, Any] = options or {} + self.metadata: dict[str, Any] = metadata or {} + + @property + def response(self) -> AgentResponse | None: + """The agent's response. Set by the framework after invocation, read-only for providers.""" + return self._response + + def extend_messages(self, source: str | object, messages: Sequence[Message]) -> None: + """Add context messages from a specific source. + + Messages are copied before attribution is added, so the caller's + original message objects are never mutated. The copies are stored + keyed by source_id, maintaining insertion order based on provider + execution order. Each message gets an ``attribution`` marker in + ``additional_properties`` for downstream filtering. + + Args: + source: Either a plain ``source_id`` string, or an object with a + ``source_id`` attribute (e.g. a context provider). When an + object is passed, its class name is recorded as + ``source_type`` in the attribution. + messages: The messages to add. + """ + if isinstance(source, str): + source_id = source + attribution: dict[str, str] = {"source_id": source_id} + else: + source_id = source.source_id # type: ignore[attr-defined] + attribution = {"source_id": source_id, "source_type": type(source).__name__} + + copied: list[Message] = [] + for message in messages: + msg_copy = copy.copy(message) + msg_copy.additional_properties = dict(message.additional_properties) + msg_copy.additional_properties.setdefault("_attribution", attribution) + copied.append(msg_copy) + if source_id not in self.context_messages: + self.context_messages[source_id] = [] + self.context_messages[source_id].extend(copied) + + def extend_instructions(self, source_id: str, instructions: str | Sequence[str]) -> None: + """Add instructions to be prepended to the conversation. + + Args: + source_id: The provider source_id adding these instructions. + instructions: A single instruction string or sequence of strings. + """ + if isinstance(instructions, str): + instructions = [instructions] + self.instructions.extend(instructions) + + def extend_tools(self, source_id: str, tools: Sequence[Any]) -> None: + """Add tools to be available for this invocation. + + Tools are added with source attribution in their metadata. + + Args: + source_id: The provider source_id adding these tools. + tools: The tools to add. + """ + for tool in tools: + if hasattr(tool, "additional_properties") and isinstance(tool.additional_properties, dict): + tool.additional_properties["context_source"] = source_id + self.tools.extend(tools) + + def get_messages( + self, + *, + sources: set[str] | None = None, + exclude_sources: set[str] | None = None, + include_input: bool = False, + include_response: bool = False, + ) -> list[Message]: + """Get context messages, optionally filtered and including input/response. + + Returns messages in provider execution order (dict insertion order), + with input and response appended if requested. + + Args: + sources: If provided, only include context messages from these sources. + exclude_sources: If provided, exclude context messages from these sources. + include_input: If True, append input_messages after context. + include_response: If True, append response.messages at the end. + + Returns: + Flattened list of messages in conversation order. + """ + result: list[Message] = [] + for source_id, messages in self.context_messages.items(): + if sources is not None and source_id not in sources: + continue + if exclude_sources is not None and source_id in exclude_sources: + continue + result.extend(messages) + if include_input and self.input_messages: + result.extend(self.input_messages) + if include_response and self.response and self.response.messages: + result.extend(self.response.messages) + return result + + +class BaseContextProvider: + """Base class for context providers (hooks pattern). + + Context providers participate in the context engineering pipeline, + adding context before model invocation and processing responses after. + + Note: + This class uses a temporary name prefixed with ``_`` to avoid collision + with the existing ``ContextProvider`` in ``_memory.py``. It will be + renamed to ``ContextProvider`` in PR2 when the old class is removed. + + Attributes: + source_id: Unique identifier for this provider instance (required). + Used for message/tool attribution so other providers can filter. + """ + + def __init__(self, source_id: str): + """Initialize the provider. + + Args: + source_id: Unique identifier for this provider instance. + """ + self.source_id = source_id + + async def before_run( + self, + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Called before model invocation. + + Override to add context (messages, instructions, tools) to the + SessionContext before the model is invoked. + + Args: + agent: The agent running this invocation. + session: The current session. + context: The invocation context - add messages/instructions/tools here. + state: The session's mutable state dict. + """ + + async def after_run( + self, + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Called after model invocation. + + Override to process the response (store messages, extract info, etc.). + The context.response will be populated at this point. + + Args: + agent: The agent that ran this invocation. + session: The current session. + context: The invocation context with response populated. + state: The session's mutable state dict. + """ + + +class BaseHistoryProvider(BaseContextProvider): + """Base class for conversation history storage providers. + + A single class configurable for different use cases: + - Primary memory storage (loads + stores messages) + - Audit/logging storage (stores only, doesn't load) + - Evaluation storage (stores only for later analysis) + + Note: + This class uses a temporary name prefixed with ``_`` to avoid collision + with existing types. It will be renamed to ``HistoryProvider`` in PR2. + + Subclasses only need to implement ``get_messages()`` and ``save_messages()``. + The default ``before_run``/``after_run`` handle loading and storing based on + configuration flags. Override them for custom behavior. + + Attributes: + load_messages: Whether to load messages before invocation (default True). + When False, the agent skips calling ``before_run`` entirely. + store_inputs: Whether to store input messages (default True). + store_context_messages: Whether to store context from other providers (default False). + store_context_from: If set, only store context from these source_ids. + store_outputs: Whether to store response messages (default True). + """ + + def __init__( + self, + source_id: str, + *, + load_messages: bool = True, + store_inputs: bool = True, + store_context_messages: bool = False, + store_context_from: set[str] | None = None, + store_outputs: bool = True, + ): + """Initialize the history provider. + + Args: + source_id: Unique identifier for this provider instance. + load_messages: Whether to load messages before invocation. + store_inputs: Whether to store input messages. + store_context_messages: Whether to store context from other providers. + store_context_from: If set, only store context from these source_ids. + store_outputs: Whether to store response messages. + """ + super().__init__(source_id) + self.load_messages = load_messages + self.store_inputs = store_inputs + self.store_context_messages = store_context_messages + self.store_context_from = store_context_from + self.store_outputs = store_outputs + + @abstractmethod + async def get_messages(self, session_id: str | None, **kwargs: Any) -> list[Message]: + """Retrieve stored messages for this session. + + Args: + session_id: The session ID to retrieve messages for. + **kwargs: Additional arguments (e.g., ``state`` for in-memory providers). + + Returns: + List of stored messages. + """ + ... + + @abstractmethod + async def save_messages(self, session_id: str | None, messages: Sequence[Message], **kwargs: Any) -> None: + """Persist messages for this session. + + Args: + session_id: The session ID to store messages for. + messages: The messages to persist. + **kwargs: Additional arguments (e.g., ``state`` for in-memory providers). + """ + ... + + def _get_context_messages_to_store(self, context: SessionContext) -> list[Message]: + """Get context messages that should be stored based on configuration.""" + if not self.store_context_messages: + return [] + if self.store_context_from is not None: + return context.get_messages(sources=self.store_context_from) + return context.get_messages(exclude_sources={self.source_id}) + + async def before_run( + self, + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Load history into context. Skipped by the agent when load_messages=False.""" + history = await self.get_messages(context.session_id, state=state) + context.extend_messages(self, history) + + async def after_run( + self, + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Store messages based on configuration.""" + messages_to_store: list[Message] = [] + messages_to_store.extend(self._get_context_messages_to_store(context)) + if self.store_inputs: + messages_to_store.extend(context.input_messages) + if self.store_outputs and context.response and context.response.messages: + messages_to_store.extend(context.response.messages) + if messages_to_store: + await self.save_messages(context.session_id, messages_to_store, state=state) + + +class AgentSession: + """A conversation session with an agent. + + Lightweight state container. Provider instances are owned by the agent, + not the session. The session only holds session IDs and a mutable state dict. + + Attributes: + session_id: Unique identifier for this session. + service_session_id: Service-managed session ID (if using service-side storage). + state: Mutable state dict shared with all providers. + """ + + def __init__( + self, + *, + session_id: str | None = None, + service_session_id: str | None = None, + ): + """Initialize the session. + + Args: + session_id: Optional session ID (generated if not provided). + service_session_id: Optional service-managed session ID. + """ + self._session_id = session_id or str(uuid.uuid4()) + self.service_session_id = service_session_id + self.state: dict[str, Any] = {} + + @property + def session_id(self) -> str: + """The unique identifier for this session.""" + return self._session_id + + def to_dict(self) -> dict[str, Any]: + """Serialize session to a plain dict for storage/transfer. + + Values in ``state`` that implement ``SerializationProtocol`` (i.e. have + ``to_dict``/``from_dict``) are serialized automatically. Built-in types + (str, int, float, bool, None, list, dict) are kept as-is. + """ + return { + "type": "session", + "session_id": self._session_id, + "service_session_id": self.service_session_id, + "state": _serialize_state(self.state), + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> AgentSession: + """Restore session from a previously serialized dict. + + Values in ``state`` that were serialized via ``SerializationProtocol`` + (containing a ``type`` key) are restored to their original types. + + Args: + data: Dict from a previous ``to_dict()`` call. + + Returns: + Restored AgentSession instance. + """ + session = cls( + session_id=data["session_id"], + service_session_id=data.get("service_session_id"), + ) + session.state = _deserialize_state(data.get("state", {})) + return session + + +class InMemoryHistoryProvider(BaseHistoryProvider): + """Built-in history provider that stores messages in session.state. + + Messages are stored in ``state[source_id]["messages"]`` as a list of + ``Message`` objects. Serialization to/from dicts is handled by + ``AgentSession.to_dict()``/``from_dict()`` using ``SerializationProtocol``. + + This provider holds no instance state — all data lives in the session's + state dict, passed as a named ``state`` parameter to ``get_messages``/``save_messages``. + + This is the default provider auto-added by the agent when no providers + are configured and ``conversation_id`` or ``store=True`` is set. + """ + + async def get_messages( + self, session_id: str | None, *, state: dict[str, Any] | None = None, **kwargs: Any + ) -> list[Message]: + """Retrieve messages from session state.""" + if state is None: + return [] + my_state = state.get(self.source_id, {}) + return list(my_state.get("messages", [])) + + async def save_messages( + self, + session_id: str | None, + messages: Sequence[Message], + *, + state: dict[str, Any] | None = None, + **kwargs: Any, + ) -> None: + """Persist messages to session state.""" + if state is None: + return + my_state = state.setdefault(self.source_id, {}) + existing = my_state.get("messages", []) + my_state["messages"] = [*existing, *messages] diff --git a/python/packages/core/agent_framework/_threads.py b/python/packages/core/agent_framework/_threads.py index df142643f6..83b33519d8 100644 --- a/python/packages/core/agent_framework/_threads.py +++ b/python/packages/core/agent_framework/_threads.py @@ -7,7 +7,7 @@ from typing import Any, Protocol, TypeVar from ._memory import ContextProvider from ._serialization import SerializationMixin -from ._types import ChatMessage +from ._types import Message from .exceptions import AgentThreadException __all__ = ["AgentThread", "ChatMessageStore", "ChatMessageStoreProtocol"] @@ -22,17 +22,17 @@ class ChatMessageStoreProtocol(Protocol): Examples: .. code-block:: python - from agent_framework import ChatMessage + from agent_framework import Message class MyMessageStore: def __init__(self): self._messages = [] - async def list_messages(self) -> list[ChatMessage]: + async def list_messages(self) -> list[Message]: return self._messages - async def add_messages(self, messages: Sequence[ChatMessage]) -> None: + async def add_messages(self, messages: Sequence[Message]) -> None: self._messages.extend(messages) @classmethod @@ -52,7 +52,7 @@ class ChatMessageStoreProtocol(Protocol): store = MyMessageStore() """ - async def list_messages(self) -> list[ChatMessage]: + async def list_messages(self) -> list[Message]: """Gets all the messages from the store that should be used for the next agent invocation. Messages are returned in ascending chronological order, with the oldest message first. @@ -65,11 +65,11 @@ class ChatMessageStoreProtocol(Protocol): """ ... - async def add_messages(self, messages: Sequence[ChatMessage]) -> None: + async def add_messages(self, messages: Sequence[Message]) -> None: """Adds messages to the store. Args: - messages: The sequence of ChatMessage objects to add to the store. + messages: The sequence of Message objects to add to the store. """ ... @@ -128,7 +128,7 @@ class ChatMessageStoreState(SerializationMixin): def __init__( self, - messages: Sequence[ChatMessage] | Sequence[MutableMapping[str, Any]] | None = None, + messages: Sequence[Message] | Sequence[MutableMapping[str, Any]] | None = None, **kwargs: Any, ) -> None: """Create the store state. @@ -141,16 +141,16 @@ class ChatMessageStoreState(SerializationMixin): """ if not messages: - self.messages: list[ChatMessage] = [] + self.messages: list[Message] = [] return if not isinstance(messages, list): raise TypeError("Messages should be a list") - new_messages: list[ChatMessage] = [] + new_messages: list[Message] = [] for msg in messages: - if isinstance(msg, ChatMessage): + if isinstance(msg, Message): new_messages.append(msg) else: - new_messages.append(ChatMessage.from_dict(msg)) + new_messages.append(Message.from_dict(msg)) self.messages = new_messages @@ -198,13 +198,13 @@ class ChatMessageStore: Examples: .. code-block:: python - from agent_framework import ChatMessageStore, ChatMessage + from agent_framework import ChatMessageStore, Message # Create an empty store store = ChatMessageStore() # Add messages - message = ChatMessage(role="user", text="Hello") + message = Message(role="user", text="Hello") await store.add_messages([message]) # Retrieve messages @@ -217,7 +217,7 @@ class ChatMessageStore: restored_store = await ChatMessageStore.deserialize(state) """ - def __init__(self, messages: Sequence[ChatMessage] | None = None): + def __init__(self, messages: Sequence[Message] | None = None): """Create a ChatMessageStore for use in a thread. Args: @@ -225,19 +225,19 @@ class ChatMessageStore: """ self.messages = list(messages) if messages else [] - async def add_messages(self, messages: Sequence[ChatMessage]) -> None: + async def add_messages(self, messages: Sequence[Message]) -> None: """Add messages to the store. Args: - messages: Sequence of ChatMessage objects to add to the store. + messages: Sequence of Message objects to add to the store. """ self.messages.extend(messages) - async def list_messages(self) -> list[ChatMessage]: + async def list_messages(self) -> list[Message]: """Get all messages from the store in chronological order. Returns: - List of ChatMessage objects, ordered from oldest to newest. + List of Message objects, ordered from oldest to newest. """ return self.messages @@ -302,21 +302,21 @@ class AgentThread: Examples: .. code-block:: python - from agent_framework import ChatAgent, ChatMessageStore + from agent_framework import Agent, ChatMessageStore from agent_framework.openai import OpenAIChatClient client = OpenAIChatClient(model="gpt-4o") # Create agent with service-managed threads using a service_thread_id - service_agent = ChatAgent(name="assistant", client=client) + service_agent = Agent(name="assistant", client=client) service_thread = await service_agent.get_new_thread(service_thread_id="thread_abc123") # Create agent with service-managed threads using conversation_id - conversation_agent = ChatAgent(name="assistant", client=client, conversation_id="thread_abc123") + conversation_agent = Agent(name="assistant", client=client, conversation_id="thread_abc123") conversation_thread = await conversation_agent.get_new_thread() # Create agent with custom message store factory - local_agent = ChatAgent(name="assistant", client=client, chat_message_store_factory=ChatMessageStore) + local_agent = Agent(name="assistant", client=client, chat_message_store_factory=ChatMessageStore) local_thread = await local_agent.get_new_thread() # Serialize and restore thread state @@ -401,11 +401,11 @@ class AgentThread: self._message_store = message_store - async def on_new_messages(self, new_messages: ChatMessage | Sequence[ChatMessage]) -> None: + async def on_new_messages(self, new_messages: Message | Sequence[Message]) -> None: """Invoked when a new message has been contributed to the chat by any participant. Args: - new_messages: The new ChatMessage or sequence of ChatMessage objects to add to the thread. + new_messages: The new Message or sequence of Message objects to add to the thread. """ if self._service_thread_id is not None: # If the thread messages are stored in the service there is nothing to do here, @@ -416,7 +416,7 @@ class AgentThread: # create a default in memory store. self._message_store = ChatMessageStore() # If a store has been provided, we need to add the messages to the store. - if isinstance(new_messages, ChatMessage): + if isinstance(new_messages, Message): new_messages = [new_messages] await self._message_store.add_messages(new_messages) diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 0d31471aba..b838551f81 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -10,7 +10,6 @@ from collections.abc import ( AsyncIterable, Awaitable, Callable, - Collection, Mapping, MutableMapping, Sequence, @@ -25,18 +24,16 @@ from typing import ( Final, Generic, Literal, - Protocol, TypedDict, Union, cast, get_args, get_origin, overload, - runtime_checkable, ) from opentelemetry.metrics import Histogram, NoOpHistogram -from pydantic import AnyUrl, BaseModel, Field, ValidationError, create_model +from pydantic import BaseModel, Field, ValidationError, create_model from ._logging import get_logger from ._serialization import SerializationMixin @@ -58,21 +55,17 @@ if sys.version_info >= (3, 12): from typing import override # type: ignore # pragma: no cover else: from typing_extensions import override # type: ignore[import] # pragma: no cover -if sys.version_info >= (3, 11): - from typing import TypedDict # type: ignore # pragma: no cover -else: - from typing_extensions import TypedDict # type: ignore # pragma: no cover if TYPE_CHECKING: - from ._clients import ChatClientProtocol + from ._clients import SupportsChatGetResponse from ._middleware import FunctionMiddlewarePipeline, FunctionMiddlewareTypes from ._types import ( - ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, Content, + Message, ResponseStream, ) @@ -85,13 +78,6 @@ __all__ = [ "FunctionInvocationConfiguration", "FunctionInvocationLayer", "FunctionTool", - "HostedCodeInterpreterTool", - "HostedFileSearchTool", - "HostedImageGenerationTool", - "HostedMCPSpecificApproval", - "HostedMCPTool", - "HostedWebSearchTool", - "ToolProtocol", "normalize_function_invocation_configuration", "tool", ] @@ -100,7 +86,7 @@ __all__ = [ logger = get_logger() DEFAULT_MAX_ITERATIONS: Final[int] = 40 DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST: Final[int] = 3 -ChatClientT = TypeVar("ChatClientT", bound="ChatClientProtocol[Any]") +ChatClientT = TypeVar("ChatClientT", bound="SupportsChatGetResponse[Any]") # region Helpers ArgsT = TypeVar("ArgsT", bound=BaseModel, default=BaseModel) @@ -163,380 +149,6 @@ def _parse_inputs( # region Tools -@runtime_checkable -class ToolProtocol(Protocol): - """Represents a generic tool. - - This protocol defines the interface that all tools must implement to be compatible - with the agent framework. It is implemented by various tool classes such as HostedMCPTool, - HostedWebSearchTool, and FunctionTool's. A FunctionTool is usually created by the `tool` decorator. - - Since each connector needs to parse tools differently, users can pass a dict to - specify a service-specific tool when no abstraction is available. - - Attributes: - name: The name of the tool. - description: A description of the tool, suitable for use in describing the purpose to a model. - additional_properties: Additional properties associated with the tool. - """ - - name: str - """The name of the tool.""" - description: str - """A description of the tool, suitable for use in describing the purpose to a model.""" - additional_properties: dict[str, Any] | None - """Additional properties associated with the tool.""" - - def __str__(self) -> str: - """Return a string representation of the tool.""" - ... - - -class BaseTool(SerializationMixin): - """Base class for AI tools, providing common attributes and methods. - - Used as the base class for the various tools in the agent framework, such as HostedMCPTool, - HostedWebSearchTool, and FunctionTool. - - Since each connector needs to parse tools differently, this class is not exposed directly to end users. - In most cases, users can pass a dict to specify a service-specific tool when no abstraction is available. - """ - - DEFAULT_EXCLUDE: ClassVar[set[str]] = {"additional_properties"} - - def __init__( - self, - *, - name: str, - description: str = "", - additional_properties: dict[str, Any] | None = None, - **kwargs: Any, - ) -> None: - """Initialize the BaseTool. - - Keyword Args: - name: The name of the tool. - description: A description of the tool. - additional_properties: Additional properties associated with the tool. - **kwargs: Additional keyword arguments. - """ - self.name = name - self.description = description - self.additional_properties = additional_properties - for key, value in kwargs.items(): - setattr(self, key, value) - - def __str__(self) -> str: - """Return a string representation of the tool.""" - if self.description: - return f"{self.__class__.__name__}(name={self.name}, description={self.description})" - return f"{self.__class__.__name__}(name={self.name})" - - -class HostedCodeInterpreterTool(BaseTool): - """Represents a hosted tool that can be specified to an AI service to enable it to execute generated code. - - This tool does not implement code interpretation itself. It serves as a marker to inform a service - that it is allowed to execute generated code if the service is capable of doing so. - - Examples: - .. code-block:: python - - from agent_framework import HostedCodeInterpreterTool - - # Create a code interpreter tool - code_tool = HostedCodeInterpreterTool() - - # With file inputs - code_tool_with_files = HostedCodeInterpreterTool(inputs=[{"file_id": "file-123"}, {"file_id": "file-456"}]) - """ - - def __init__( - self, - *, - inputs: Content | dict[str, Any] | str | list[Content | dict[str, Any] | str] | None = None, - description: str | None = None, - additional_properties: dict[str, Any] | None = None, - **kwargs: Any, - ) -> None: - """Initialize the HostedCodeInterpreterTool. - - Keyword Args: - inputs: A list of contents that the tool can accept as input. Defaults to None. - This should mostly be HostedFileContent or HostedVectorStoreContent. - Can also be DataContent, depending on the service used. - When supplying a list, it can contain: - - Content instances - - dicts with properties for Content (e.g., {"uri": "http://example.com", "media_type": "text/html"}) - - strings (which will be converted to UriContent with media_type "text/plain"). - If None, defaults to an empty list. - description: A description of the tool. - additional_properties: Additional properties associated with the tool. - **kwargs: Additional keyword arguments to pass to the base class. - """ - if "name" in kwargs: - raise ValueError("The 'name' argument is reserved for the HostedCodeInterpreterTool and cannot be set.") - - self.inputs = _parse_inputs(inputs) if inputs else [] - - super().__init__( - name="code_interpreter", - description=description or "", - additional_properties=additional_properties, - **kwargs, - ) - - -class HostedWebSearchTool(BaseTool): - """Represents a web search tool that can be specified to an AI service to enable it to perform web searches. - - Examples: - .. code-block:: python - - from agent_framework import HostedWebSearchTool - - # Create a basic web search tool - search_tool = HostedWebSearchTool() - - # With location context - search_tool_with_location = HostedWebSearchTool( - description="Search the web for information", - additional_properties={"user_location": {"city": "Seattle", "country": "US"}}, - ) - """ - - def __init__( - self, - description: str | None = None, - additional_properties: dict[str, Any] | None = None, - **kwargs: Any, - ): - """Initialize a HostedWebSearchTool. - - Keyword Args: - description: A description of the tool. - additional_properties: Additional properties associated with the tool - (e.g., {"user_location": {"city": "Seattle", "country": "US"}}). - **kwargs: Additional keyword arguments to pass to the base class. - if additional_properties is not provided, any kwargs will be added to additional_properties. - """ - args: dict[str, Any] = { - "name": "web_search", - } - if additional_properties is not None: - args["additional_properties"] = additional_properties - elif kwargs: - args["additional_properties"] = kwargs - if description is not None: - args["description"] = description - super().__init__(**args) - - -class HostedImageGenerationToolOptions(TypedDict, total=False): - """Options for HostedImageGenerationTool.""" - - count: int - image_size: str - media_type: str - model_id: str - response_format: Literal["uri", "data", "hosted"] - streaming_count: int - - -class HostedImageGenerationTool(BaseTool): - """Represents a hosted tool that can be specified to an AI service to enable it to perform image generation.""" - - def __init__( - self, - *, - options: HostedImageGenerationToolOptions | None = None, - description: str | None = None, - additional_properties: dict[str, Any] | None = None, - **kwargs: Any, - ): - """Initialize a HostedImageGenerationTool.""" - if "name" in kwargs: - raise ValueError("The 'name' argument is reserved for the HostedImageGenerationTool and cannot be set.") - - self.options = options - super().__init__( - name="image_generation", - description=description or "", - additional_properties=additional_properties, - **kwargs, - ) - - -class HostedMCPSpecificApproval(TypedDict, total=False): - """Represents the specific mode for a hosted tool. - - When using this mode, the user must specify which tools always or never require approval. - This is represented as a dictionary with two optional keys: - - Attributes: - always_require_approval: A sequence of tool names that always require approval. - never_require_approval: A sequence of tool names that never require approval. - """ - - always_require_approval: Collection[str] | None - never_require_approval: Collection[str] | None - - -class HostedMCPTool(BaseTool): - """Represents a MCP tool that is managed and executed by the service. - - Examples: - .. code-block:: python - - from agent_framework import HostedMCPTool - - # Create a basic MCP tool - mcp_tool = HostedMCPTool( - name="my_mcp_tool", - url="https://example.com/mcp", - ) - - # With approval mode and allowed tools - mcp_tool_with_approval = HostedMCPTool( - name="my_mcp_tool", - description="My MCP tool", - url="https://example.com/mcp", - approval_mode="always_require", - allowed_tools=["tool1", "tool2"], - headers={"Authorization": "Bearer token"}, - ) - - # With specific approval mode - mcp_tool_specific = HostedMCPTool( - name="my_mcp_tool", - url="https://example.com/mcp", - approval_mode={ - "always_require_approval": ["dangerous_tool"], - "never_require_approval": ["safe_tool"], - }, - ) - """ - - def __init__( - self, - *, - name: str, - description: str | None = None, - url: AnyUrl | str, - approval_mode: Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None = None, - allowed_tools: Collection[str] | None = None, - headers: dict[str, str] | None = None, - additional_properties: dict[str, Any] | None = None, - **kwargs: Any, - ) -> None: - """Create a hosted MCP tool. - - Keyword Args: - name: The name of the tool. - description: A description of the tool. - url: The URL of the tool. - approval_mode: The approval mode for the tool. This can be: - - "always_require": The tool always requires approval before use. - - "never_require": The tool never requires approval before use. - - A dict with keys `always_require_approval` or `never_require_approval`, - followed by a sequence of strings with the names of the relevant tools. - allowed_tools: A list of tools that are allowed to use this tool. - headers: Headers to include in requests to the tool. - additional_properties: Additional properties to include in the tool definition. - **kwargs: Additional keyword arguments to pass to the base class. - """ - try: - # Validate approval_mode - if approval_mode is not None: - if isinstance(approval_mode, str): - if approval_mode not in ("always_require", "never_require"): - raise ValueError( - f"Invalid approval_mode: {approval_mode}. " - "Must be 'always_require', 'never_require', or a dict with 'always_require_approval' " - "or 'never_require_approval' keys." - ) - elif isinstance(approval_mode, dict): - # Validate that the dict has sets - for key, value in approval_mode.items(): - if not isinstance(value, set): - approval_mode[key] = set(value) # type: ignore - - # Validate allowed_tools - if allowed_tools is not None and isinstance(allowed_tools, dict): - raise TypeError( - f"allowed_tools must be a sequence of strings, not a dict. Got: {type(allowed_tools).__name__}" - ) - - super().__init__( - name=name, - description=description or "", - additional_properties=additional_properties, - **kwargs, - ) - self.url = url if isinstance(url, AnyUrl) else AnyUrl(url) - self.approval_mode = approval_mode - self.allowed_tools = set(allowed_tools) if allowed_tools else None - self.headers = headers - except (ValidationError, ValueError, TypeError) as err: - raise ToolException(f"Error initializing HostedMCPTool: {err}", inner_exception=err) from err - - -class HostedFileSearchTool(BaseTool): - """Represents a file search tool that can be specified to an AI service to enable it to perform file searches. - - Examples: - .. code-block:: python - - from agent_framework import HostedFileSearchTool - - # Create a basic file search tool - file_search = HostedFileSearchTool() - - # With vector store inputs and max results - file_search_with_inputs = HostedFileSearchTool( - inputs=[{"vector_store_id": "vs_123"}], - max_results=10, - description="Search files in vector store", - ) - """ - - def __init__( - self, - *, - inputs: Content | dict[str, Any] | str | list[Content | dict[str, Any] | str] | None = None, - max_results: int | None = None, - description: str | None = None, - additional_properties: dict[str, Any] | None = None, - **kwargs: Any, - ): - """Initialize a FileSearchTool. - - Keyword Args: - inputs: A list of contents that the tool can accept as input. Defaults to None. - This should be one or more HostedVectorStoreContents. - When supplying a list, it can contain: - - Content instances - - dicts with properties for Content (e.g., {"uri": "http://example.com", "media_type": "text/html"}) - - strings (which will be converted to UriContent with media_type "text/plain"). - If None, defaults to an empty list. - max_results: The maximum number of results to return from the file search. - If None, max limit is applied. - description: A description of the tool. - additional_properties: Additional properties associated with the tool. - **kwargs: Additional keyword arguments to pass to the base class. - """ - if "name" in kwargs: - raise ValueError("The 'name' argument is reserved for the HostedFileSearchTool and cannot be set.") - - self.inputs = _parse_inputs(inputs) if inputs else None - self.max_results = max_results - - super().__init__( - name="file_search", - description=description or "", - additional_properties=additional_properties, - **kwargs, - ) def _default_histogram() -> Histogram: @@ -576,12 +188,17 @@ class EmptyInputModel(BaseModel): """An empty input model for functions with no parameters.""" -class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]): +class FunctionTool(SerializationMixin, Generic[ArgsT, ReturnT]): """A tool that wraps a Python function to make it callable by AI models. This class wraps a Python function to make it callable by AI models with automatic parameter validation and JSON schema generation. + Attributes: + name: The name of the tool. + description: A description of the tool, suitable for use in describing the purpose to a model. + additional_properties: Additional properties associated with the tool. + Examples: .. code-block:: python @@ -619,7 +236,12 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]): """ INJECTABLE: ClassVar[set[str]] = {"func"} - DEFAULT_EXCLUDE: ClassVar[set[str]] = {"input_model", "_invocation_duration_histogram", "_cached_parameters"} + DEFAULT_EXCLUDE: ClassVar[set[str]] = { + "additional_properties", + "input_model", + "_invocation_duration_histogram", + "_cached_parameters", + } def __init__( self, @@ -661,12 +283,14 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]): the expected arguments. **kwargs: Additional keyword arguments. """ - super().__init__( - name=name, - description=description, - additional_properties=additional_properties, - **kwargs, - ) + # Core attributes (formerly from BaseTool) + self.name = name + self.description = description + self.additional_properties = additional_properties + for key, value in kwargs.items(): + setattr(self, key, value) + + # FunctionTool-specific attributes self.func = func self._instance = None # Store the instance for bound methods self.input_model = self._resolve_input_model(input_model) @@ -690,6 +314,12 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]): self._forward_runtime_kwargs = True break + def __str__(self) -> str: + """Return a string representation of the tool.""" + if self.description: + return f"{self.__class__.__name__}(name={self.name}, description={self.description})" + return f"{self.__class__.__name__}(name={self.name})" + @property def declaration_only(self) -> bool: """Indicate whether the function is declaration only (i.e., has no implementation).""" @@ -907,10 +537,10 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]): def _tools_to_dict( tools: ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None ), ) -> list[str | dict[str, Any]] | None: @@ -1464,7 +1094,7 @@ class FunctionInvocationConfiguration(TypedDict, total=False): max_iterations: int max_consecutive_errors_per_request: int terminate_on_unknown_calls: bool - additional_tools: Sequence[ToolProtocol] + additional_tools: Sequence[FunctionTool] include_detailed_errors: bool @@ -1638,10 +1268,10 @@ async def _auto_invoke_function( def _get_tool_map( - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]], + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]], ) -> dict[str, FunctionTool[Any, Any]]: tool_list: dict[str, FunctionTool[Any, Any]] = {} for tool_item in tools if isinstance(tools, list) else [tools]: @@ -1659,10 +1289,10 @@ async def _try_execute_function_calls( custom_args: dict[str, Any], attempt_idx: int, function_calls: Sequence[Content], - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]], + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]], config: FunctionInvocationConfiguration, middleware_pipeline: Any = None, # Optional MiddlewarePipeline to avoid circular imports ) -> tuple[Sequence[Content], bool]: @@ -1848,8 +1478,8 @@ def _extract_tools(options: dict[str, Any] | None) -> Any: options: The options dict containing chat options. Returns: - ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | - Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] | None + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | + Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None """ if options and isinstance(options, dict): return options.get("tools") @@ -1857,14 +1487,14 @@ def _extract_tools(options: dict[str, Any] | None) -> Any: def _collect_approval_responses( - messages: list[ChatMessage], + messages: list[Message], ) -> dict[str, Content]: """Collect approval responses (both approved and rejected) from messages.""" - from ._types import ChatMessage + from ._types import Message fcc_todo: dict[str, Content] = {} for msg in messages: - for content in msg.contents if isinstance(msg, ChatMessage) else []: + for content in msg.contents if isinstance(msg, Message) else []: # Collect BOTH approved and rejected responses if content.type == "function_approval_response": fcc_todo[content.id] = content # type: ignore[attr-defined, index] @@ -1872,7 +1502,7 @@ def _collect_approval_responses( def _replace_approval_contents_with_results( - messages: list[ChatMessage], + messages: list[Message], fcc_todo: dict[str, Content], approved_function_results: list[Content], ) -> None: @@ -1941,7 +1571,7 @@ def _extract_function_calls(response: ChatResponse) -> list[Content]: ] -def _prepend_fcc_messages(response: ChatResponse, fcc_messages: list[ChatMessage]) -> None: +def _prepend_fcc_messages(response: ChatResponse, fcc_messages: list[Message]) -> None: if not fcc_messages: return for msg in reversed(fcc_messages): @@ -1961,7 +1591,7 @@ class FunctionRequestResult(TypedDict, total=False): action: Literal["return", "continue", "stop"] errors_in_a_row: int - result_message: ChatMessage | None + result_message: Message | None update_role: Literal["assistant", "tool"] | None function_call_results: list[Content] | None @@ -1970,12 +1600,12 @@ def _handle_function_call_results( *, response: ChatResponse, function_call_results: list[Content], - fcc_messages: list[ChatMessage], + fcc_messages: list[Message], errors_in_a_row: int, had_errors: bool, max_errors: int, ) -> FunctionRequestResult: - from ._types import ChatMessage + from ._types import Message if any(fccr.type in {"function_approval_request", "function_call"} for fccr in function_call_results): # Only add items that aren't already in the message (e.g. function_approval_request wrappers). @@ -1985,7 +1615,7 @@ def _handle_function_call_results( if response.messages and response.messages[0].role == "assistant": response.messages[0].contents.extend(new_items) else: - response.messages.append(ChatMessage(role="assistant", contents=new_items)) + response.messages.append(Message(role="assistant", contents=new_items)) return { "action": "return", "errors_in_a_row": errors_in_a_row, @@ -2012,7 +1642,7 @@ def _handle_function_call_results( else: errors_in_a_row = 0 - result_message = ChatMessage(role="tool", contents=function_call_results) + result_message = Message(role="tool", contents=function_call_results) response.messages.append(result_message) fcc_messages.extend(response.messages) return { @@ -2027,10 +1657,10 @@ def _handle_function_call_results( async def _process_function_requests( *, response: ChatResponse | None, - prepped_messages: list[ChatMessage] | None, + prepped_messages: list[Message] | None, tool_options: dict[str, Any] | None, attempt_idx: int, - fcc_messages: list[ChatMessage] | None, + fcc_messages: list[Message] | None, errors_in_a_row: int, max_errors: int, execute_function_calls: Callable[..., Awaitable[tuple[list[Content], bool, bool]]], @@ -2139,7 +1769,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: ChatOptions[ResponseModelBoundT], @@ -2149,7 +1779,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: OptionsCoT | ChatOptions[None] | None = None, @@ -2159,7 +1789,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[True], options: OptionsCoT | ChatOptions[Any] | None = None, @@ -2168,7 +1798,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: bool = False, options: OptionsCoT | ChatOptions[Any] | None = None, @@ -2213,7 +1843,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): nonlocal filtered_kwargs errors_in_a_row: int = 0 prepped_messages = prepare_messages(messages) - fcc_messages: list[ChatMessage] = [] + fcc_messages: list[Message] = [] response: ChatResponse | None = None for attempt_idx in range( @@ -2307,7 +1937,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): nonlocal stream_result_hooks errors_in_a_row: int = 0 prepped_messages = prepare_messages(messages) - fcc_messages: list[ChatMessage] = [] + fcc_messages: list[Message] = [] response: ChatResponse | None = None for attempt_idx in range( diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 9c28a5a7e1..7d8b5a7909 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -15,7 +15,7 @@ from pydantic import BaseModel from ._logging import get_logger from ._serialization import SerializationMixin -from ._tools import ToolProtocol, tool +from ._tools import FunctionTool, tool from .exceptions import AdditionItemMismatch, ContentError if sys.version_info >= (3, 13): @@ -31,14 +31,15 @@ __all__ = [ "AgentResponse", "AgentResponseUpdate", "Annotation", - "ChatMessage", "ChatOptions", "ChatResponse", "ChatResponseUpdate", "Content", + "ContinuationToken", "FinalT", "FinishReason", "FinishReasonLiteral", + "Message", "OuterFinalT", "OuterUpdateT", "ResponseStream", @@ -1419,14 +1420,14 @@ Known values: "system", "user", "assistant", "tool" Examples: .. code-block:: python - from agent_framework import ChatMessage + from agent_framework import Message # Use string values directly - user_msg = ChatMessage("user", ["Hello"]) - assistant_msg = ChatMessage("assistant", ["Hi there!"]) + user_msg = Message("user", ["Hello"]) + assistant_msg = Message("assistant", ["Hi there!"]) # Custom roles are also supported - custom_msg = ChatMessage("custom", ["Custom role message"]) + custom_msg = Message("custom", ["Custom role message"]) # Compare roles directly as strings if user_msg.role == "user": @@ -1460,10 +1461,10 @@ Examples: """ -# region ChatMessage +# region Message -class ChatMessage(SerializationMixin): +class Message(SerializationMixin): """Represents a chat message. Attributes: @@ -1478,17 +1479,17 @@ class ChatMessage(SerializationMixin): Examples: .. code-block:: python - from agent_framework import ChatMessage, Content + from agent_framework import Message, Content # Create a message with text content - user_msg = ChatMessage("user", ["What's the weather?"]) + user_msg = Message("user", ["What's the weather?"]) print(user_msg.text) # "What's the weather?" # Create a system message - system_msg = ChatMessage("system", ["You are a helpful assistant."]) + system_msg = Message("system", ["You are a helpful assistant."]) # Create a message with mixed content types - assistant_msg = ChatMessage( + assistant_msg = Message( "assistant", ["The weather is sunny!", Content.from_image_uri("https://...")], ) @@ -1498,13 +1499,13 @@ class ChatMessage(SerializationMixin): msg_dict = user_msg.to_dict() # {'type': 'chat_message', 'role': 'user', # 'contents': [{'type': 'text', 'text': "What's the weather?"}], 'additional_properties': {}} - restored_msg = ChatMessage.from_dict(msg_dict) + restored_msg = Message.from_dict(msg_dict) print(restored_msg.text) # "What's the weather?" # Serialization - to_json and from_json msg_json = user_msg.to_json() # '{"type": "chat_message", "role": "user", "contents": [...], ...}' - restored_from_json = ChatMessage.from_json(msg_json) + restored_from_json = Message.from_json(msg_json) print(restored_from_json.role) # "user" """ @@ -1522,7 +1523,7 @@ class ChatMessage(SerializationMixin): additional_properties: MutableMapping[str, Any] | None = None, raw_representation: Any | None = None, ) -> None: - """Initialize ChatMessage. + """Initialize Message. Args: role: The role of the author of the message (e.g., "user", "assistant", "system", "tool"). @@ -1567,86 +1568,86 @@ class ChatMessage(SerializationMixin): def prepare_messages( - messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage], + messages: str | Content | Message | Sequence[str | Content | Message], system_instructions: str | Sequence[str] | None = None, -) -> list[ChatMessage]: - """Convert various message input formats into a list of ChatMessage objects. +) -> list[Message]: + """Convert various message input formats into a list of Message objects. Args: messages: The input messages in various supported formats. Can be: - A string (converted to a user message) - - A Content object (wrapped in a user ChatMessage) - - A ChatMessage object + - A Content object (wrapped in a user Message) + - A Message object - A sequence containing any mix of the above system_instructions: The system instructions. They will be inserted to the start of the messages list. Returns: - A list of ChatMessage objects. + A list of Message objects. """ if system_instructions is not None: if isinstance(system_instructions, str): system_instructions = [system_instructions] - system_instruction_messages = [ChatMessage("system", [instr]) for instr in system_instructions] + system_instruction_messages = [Message("system", [instr]) for instr in system_instructions] else: system_instruction_messages = [] if isinstance(messages, str): - return [*system_instruction_messages, ChatMessage("user", [messages])] + return [*system_instruction_messages, Message("user", [messages])] if isinstance(messages, Content): - return [*system_instruction_messages, ChatMessage("user", [messages])] - if isinstance(messages, ChatMessage): + return [*system_instruction_messages, Message("user", [messages])] + if isinstance(messages, Message): return [*system_instruction_messages, messages] - return_messages: list[ChatMessage] = system_instruction_messages + return_messages: list[Message] = system_instruction_messages for msg in messages: if isinstance(msg, (str, Content)): - msg = ChatMessage("user", [msg]) + msg = Message("user", [msg]) return_messages.append(msg) return return_messages def normalize_messages( - messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, -) -> list[ChatMessage]: - """Normalize message inputs to a list of ChatMessage objects. + messages: str | Content | Message | Sequence[str | Content | Message] | None = None, +) -> list[Message]: + """Normalize message inputs to a list of Message objects. Args: messages: The input messages in various supported formats. Can be: - None (returns empty list) - A string (converted to a user message) - - A Content object (wrapped in a user ChatMessage) - - A ChatMessage object + - A Content object (wrapped in a user Message) + - A Message object - A sequence containing any mix of the above Returns: - A list of ChatMessage objects. + A list of Message objects. """ if messages is None: return [] if isinstance(messages, str): - return [ChatMessage("user", [messages])] + return [Message("user", [messages])] if isinstance(messages, Content): - return [ChatMessage("user", [messages])] + return [Message("user", [messages])] - if isinstance(messages, ChatMessage): + if isinstance(messages, Message): return [messages] - result: list[ChatMessage] = [] + result: list[Message] = [] for msg in messages: if isinstance(msg, (str, Content)): - result.append(ChatMessage("user", [msg])) + result.append(Message("user", [msg])) else: result.append(msg) return result def prepend_instructions_to_messages( - messages: list[ChatMessage], + messages: list[Message], instructions: str | Sequence[str] | None, role: RoleLiteral | str = "system", -) -> list[ChatMessage]: +) -> list[Message]: """Prepend instructions to a list of messages with a specified role. This is a helper method for chat clients that need to add instructions @@ -1654,7 +1655,7 @@ def prepend_instructions_to_messages( instructions (e.g., OpenAI uses "system", some providers might use "user"). Args: - messages: The existing list of ChatMessage objects. + messages: The existing list of Message objects. instructions: The instructions to prepend. Can be a single string or a sequence of strings. role: The role to use for the instruction messages. Defaults to "system". @@ -1664,9 +1665,9 @@ def prepend_instructions_to_messages( Examples: .. code-block:: python - from agent_framework import prepend_instructions_to_messages, ChatMessage + from agent_framework import prepend_instructions_to_messages, Message - messages = [ChatMessage("user", ["Hello"])] + messages = [Message("user", ["Hello"])] instructions = "You are a helpful assistant" # Prepend as system message (default) @@ -1681,7 +1682,7 @@ def prepend_instructions_to_messages( if isinstance(instructions, str): instructions = [instructions] - instruction_messages = [ChatMessage(role, [instr]) for instr in instructions] + instruction_messages = [Message(role, [instr]) for instr in instructions] return [*instruction_messages, *messages] @@ -1703,7 +1704,7 @@ def _process_update(response: ChatResponse | AgentResponse, update: ChatResponse is_new_message = True if is_new_message: - message = ChatMessage("assistant", []) + message = Message("assistant", []) response.messages.append(message) else: message = response.messages[-1] @@ -1760,6 +1761,7 @@ def _process_update(response: ChatResponse | AgentResponse, update: ChatResponse response.finish_reason = update.finish_reason if update.model_id is not None: response.model_id = update.model_id + response.continuation_token = update.continuation_token def _coalesce_text_content(contents: list[Content], type_str: Literal["text", "text_reasoning"]) -> None: @@ -1796,6 +1798,39 @@ def _finalize_response(response: ChatResponse | AgentResponse) -> None: _coalesce_text_content(msg.contents, "text_reasoning") +# region ContinuationToken + + +class ContinuationToken(TypedDict): + """Opaque token for resuming long-running agent operations. + + A JSON-serializable dict used to poll for completion or resume a + streaming response. Presence on a response indicates the operation + is still in progress; ``None`` means the operation is complete. + + Each provider subclasses this with its own fields; consumers should + treat the token as opaque and simply pass it back to the same agent. + + Examples: + .. code-block:: python + + import json + + # Persist token across restarts + token_json = json.dumps(response.continuation_token) + + # Restore and resume + token = json.loads(token_json) + response = await agent.run( + thread=thread, + options={"continuation_token": token}, + ) + """ + + +# endregion + + class ChatResponse(SerializationMixin, Generic[ResponseModelT]): """Represents the response to a chat request. @@ -1812,17 +1847,17 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]): raw_representation: The raw representation of the chat response from an underlying implementation. Note: - The `author_name` attribute is available on the `ChatMessage` objects inside `messages`, + The `author_name` attribute is available on the `Message` objects inside `messages`, not on the `ChatResponse` itself. Use `response.messages[0].author_name` to access the author name of individual messages. Examples: .. code-block:: python - from agent_framework import ChatResponse, ChatMessage + from agent_framework import ChatResponse, Message # Create a response with messages - msg = ChatMessage("assistant", ["The weather is sunny."]) + msg = Message("assistant", ["The weather is sunny."]) response = ChatResponse( messages=[msg], finish_reason="stop", @@ -1852,7 +1887,7 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]): def __init__( self, *, - messages: ChatMessage | Sequence[ChatMessage] | None = None, + messages: Message | Sequence[Message] | None = None, response_id: str | None = None, conversation_id: str | None = None, model_id: str | None = None, @@ -1861,13 +1896,14 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]): usage_details: UsageDetails | None = None, value: ResponseModelT | None = None, response_format: type[BaseModel] | None = None, + continuation_token: ContinuationToken | None = None, additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, ) -> None: """Initializes a ChatResponse with the provided parameters. Keyword Args: - messages: A single ChatMessage or sequence of ChatMessage objects to include in the response. + messages: A single Message or sequence of Message objects to include in the response. response_id: Optional ID of the chat response. conversation_id: Optional identifier for the state of the conversation. model_id: Optional model ID used in the creation of the chat response. @@ -1876,21 +1912,23 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]): usage_details: Optional usage details for the chat response. value: Optional value of the structured output. response_format: Optional response format for the chat response. + continuation_token: Optional token for resuming a long-running background operation. + When present, indicates the operation is still in progress. additional_properties: Optional additional properties associated with the chat response. raw_representation: Optional raw representation of the chat response from an underlying implementation. """ if messages is None: - self.messages: list[ChatMessage] = [] - elif isinstance(messages, ChatMessage): + self.messages: list[Message] = [] + elif isinstance(messages, Message): self.messages = [messages] else: - # Handle both ChatMessage objects and dicts (for from_dict support) - processed_messages: list[ChatMessage] = [] + # Handle both Message objects and dicts (for from_dict support) + processed_messages: list[Message] = [] for msg in messages: - if isinstance(msg, ChatMessage): + if isinstance(msg, Message): processed_messages.append(msg) elif isinstance(msg, dict): - processed_messages.append(ChatMessage.from_dict(msg)) + processed_messages.append(Message.from_dict(msg)) else: processed_messages.append(msg) self.messages = processed_messages @@ -1907,6 +1945,7 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]): self._response_format: type[BaseModel] | None = response_format self._value_parsed: bool = value is not None self.additional_properties = additional_properties or {} + self.continuation_token = continuation_token self.raw_representation: Any | list[Any] | None = raw_representation @overload @@ -2018,7 +2057,7 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]): @property def text(self) -> str: """Returns the concatenated text of all messages in the response.""" - return ("\n".join(message.text for message in self.messages if isinstance(message, ChatMessage))).strip() + return ("\n".join(message.text for message in self.messages if isinstance(message, Message))).strip() @property def value(self) -> ResponseModelT | None: @@ -2057,7 +2096,7 @@ class ChatResponseUpdate(SerializationMixin): author_name: The name of the author of the response update. This is primarily used in multi-agent scenarios to identify which agent or participant generated the response. When updates are combined into a `ChatResponse`, the `author_name` is propagated - to the resulting `ChatMessage` objects. + to the resulting `Message` objects. response_id: The ID of the response of which this update is a part. message_id: The ID of the message of which this update is a part. conversation_id: An identifier for the state of the conversation of which this update is a part. @@ -2109,6 +2148,7 @@ class ChatResponseUpdate(SerializationMixin): model_id: str | None = None, created_at: CreatedAtT | None = None, finish_reason: FinishReasonLiteral | FinishReason | None = None, + continuation_token: ContinuationToken | None = None, additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, ) -> None: @@ -2124,6 +2164,8 @@ class ChatResponseUpdate(SerializationMixin): model_id: Optional model ID associated with this response update. created_at: Optional timestamp for the chat response update. finish_reason: Optional finish reason for the operation. + continuation_token: Optional token for resuming a long-running background operation. + When present, indicates the operation is still in progress. additional_properties: Optional additional properties associated with the chat response update. raw_representation: Optional raw representation of the chat response update from an underlying implementation. @@ -2151,6 +2193,7 @@ class ChatResponseUpdate(SerializationMixin): self.model_id = model_id self.created_at = created_at self.finish_reason = finish_reason + self.continuation_token = continuation_token self.additional_properties = additional_properties self.raw_representation = raw_representation @@ -2174,17 +2217,17 @@ class AgentResponse(SerializationMixin, Generic[ResponseModelT]): messages in scenarios involving function calls, RAG retrievals, or complex logic. Note: - The `author_name` attribute is available on the `ChatMessage` objects inside `messages`, + The `author_name` attribute is available on the `Message` objects inside `messages`, not on the `AgentResponse` itself. Use `response.messages[0].author_name` to access the author name of individual messages. Examples: .. code-block:: python - from agent_framework import AgentResponse, ChatMessage + from agent_framework import AgentResponse, Message # Create agent response - msg = ChatMessage("assistant", ["Task completed successfully."]) + msg = Message("assistant", ["Task completed successfully."]) response = AgentResponse(messages=[msg], response_id="run_123") print(response.text) # "Task completed successfully." @@ -2215,20 +2258,21 @@ class AgentResponse(SerializationMixin, Generic[ResponseModelT]): def __init__( self, *, - messages: ChatMessage | Sequence[ChatMessage] | None = None, + messages: Message | Sequence[Message] | None = None, response_id: str | None = None, agent_id: str | None = None, created_at: CreatedAtT | None = None, usage_details: UsageDetails | None = None, value: ResponseModelT | None = None, response_format: type[BaseModel] | None = None, + continuation_token: ContinuationToken | None = None, raw_representation: Any | None = None, additional_properties: dict[str, Any] | None = None, ) -> None: """Initialize an AgentResponse. Keyword Args: - messages: A single ChatMessage or sequence of ChatMessage objects to include in the response. + messages: A single Message or sequence of Message objects to include in the response. response_id: The ID of the chat response. agent_id: The identifier of the agent that produced this response. Useful in multi-agent scenarios to track which agent generated the response. @@ -2236,21 +2280,23 @@ class AgentResponse(SerializationMixin, Generic[ResponseModelT]): usage_details: The usage details for the chat response. value: The structured output of the agent run response, if applicable. response_format: Optional response format for the agent response. + continuation_token: Optional token for resuming a long-running background operation. + When present, indicates the operation is still in progress. additional_properties: Any additional properties associated with the chat response. raw_representation: The raw representation of the chat response from an underlying implementation. """ if messages is None: - self.messages: list[ChatMessage] = [] - elif isinstance(messages, ChatMessage): + self.messages: list[Message] = [] + elif isinstance(messages, Message): self.messages = [messages] else: - # Handle both ChatMessage objects and dicts (for from_dict support) - processed_messages: list[ChatMessage] = [] + # Handle both Message objects and dicts (for from_dict support) + processed_messages: list[Message] = [] for msg in messages: - if isinstance(msg, ChatMessage): + if isinstance(msg, Message): processed_messages.append(msg) elif isinstance(msg, dict): - processed_messages.append(ChatMessage.from_dict(msg)) + processed_messages.append(Message.from_dict(msg)) else: processed_messages.append(msg) self.messages = processed_messages @@ -2262,6 +2308,7 @@ class AgentResponse(SerializationMixin, Generic[ResponseModelT]): self._response_format: type[BaseModel] | None = response_format self._value_parsed: bool = value is not None self.additional_properties = additional_properties or {} + self.continuation_token = continuation_token self.raw_representation = raw_representation @property @@ -2393,7 +2440,7 @@ class AgentResponseUpdate(SerializationMixin): role: The role of the author of the response update. author_name: The name of the author of the response update. In multi-agent scenarios, this identifies which agent generated this update. When updates are combined into - an `AgentResponse`, the `author_name` is propagated to the resulting `ChatMessage` objects. + an `AgentResponse`, the `author_name` is propagated to the resulting `Message` objects. agent_id: The identifier of the agent that produced this update. Useful in multi-agent scenarios to track which agent generated specific parts of the response. response_id: The ID of the response of which this update is a part. @@ -2444,6 +2491,7 @@ class AgentResponseUpdate(SerializationMixin): response_id: str | None = None, message_id: str | None = None, created_at: CreatedAtT | None = None, + continuation_token: ContinuationToken | None = None, additional_properties: dict[str, Any] | None = None, raw_representation: Any | None = None, ) -> None: @@ -2458,6 +2506,8 @@ class AgentResponseUpdate(SerializationMixin): response_id: Optional ID of the response of which this update is a part. message_id: Optional ID of the message of which this update is a part. created_at: Optional timestamp for the chat response update. + continuation_token: Optional token for resuming a long-running background operation. + When present, indicates the operation is still in progress. additional_properties: Optional additional properties associated with the chat response update. raw_representation: Optional raw representation of the chat response update. @@ -2486,6 +2536,7 @@ class AgentResponseUpdate(SerializationMixin): self.response_id = response_id self.message_id = message_id self.created_at = created_at + self.continuation_token = continuation_token self.additional_properties = additional_properties self.raw_representation: Any | list[Any] | None = raw_representation @@ -2514,6 +2565,7 @@ def map_chat_to_agent_update(update: ChatResponseUpdate, agent_name: str | None) response_id=update.response_id, message_id=update.message_id, created_at=update.created_at, + continuation_token=update.continuation_token, additional_properties=update.additional_properties, raw_representation=update, ) @@ -2920,10 +2972,10 @@ class _ChatOptionsBase(TypedDict, total=False): # Tool configuration (forward reference to avoid circular import) tools: ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None ) tool_choice: ToolMode | Literal["auto", "required", "none"] @@ -3013,17 +3065,17 @@ async def validate_chat_options(options: dict[str, Any]) -> dict[str, Any]: def normalize_tools( tools: ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None ), -) -> list[ToolProtocol | MutableMapping[str, Any]]: +) -> list[FunctionTool | MutableMapping[str, Any]]: """Normalize tools into a list. Converts callables to FunctionTool objects and ensures all tools are either - ToolProtocol instances or MutableMappings. + FunctionTool instances or MutableMappings. Args: tools: Tools to normalize - can be a single tool, callable, or sequence. @@ -3048,16 +3100,16 @@ def normalize_tools( # List of tools tools = normalize_tools([my_tool, another_tool]) """ - final_tools: list[ToolProtocol | MutableMapping[str, Any]] = [] + final_tools: list[FunctionTool | MutableMapping[str, Any]] = [] if not tools: return final_tools if not isinstance(tools, Sequence) or isinstance(tools, (str, MutableMapping)): # Single tool (not a sequence, or is a mapping which shouldn't be treated as sequence) - if not isinstance(tools, (ToolProtocol, MutableMapping)): + if not isinstance(tools, (FunctionTool, MutableMapping)): return [tool(tools)] return [tools] for tool_item in tools: - if isinstance(tool_item, (ToolProtocol, MutableMapping)): + if isinstance(tool_item, (FunctionTool, MutableMapping)): final_tools.append(tool_item) else: # Convert callable to FunctionTool @@ -3067,17 +3119,17 @@ def normalize_tools( async def validate_tools( tools: ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None ), -) -> list[ToolProtocol | MutableMapping[str, Any]]: +) -> list[FunctionTool | MutableMapping[str, Any]]: """Validate and normalize tools into a list. Converts callables to FunctionTool objects, expands MCP tools to their constituent - functions (connecting them if needed), and ensures all tools are either ToolProtocol + functions (connecting them if needed), and ensures all tools are either FunctionTool instances or MutableMappings. Args: @@ -3107,7 +3159,7 @@ async def validate_tools( normalized = normalize_tools(tools) # Handle MCP tool expansion (async-only) - final_tools: list[ToolProtocol | MutableMapping[str, Any]] = [] + final_tools: list[FunctionTool | MutableMapping[str, Any]] = [] for tool_ in normalized: # Import MCPTool here to avoid circular imports from ._mcp import MCPTool diff --git a/python/packages/core/agent_framework/_workflows/__init__.py b/python/packages/core/agent_framework/_workflows/__init__.py index c5666f7b26..e573c51e23 100644 --- a/python/packages/core/agent_framework/_workflows/__init__.py +++ b/python/packages/core/agent_framework/_workflows/__init__.py @@ -52,8 +52,8 @@ from ._request_info_mixin import response_handler from ._runner import Runner from ._runner_context import ( InProcRunnerContext, - Message, RunnerContext, + WorkflowMessage, ) from ._validation import ( EdgeDuplicationError, @@ -92,7 +92,6 @@ __all__ = [ "GraphConnectivityError", "InMemoryCheckpointStorage", "InProcRunnerContext", - "Message", "Runner", "RunnerContext", "SingleEdgeGroup", @@ -117,6 +116,7 @@ __all__ = [ "WorkflowEventType", "WorkflowException", "WorkflowExecutor", + "WorkflowMessage", "WorkflowRunResult", "WorkflowRunState", "WorkflowRunnerException", diff --git a/python/packages/core/agent_framework/_workflows/_agent.py b/python/packages/core/agent_framework/_workflows/_agent.py index 46161e61e4..962da2bcd4 100644 --- a/python/packages/core/agent_framework/_workflows/_agent.py +++ b/python/packages/core/agent_framework/_workflows/_agent.py @@ -16,8 +16,8 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, + Message, UsageDetails, ) @@ -107,8 +107,8 @@ class WorkflowAgent(BaseAgent): except KeyError as exc: # Defensive: workflow lacks a configured entry point raise ValueError("Workflow's start executor is not defined.") from exc - if not any(is_type_compatible(list[ChatMessage], input_type) for input_type in start_executor.input_types): - raise ValueError("Workflow's start executor cannot handle list[ChatMessage]") + if not any(is_type_compatible(list[Message], input_type) for input_type in start_executor.input_types): + raise ValueError("Workflow's start executor cannot handle list[Message]") super().__init__(id=id, name=name, description=description, **kwargs) self._workflow: Workflow = workflow @@ -127,7 +127,7 @@ class WorkflowAgent(BaseAgent): @overload def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: Literal[True], thread: AgentThread | None = None, @@ -139,7 +139,7 @@ class WorkflowAgent(BaseAgent): @overload async def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: Literal[False] = ..., thread: AgentThread | None = None, @@ -150,7 +150,7 @@ class WorkflowAgent(BaseAgent): def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -202,7 +202,7 @@ class WorkflowAgent(BaseAgent): async def _run_non_streaming( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, checkpoint_id: str | None = None, @@ -225,7 +225,7 @@ class WorkflowAgent(BaseAgent): async def _run_streaming( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, checkpoint_id: str | None = None, @@ -257,7 +257,7 @@ class WorkflowAgent(BaseAgent): async def _run_impl( self, - input_messages: list[ChatMessage], + input_messages: list[Message], response_id: str, thread: AgentThread, checkpoint_id: str | None = None, @@ -289,7 +289,7 @@ class WorkflowAgent(BaseAgent): async def _run_stream_impl( self, - input_messages: list[ChatMessage], + input_messages: list[Message], response_id: str, thread: AgentThread, checkpoint_id: str | None = None, @@ -319,7 +319,7 @@ class WorkflowAgent(BaseAgent): async def _run_core( self, - input_messages: list[ChatMessage], + input_messages: list[Message], thread: AgentThread, checkpoint_id: str | None, checkpoint_storage: CheckpointStorage | None, @@ -393,8 +393,8 @@ class WorkflowAgent(BaseAgent): async def _build_conversation_messages( self, thread: AgentThread, - input_messages: list[ChatMessage], - ) -> list[ChatMessage]: + input_messages: list[Message], + ) -> list[Message]: """Build the complete conversation by prepending thread history to input messages. Args: @@ -402,9 +402,9 @@ class WorkflowAgent(BaseAgent): input_messages: The new input messages to append. Returns: - A list of ChatMessage objects representing the full conversation. + A list of Message objects representing the full conversation. """ - conversation_messages: list[ChatMessage] = [] + conversation_messages: list[Message] = [] if thread.message_store: history = await thread.message_store.list_messages() if history: @@ -412,7 +412,7 @@ class WorkflowAgent(BaseAgent): conversation_messages.extend(input_messages) return conversation_messages - def _process_pending_requests(self, input_messages: list[ChatMessage]) -> dict[str, Any]: + def _process_pending_requests(self, input_messages: list[Message]) -> dict[str, Any]: """Process pending requests by extracting function responses and updating state. Args: @@ -444,7 +444,7 @@ class WorkflowAgent(BaseAgent): output_events: list[WorkflowEvent[Any]], ) -> AgentResponse: """Convert a list of workflow output events to an AgentResponse.""" - messages: list[ChatMessage] = [] + messages: list[Message] = [] raw_representations: list[object] = [] merged_usage: UsageDetails | None = None latest_created_at: str | None = None @@ -453,7 +453,7 @@ class WorkflowAgent(BaseAgent): if output_event.type == "request_info": function_call, approval_request = self._process_request_info_event(output_event) messages.append( - ChatMessage( + Message( contents=[function_call, approval_request], role="assistant", author_name=output_event.source_executor_id, @@ -484,11 +484,11 @@ class WorkflowAgent(BaseAgent): if data.created_at else latest_created_at ) - elif isinstance(data, ChatMessage): + elif isinstance(data, Message): messages.append(data) raw_representations.append(data.raw_representation) - elif is_instance_of(data, list[ChatMessage]): - chat_messages = cast(list[ChatMessage], data) + elif is_instance_of(data, list[Message]): + chat_messages = cast(list[Message], data) messages.extend(chat_messages) raw_representations.append(data) else: @@ -497,7 +497,7 @@ class WorkflowAgent(BaseAgent): continue messages.append( - ChatMessage( + Message( contents=contents, role="assistant", author_name=output_event.executor_id, @@ -591,7 +591,7 @@ class WorkflowAgent(BaseAgent): ) ) return updates - if isinstance(data, ChatMessage): + if isinstance(data, Message): return [ AgentResponseUpdate( contents=list(data.contents), @@ -603,9 +603,9 @@ class WorkflowAgent(BaseAgent): raw_representation=data, ) ] - if is_instance_of(data, list[ChatMessage]): - # Convert each ChatMessage to an AgentResponseUpdate - chat_messages = cast(list[ChatMessage], data) + if is_instance_of(data, list[Message]): + # Convert each Message to an AgentResponseUpdate + chat_messages = cast(list[Message], data) updates = [] for msg in chat_messages: updates.append( @@ -669,7 +669,7 @@ class WorkflowAgent(BaseAgent): # Ignore workflow-internal events return [] - def _extract_function_responses(self, input_messages: list[ChatMessage]) -> dict[str, Any]: + def _extract_function_responses(self, input_messages: list[Message]) -> dict[str, Any]: """Extract function responses from input messages.""" function_responses: dict[str, Any] = {} for message in input_messages: @@ -820,7 +820,7 @@ class WorkflowAgent(BaseAgent): ) # PHASE 2: CONVERT GROUPED UPDATES TO RESPONSES AND MERGE - final_messages: list[ChatMessage] = [] + final_messages: list[Message] = [] merged_usage: UsageDetails | None = None latest_created_at: str | None = None merged_additional_properties: dict[str, Any] | None = None diff --git a/python/packages/core/agent_framework/_workflows/_agent_executor.py b/python/packages/core/agent_framework/_workflows/_agent_executor.py index 4158380086..0923e5c93c 100644 --- a/python/packages/core/agent_framework/_workflows/_agent_executor.py +++ b/python/packages/core/agent_framework/_workflows/_agent_executor.py @@ -11,7 +11,7 @@ from agent_framework import Content from .._agents import SupportsAgentRun from .._threads import AgentThread -from .._types import AgentResponse, AgentResponseUpdate, ChatMessage +from .._types import AgentResponse, AgentResponseUpdate, Message from ._agent_utils import resolve_agent_id from ._checkpoint_encoding import decode_checkpoint_value, encode_checkpoint_value from ._const import WORKFLOW_RUN_KWARGS_KEY @@ -40,7 +40,7 @@ class AgentExecutorRequest: If False, the messages will be saved to the executor's cache but not sent to the agent. """ - messages: list[ChatMessage] + messages: list[Message] should_respond: bool = True @@ -58,7 +58,7 @@ class AgentExecutorResponse: executor_id: str agent_response: AgentResponse - full_conversation: list[ChatMessage] | None = None + full_conversation: list[Message] | None = None class AgentExecutor(Executor): @@ -104,9 +104,9 @@ class AgentExecutor(Executor): self._pending_responses_to_agent: list[Content] = [] # AgentExecutor maintains an internal cache of messages in between runs - self._cache: list[ChatMessage] = [] + self._cache: list[Message] = [] # This tracks the full conversation after each run - self._full_conversation: list[ChatMessage] = [] + self._full_conversation: list[Message] = [] @property def description(self) -> str | None: @@ -157,20 +157,20 @@ class AgentExecutor(Executor): @handler async def from_message( self, - message: ChatMessage, + message: Message, ctx: WorkflowContext[AgentExecutorResponse, AgentResponse | AgentResponseUpdate], ) -> None: - """Accept a single ChatMessage as input.""" + """Accept a single Message as input.""" self._cache = normalize_messages_input(message) await self._run_agent_and_emit(ctx) @handler async def from_messages( self, - messages: list[str | ChatMessage], + messages: list[str | Message], ctx: WorkflowContext[AgentExecutorResponse, AgentResponse | AgentResponseUpdate], ) -> None: - """Accept a list of chat inputs (strings or ChatMessage) as conversation context.""" + """Accept a list of chat inputs (strings or Message) as conversation context.""" self._cache = normalize_messages_input(messages) await self._run_agent_and_emit(ctx) @@ -198,7 +198,7 @@ class AgentExecutor(Executor): # Use role="tool" for function_result responses (from declaration-only tools) # so the LLM receives proper tool results instead of orphaned tool_calls. role = "tool" if all(r.type == "function_result" for r in self._pending_responses_to_agent) else "user" - self._cache = normalize_messages_input(ChatMessage(role=role, contents=self._pending_responses_to_agent)) + self._cache = normalize_messages_input(Message(role=role, contents=self._pending_responses_to_agent)) self._pending_responses_to_agent.clear() await self._run_agent_and_emit(ctx) @@ -216,8 +216,8 @@ class AgentExecutor(Executor): """ # Check if using AzureAIAgentClient with server-side thread and warn about checkpointing limitations if is_chat_agent(self._agent) and self._agent_thread.service_thread_id is not None: - client_class_name = self._agent.chat_client.__class__.__name__ - client_module = self._agent.chat_client.__class__.__module__ + client_class_name = self._agent.client.__class__.__name__ + client_module = self._agent.client.__class__.__module__ if client_class_name == "AzureAIAgentClient" and "azure_ai" in client_module: logger.warning( diff --git a/python/packages/core/agent_framework/_workflows/_conversation_history.py b/python/packages/core/agent_framework/_workflows/_conversation_history.py index 52d7d99c74..3df5282ee4 100644 --- a/python/packages/core/agent_framework/_workflows/_conversation_history.py +++ b/python/packages/core/agent_framework/_workflows/_conversation_history.py @@ -2,16 +2,16 @@ """Helpers for managing chat conversation history. -These utilities operate on standard `list[ChatMessage]` collections and simple +These utilities operate on standard `list[Message]` collections and simple dictionary snapshots so orchestrators can share logic without new mixins. """ from collections.abc import Sequence -from .._types import ChatMessage +from .._types import Message -def latest_user_message(conversation: Sequence[ChatMessage]) -> ChatMessage: +def latest_user_message(conversation: Sequence[Message]) -> Message: """Return the most recent user-authored message from `conversation`.""" for message in reversed(conversation): role_value = getattr(message.role, "value", message.role) @@ -20,7 +20,7 @@ def latest_user_message(conversation: Sequence[ChatMessage]) -> ChatMessage: raise ValueError("No user message in conversation") -def ensure_author(message: ChatMessage, fallback: str) -> ChatMessage: +def ensure_author(message: Message, fallback: str) -> Message: """Attach `fallback` author if message is missing `author_name`.""" message.author_name = message.author_name or fallback return message diff --git a/python/packages/core/agent_framework/_workflows/_conversation_state.py b/python/packages/core/agent_framework/_workflows/_conversation_state.py index 22433e6775..95945998df 100644 --- a/python/packages/core/agent_framework/_workflows/_conversation_state.py +++ b/python/packages/core/agent_framework/_workflows/_conversation_state.py @@ -3,20 +3,20 @@ from collections.abc import Iterable from typing import Any, cast -from agent_framework import ChatMessage +from agent_framework import Message from ._checkpoint_encoding import decode_checkpoint_value, encode_checkpoint_value """Utilities for serializing and deserializing chat conversations for persistence. -These helpers convert rich `ChatMessage` instances to checkpoint-friendly payloads +These helpers convert rich `Message` instances to checkpoint-friendly payloads using the same encoding primitives as the workflow runner. This preserves `additional_properties` and other metadata without relying on unsafe mechanisms such as pickling. """ -def encode_chat_messages(messages: Iterable[ChatMessage]) -> list[dict[str, Any]]: +def encode_chat_messages(messages: Iterable[Message]) -> list[dict[str, Any]]: """Serialize chat messages into checkpoint-safe payloads.""" encoded: list[dict[str, Any]] = [] for message in messages: @@ -32,9 +32,9 @@ def encode_chat_messages(messages: Iterable[ChatMessage]) -> list[dict[str, Any] return encoded -def decode_chat_messages(payload: Iterable[dict[str, Any]]) -> list[ChatMessage]: +def decode_chat_messages(payload: Iterable[dict[str, Any]]) -> list[Message]: """Restore chat messages from checkpoint-safe payloads.""" - restored: list[ChatMessage] = [] + restored: list[Message] = [] for item in payload: if not isinstance(item, dict): continue @@ -64,7 +64,7 @@ def decode_chat_messages(payload: Iterable[dict[str, Any]]) -> list[ChatMessage] additional[key] = decode_checkpoint_value(value) restored.append( - ChatMessage( # type: ignore[call-overload] + Message( # type: ignore[call-overload] role=role, contents=contents, author_name=item.get("author_name"), diff --git a/python/packages/core/agent_framework/_workflows/_edge_runner.py b/python/packages/core/agent_framework/_workflows/_edge_runner.py index c87994b4b4..06188e9fb2 100644 --- a/python/packages/core/agent_framework/_workflows/_edge_runner.py +++ b/python/packages/core/agent_framework/_workflows/_edge_runner.py @@ -18,7 +18,7 @@ from ._edge import ( SwitchCaseEdgeGroup, ) from ._executor import Executor -from ._runner_context import Message, RunnerContext +from ._runner_context import RunnerContext, WorkflowMessage from ._state import State logger = logging.getLogger(__name__) @@ -38,7 +38,7 @@ class EdgeRunner(ABC): self._executors = executors @abstractmethod - async def send_message(self, message: Message, state: State, ctx: RunnerContext) -> bool: + async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: """Send a message through the edge group. Args: @@ -52,7 +52,7 @@ class EdgeRunner(ABC): """ raise NotImplementedError - def _can_handle(self, executor_id: str, message: Message) -> bool: + def _can_handle(self, executor_id: str, message: WorkflowMessage) -> bool: """Check if an executor can handle the given message data.""" if executor_id not in self._executors: return False @@ -62,7 +62,7 @@ class EdgeRunner(ABC): self, target_id: str, source_ids: list[str], - message: Message, + message: WorkflowMessage, state: State, ctx: RunnerContext, ) -> None: @@ -90,7 +90,7 @@ class SingleEdgeRunner(EdgeRunner): super().__init__(edge_group, executors) self._edge = edge_group.edges[0] - async def send_message(self, message: Message, state: State, ctx: RunnerContext) -> bool: + async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: """Send a message through the single edge.""" should_execute = False target_id: str | None = None @@ -162,7 +162,7 @@ class FanOutEdgeRunner(EdgeRunner): Callable[[Any, list[str]], list[str]] | None, getattr(edge_group, "selection_func", None) ) - async def send_message(self, message: Message, state: State, ctx: RunnerContext) -> bool: + async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: """Send a message through all edges in the fan-out edge group.""" deliverable_edges: list[Edge] = [] single_target_edge: Edge | None = None @@ -283,9 +283,9 @@ class FanInEdgeRunner(EdgeRunner): self._edges = edge_group.edges # Buffer to hold messages before sending them to the target executor # Key is the source executor ID, value is a list of messages - self._buffer: dict[str, list[Message]] = defaultdict(list) + self._buffer: dict[str, list[WorkflowMessage]] = defaultdict(list) - async def send_message(self, message: Message, state: State, ctx: RunnerContext) -> bool: + async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: """Send a message through all edges in the fan-in edge group.""" execution_data: dict[str, Any] | None = None with create_edge_group_processing_span( @@ -306,7 +306,7 @@ class FanInEdgeRunner(EdgeRunner): # Check if target can handle list of message data (fan-in aggregates multiple messages) if self._can_handle( - self._edges[0].target_id, Message(data=[message.data], source_id=message.source_id) + self._edges[0].target_id, WorkflowMessage(data=[message.data], source_id=message.source_id) ): # If the edge can handle the data, buffer the message self._buffer[message.source_id].append(message) @@ -334,7 +334,7 @@ class FanInEdgeRunner(EdgeRunner): source_span_ids = [msg.source_span_id for msg in messages_to_send if msg.source_span_id] # Create a new Message object for the aggregated data - aggregated_message = Message( + aggregated_message = WorkflowMessage( data=aggregated_data, source_id=self._edge_group.__class__.__name__, # This won't be used in self._execute_on_target. trace_contexts=trace_contexts, diff --git a/python/packages/core/agent_framework/_workflows/_executor.py b/python/packages/core/agent_framework/_workflows/_executor.py index ffab65e3a3..f219c0c28f 100644 --- a/python/packages/core/agent_framework/_workflows/_executor.py +++ b/python/packages/core/agent_framework/_workflows/_executor.py @@ -17,7 +17,7 @@ from ._events import ( ) from ._model_utils import DictConvertible from ._request_info_mixin import RequestInfoMixin -from ._runner_context import Message, MessageType, RunnerContext +from ._runner_context import MessageType, RunnerContext, WorkflowMessage from ._state import State from ._typing_utils import is_instance_of, normalize_type_to_list, resolve_type_annotation from ._workflow_context import WorkflowContext, validate_workflow_context_annotation @@ -244,7 +244,7 @@ class Executor(RequestInfoMixin, DictConvertible): with create_processing_span( self.id, self.__class__.__name__, - str(MessageType.STANDARD if not isinstance(message, Message) else message.type), + str(MessageType.STANDARD if not isinstance(message, WorkflowMessage) else message.type), type(message).__name__, source_trace_contexts=trace_contexts, source_span_ids=source_span_ids, @@ -253,7 +253,7 @@ class Executor(RequestInfoMixin, DictConvertible): handler = self._find_handler(message) original_message = message - if isinstance(message, Message): + if isinstance(message, WorkflowMessage): # Unwrap raw data for handler call message = message.data @@ -265,7 +265,7 @@ class Executor(RequestInfoMixin, DictConvertible): trace_contexts=trace_contexts, source_span_ids=source_span_ids, request_id=original_message.original_request_info_event.request_id - if isinstance(original_message, Message) and original_message.original_request_info_event + if isinstance(original_message, WorkflowMessage) and original_message.original_request_info_event else None, ) @@ -351,7 +351,7 @@ class Executor(RequestInfoMixin, DictConvertible): # Add to unified handler specs list self._handler_specs.append({**handler_spec}) - def can_handle(self, message: Message) -> bool: + def can_handle(self, message: WorkflowMessage) -> bool: """Check if the executor can handle a given message type. Args: @@ -460,7 +460,7 @@ class Executor(RequestInfoMixin, DictConvertible): Returns: The handler function if found, None otherwise """ - if isinstance(message, Message): + if isinstance(message, WorkflowMessage): # Case where Message wrapper is passed instead of raw data # Handler can be a standard handler or a response handler if message.type == MessageType.STANDARD: diff --git a/python/packages/core/agent_framework/_workflows/_message_utils.py b/python/packages/core/agent_framework/_workflows/_message_utils.py index 920672cead..6d27a905ee 100644 --- a/python/packages/core/agent_framework/_workflows/_message_utils.py +++ b/python/packages/core/agent_framework/_workflows/_message_utils.py @@ -4,38 +4,38 @@ from collections.abc import Sequence -from agent_framework import ChatMessage +from agent_framework import Message def normalize_messages_input( - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, -) -> list[ChatMessage]: - """Normalize heterogeneous message inputs to a list of ChatMessage objects. + messages: str | Message | Sequence[str | Message] | None = None, +) -> list[Message]: + """Normalize heterogeneous message inputs to a list of Message objects. Args: - messages: String, ChatMessage, or sequence of either. None yields empty list. + messages: String, Message, or sequence of either. None yields empty list. Returns: - List of ChatMessage instances suitable for workflow consumption. + List of Message instances suitable for workflow consumption. """ if messages is None: return [] if isinstance(messages, str): - return [ChatMessage(role="user", text=messages)] + return [Message(role="user", text=messages)] - if isinstance(messages, ChatMessage): + if isinstance(messages, Message): return [messages] - normalized: list[ChatMessage] = [] + normalized: list[Message] = [] for item in messages: if isinstance(item, str): - normalized.append(ChatMessage(role="user", text=item)) - elif isinstance(item, ChatMessage): + normalized.append(Message(role="user", text=item)) + elif isinstance(item, Message): normalized.append(item) else: raise TypeError( - f"Messages sequence must contain only str or ChatMessage instances; found {type(item).__name__}." + f"Messages sequence must contain only str or Message instances; found {type(item).__name__}." ) return normalized diff --git a/python/packages/core/agent_framework/_workflows/_runner.py b/python/packages/core/agent_framework/_workflows/_runner.py index f3a475e034..83ce5d8085 100644 --- a/python/packages/core/agent_framework/_workflows/_runner.py +++ b/python/packages/core/agent_framework/_workflows/_runner.py @@ -24,8 +24,8 @@ from ._exceptions import ( ) from ._executor import Executor from ._runner_context import ( - Message, RunnerContext, + WorkflowMessage, ) from ._state import State @@ -162,14 +162,14 @@ class Runner: self._running = False async def _run_iteration(self) -> None: - async def _deliver_messages(source_executor_id: str, messages: list[Message]) -> None: + async def _deliver_messages(source_executor_id: str, messages: list[WorkflowMessage]) -> None: """Outer loop to concurrently deliver messages from all sources to their targets.""" - async def _deliver_message_inner(edge_runner: EdgeRunner, message: Message) -> bool: + async def _deliver_message_inner(edge_runner: EdgeRunner, message: WorkflowMessage) -> bool: """Inner loop to deliver a single message through an edge runner.""" return await edge_runner.send_message(message, self._state, self._ctx) - def _normalize_message_payload(message: Message) -> None: + def _normalize_message_payload(message: WorkflowMessage) -> None: data = message.data if not isinstance(data, dict): return diff --git a/python/packages/core/agent_framework/_workflows/_runner_context.py b/python/packages/core/agent_framework/_workflows/_runner_context.py index ed81026245..db6558306a 100644 --- a/python/packages/core/agent_framework/_workflows/_runner_context.py +++ b/python/packages/core/agent_framework/_workflows/_runner_context.py @@ -29,25 +29,25 @@ T = TypeVar("T") class MessageType(Enum): - """Enumeration of message types in the workflow.""" + """Enumeration of WorkflowMessage types in the workflow.""" STANDARD = "standard" - """A standard message between executors.""" + """A standard WorkflowMessage between executors.""" RESPONSE = "response" - """A response message to a pending request.""" + """A response WorkflowMessage to a pending request.""" @dataclass -class Message: - """A class representing a message in the workflow.""" +class WorkflowMessage: + """A class representing a WorkflowMessage in the workflow.""" data: Any source_id: str target_id: str | None = None type: MessageType = MessageType.STANDARD - # OpenTelemetry trace context fields for message propagation + # OpenTelemetry trace context fields for WorkflowMessage propagation # These are plural to support fan-in scenarios where multiple messages are aggregated trace_contexts: list[dict[str, str]] | None = None # W3C Trace Context headers from multiple sources source_span_ids: list[str] | None = None # Publishing span IDs for linking from multiple sources @@ -67,7 +67,7 @@ class Message: return self.source_span_ids[0] if self.source_span_ids else None def to_dict(self) -> dict[str, Any]: - """Convert the Message to a dictionary for serialization.""" + """Convert the WorkflowMessage to a dictionary for serialization.""" return { "data": encode_checkpoint_value(self.data), "source_id": self.source_id, @@ -79,16 +79,16 @@ class Message: } @staticmethod - def from_dict(data: dict[str, Any]) -> Message: - """Create a Message from a dictionary.""" + def from_dict(data: dict[str, Any]) -> WorkflowMessage: + """Create a WorkflowMessage from a dictionary.""" # Validation if "data" not in data: - raise KeyError("Missing 'data' field in Message dictionary.") + raise KeyError("Missing 'data' field in WorkflowMessage dictionary.") if "source_id" not in data: - raise KeyError("Missing 'source_id' field in Message dictionary.") + raise KeyError("Missing 'source_id' field in WorkflowMessage dictionary.") - return Message( + return WorkflowMessage( data=decode_checkpoint_value(data["data"]), source_id=data["source_id"], target_id=data.get("target_id"), @@ -119,15 +119,15 @@ class RunnerContext(Protocol): If checkpoint storage is not configured, checkpoint methods may raise. """ - async def send_message(self, message: Message) -> None: - """Send a message from the executor to the context. + async def send_message(self, WorkflowMessage: WorkflowMessage) -> None: + """Send a WorkflowMessage from the executor to the context. Args: - message: The message to be sent. + WorkflowMessage: The WorkflowMessage to be sent. """ ... - async def drain_messages(self) -> dict[str, list[Message]]: + async def drain_messages(self) -> dict[str, list[WorkflowMessage]]: """Drain all messages from the context. Returns: @@ -291,7 +291,7 @@ class InProcRunnerContext: Args: checkpoint_storage: Optional storage to enable checkpointing. """ - self._messages: dict[str, list[Message]] = {} + self._messages: dict[str, list[WorkflowMessage]] = {} # Event queue for immediate streaming of events self._event_queue: asyncio.Queue[WorkflowEvent] = asyncio.Queue() @@ -307,11 +307,11 @@ class InProcRunnerContext: self._streaming: bool = False # region Messaging and Events - async def send_message(self, message: Message) -> None: - self._messages.setdefault(message.source_id, []) - self._messages[message.source_id].append(message) + async def send_message(self, WorkflowMessage: WorkflowMessage) -> None: + self._messages.setdefault(WorkflowMessage.source_id, []) + self._messages[WorkflowMessage.source_id].append(WorkflowMessage) - async def drain_messages(self) -> dict[str, list[Message]]: + async def drain_messages(self) -> dict[str, list[WorkflowMessage]]: messages = copy(self._messages) self._messages.clear() return messages @@ -422,7 +422,7 @@ class InProcRunnerContext: self._messages.clear() messages_data = checkpoint.messages for source_id, message_list in messages_data.items(): - self._messages[source_id] = [Message.from_dict(msg) for msg in message_list] + self._messages[source_id] = [WorkflowMessage.from_dict(msg) for msg in message_list] # Restore pending request info events self._pending_request_info_events.clear() @@ -504,7 +504,7 @@ class InProcRunnerContext: source_executor_id = event.source_executor_id # Create ResponseMessage instance - response_msg = Message( + response_msg = WorkflowMessage( data=response, source_id=INTERNAL_SOURCE_ID(source_executor_id), target_id=source_executor_id, diff --git a/python/packages/core/agent_framework/_workflows/_typing_utils.py b/python/packages/core/agent_framework/_workflows/_typing_utils.py index 5bff0900b6..41ed071f0a 100644 --- a/python/packages/core/agent_framework/_workflows/_typing_utils.py +++ b/python/packages/core/agent_framework/_workflows/_typing_utils.py @@ -3,19 +3,19 @@ from types import UnionType from typing import Any, TypeGuard, Union, cast, get_args, get_origin -from .._agents import ChatAgent +from .._agents import Agent -def is_chat_agent(agent: Any) -> TypeGuard[ChatAgent]: - """Check if the given agent is a ChatAgent. +def is_chat_agent(agent: Any) -> TypeGuard[Agent]: + """Check if the given agent is a Agent. Args: agent (Any): The agent to check. Returns: - TypeGuard[ChatAgent]: True if the agent is a ChatAgent, False otherwise. + TypeGuard[Agent]: True if the agent is a Agent, False otherwise. """ - return isinstance(agent, ChatAgent) + return isinstance(agent, Agent) def resolve_type_annotation( @@ -255,7 +255,7 @@ def is_type_compatible(source_type: type | UnionType | Any, target_type: type | A type is compatible if values of source_type can be assigned to variables of target_type. For example: - - list[ChatMessage] is compatible with list[str | ChatMessage] + - list[Message] is compatible with list[str | Message] - str is compatible with str | int - int is compatible with Any diff --git a/python/packages/core/agent_framework/_workflows/_workflow.py b/python/packages/core/agent_framework/_workflows/_workflow.py index 08e7512234..88a92dc703 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow.py +++ b/python/packages/core/agent_framework/_workflows/_workflow.py @@ -841,14 +841,14 @@ class Workflow(DictConvertible): def as_agent(self, name: str | None = None) -> WorkflowAgent: """Create a WorkflowAgent that wraps this workflow. - The returned agent converts standard agent inputs (strings, ChatMessage, or lists of these) - into a list[ChatMessage] that is passed to the workflow's start executor. This conversion + The returned agent converts standard agent inputs (strings, Message, or lists of these) + into a list[Message] that is passed to the workflow's start executor. This conversion happens in WorkflowAgent._normalize_messages() which transforms: - - str -> [ChatMessage(USER, [str])] - - ChatMessage -> [ChatMessage] - - list[str | ChatMessage] -> list[ChatMessage] (with string elements converted) + - str -> [Message(USER, [str])] + - Message -> [Message] + - list[str | Message] -> list[Message] (with string elements converted) - The workflow's start executor must accept list[ChatMessage] as an input type, otherwise + The workflow's start executor must accept list[Message] as an input type, otherwise initialization will fail with a ValueError. Args: @@ -858,7 +858,7 @@ class Workflow(DictConvertible): A WorkflowAgent instance that wraps this workflow. Raises: - ValueError: If the workflow's start executor cannot handle list[ChatMessage] input. + ValueError: If the workflow's start executor cannot handle list[Message] input. """ # Import here to avoid circular imports from ._agent import WorkflowAgent diff --git a/python/packages/core/agent_framework/_workflows/_workflow_builder.py b/python/packages/core/agent_framework/_workflows/_workflow_builder.py index 14fd512e17..d7bdf9a918 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_builder.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_builder.py @@ -3,11 +3,9 @@ import logging import sys from collections.abc import Callable, Sequence -from dataclasses import dataclass from typing import Any from .._agents import SupportsAgentRun -from .._threads import AgentThread from ..observability import OtelAttr, capture_exception, create_workflow_span from ._agent_executor import AgentExecutor from ._agent_utils import resolve_agent_id @@ -40,76 +38,6 @@ else: logger = logging.getLogger(__name__) -@dataclass -class _EdgeRegistration: - """A data class representing an edge registration in the workflow builder. - - Args: - source: The registered source name. - target: The registered target name. - condition: An optional condition function `(data) -> bool | Awaitable[bool]`. - """ - - source: str - target: str - condition: EdgeCondition | None = None - - -@dataclass -class _FanOutEdgeRegistration: - """A data class representing a fan-out edge registration in the workflow builder. - - Args: - source: The registered source name. - targets: A list of registered target names. - """ - - source: str - targets: list[str] - - -@dataclass -class _FanInEdgeRegistration: - """A data class representing a fan-in edge registration in the workflow builder. - - Args: - sources: A list of registered source names. - target: The registered target name. - """ - - sources: list[str] - target: str - - -@dataclass -class _SwitchCaseEdgeGroupRegistration: - """A data class representing a switch-case edge group registration in the workflow builder. - - Args: - source: The registered source name. - cases: A list of case objects that determine the target executor for each message. - """ - - source: str - cases: list[Case | Default] - - -@dataclass -class _MultiSelectionEdgeGroupRegistration: - """A data class representing a multi-selection edge group registration in the workflow builder. - - Args: - source: The registered source name. - targets: A list of registered target names. - selection_func: A function that selects target executors for messages. - Takes (message, list[registered target names]) and returns list[registered target names]. - """ - - source: str - targets: list[str] - selection_func: Callable[[Any, list[str]], list[str]] - - class WorkflowBuilder: """A builder class for constructing workflows. @@ -136,14 +64,10 @@ class WorkflowBuilder: await ctx.yield_output(text[::-1]) - # Build a workflow - workflow = ( - WorkflowBuilder(start_executor="UpperCase") - .register_executor(lambda: UpperCaseExecutor(id="upper"), name="UpperCase") - .register_executor(lambda: ReverseExecutor(id="reverse"), name="Reverse") - .add_edge("UpperCase", "Reverse") - .build() - ) + upper = UpperCaseExecutor(id="upper") + reverse = ReverseExecutor(id="reverse") + + workflow = WorkflowBuilder(start_executor=upper).add_edge(upper, reverse).build() # Run the workflow events = await workflow.run("hello") @@ -156,9 +80,9 @@ class WorkflowBuilder: name: str | None = None, description: str | None = None, *, - start_executor: Executor | SupportsAgentRun | str, + start_executor: Executor | SupportsAgentRun, checkpoint_storage: CheckpointStorage | None = None, - output_executors: list[Executor | SupportsAgentRun | str] | None = None, + output_executors: list[Executor | SupportsAgentRun] | None = None, ): """Initialize the WorkflowBuilder. @@ -166,15 +90,15 @@ class WorkflowBuilder: max_iterations: Maximum number of iterations for workflow convergence. Default is 100. name: Optional human-readable name for the workflow. description: Optional description of what the workflow does. - start_executor: The starting executor for the workflow. Can be an Executor instance, - SupportsAgentRun instance, or the name of a registered executor factory. + start_executor: The starting executor for the workflow. Can be an Executor instance + or SupportsAgentRun instance. checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence. output_executors: Optional list of executors whose outputs should be collected. If not provided, outputs from all executors are collected. """ self._edge_groups: list[EdgeGroup] = [] self._executors: dict[str, Executor] = {} - self._start_executor: Executor | str | None = None + self._start_executor: Executor | None = None self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage self._max_iterations: int = max_iterations self._name: str | None = name @@ -184,18 +108,8 @@ class WorkflowBuilder: # being created for the same agent. self._agent_wrappers: dict[str, Executor] = {} - # Registrations for lazy initialization of executors - self._edge_registry: list[ - _EdgeRegistration - | _FanOutEdgeRegistration - | _SwitchCaseEdgeGroupRegistration - | _MultiSelectionEdgeGroupRegistration - | _FanInEdgeRegistration - ] = [] - self._executor_registry: dict[str, Callable[[], Executor]] = {} - # Output executors filter; if set, only outputs from these executors are yielded - self._output_executors: list[Executor | SupportsAgentRun | str] = output_executors if output_executors else [] + self._output_executors: list[Executor | SupportsAgentRun] = output_executors if output_executors else [] # Set the start executor self._set_start_executor(start_executor) @@ -258,133 +172,10 @@ class WorkflowBuilder: f"WorkflowBuilder expected an Executor or SupportsAgentRun instance; got {type(candidate).__name__}." ) - def register_executor(self, factory_func: Callable[[], Executor], name: str | list[str]) -> Self: - """Register an executor factory function for lazy initialization. - - This method allows you to register a factory function that creates an executor. - The executor will be instantiated only when the workflow is built, enabling - deferred initialization and potentially reducing startup time. - - Args: - factory_func: A callable that returns an Executor instance when called. - name: The name(s) of the registered executor factory. This doesn't have to match - the executor's ID, but it must be unique within the workflow. - - Example: - .. code-block:: python - from typing_extensions import Never - from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler - - - class UpperCaseExecutor(Executor): - @handler - async def process(self, text: str, ctx: WorkflowContext[str]) -> None: - await ctx.send_message(text.upper()) - - - class ReverseExecutor(Executor): - @handler - async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None: - await ctx.yield_output(text[::-1]) - - - # Build a workflow - workflow = ( - WorkflowBuilder(start_executor="UpperCase") - .register_executor(lambda: UpperCaseExecutor(id="upper"), name="UpperCase") - .register_executor(lambda: ReverseExecutor(id="reverse"), name="Reverse") - .add_edge("UpperCase", "Reverse") - .build() - ) - - If multiple names are provided, the same factory function will be registered under each name. - - .. code-block:: python - - from agent_framework import WorkflowBuilder, Executor, WorkflowContext, handler - - - class LoggerExecutor(Executor): - @handler - async def log(self, message: str, ctx: WorkflowContext) -> None: - print(f"Log: {message}") - - - # Register the same executor factory under multiple names - workflow = ( - WorkflowBuilder(start_executor="ExecutorA") - .register_executor(lambda: LoggerExecutor(id="logger"), name=["ExecutorA", "ExecutorB"]) - .add_edge("ExecutorA", "ExecutorB") - .build() - """ - names = [name] if isinstance(name, str) else name - - for n in names: - if n in self._executor_registry: - raise ValueError(f"An executor factory with the name '{n}' is already registered.") - - for n in names: - self._executor_registry[n] = factory_func - - return self - - def register_agent( - self, - factory_func: Callable[[], SupportsAgentRun], - name: str, - agent_thread: AgentThread | None = None, - ) -> Self: - """Register an agent factory function for lazy initialization. - - This method allows you to register a factory function that creates an agent. - The agent will be instantiated and wrapped in an AgentExecutor only when the workflow is built, - enabling deferred initialization and potentially reducing startup time. - - Args: - factory_func: A callable that returns an SupportsAgentRun instance when called. - name: The name of the registered agent factory. This doesn't have to match - the agent's internal name. But it must be unique within the workflow. - agent_thread: The thread to use for running the agent. If None, a new thread will be created when - the agent is instantiated. - - Example: - .. code-block:: python - - from agent_framework import WorkflowBuilder - from agent_framework_anthropic import AnthropicAgent - - - # Build a workflow - workflow = ( - WorkflowBuilder(start_executor="SomeOtherExecutor") - .register_executor(lambda: ..., name="SomeOtherExecutor") - .register_agent( - lambda: AnthropicAgent(name="writer", model="claude-3-5-sonnet-20241022"), - name="WriterAgent", - output_response=True, - ) - .add_edge("SomeOtherExecutor", "WriterAgent") - .build() - ) - """ - if name in self._executor_registry: - raise ValueError(f"An agent factory with the name '{name}' is already registered.") - - def wrapped_factory() -> AgentExecutor: - agent = factory_func() - return AgentExecutor( - agent, - agent_thread=agent_thread, - ) - - self._executor_registry[name] = wrapped_factory - - return self - def add_edge( self, - source: Executor | SupportsAgentRun | str, - target: Executor | SupportsAgentRun | str, + source: Executor | SupportsAgentRun, + target: Executor | SupportsAgentRun, condition: EdgeCondition | None = None, ) -> Self: """Add a directed edge between two executors. @@ -393,17 +184,12 @@ class WorkflowBuilder: Messages sent by the source executor will be routed to the target executor. Args: - source: The source executor or registered name of the source factory for the edge. - target: The target executor or registered name of the target factory for the edge. + source: The source executor or agent for the edge. + target: The target executor or agent for the edge. condition: An optional condition function `(data) -> bool | Awaitable[bool]` that determines whether the edge should be traversed. Example: `lambda data: data["ready"]`. - Note: If instances are provided for both source and target, they will be shared across - all workflow instances created from the built Workflow. To avoid this, consider - registering the executors and agents using `register_executor` and `register_agent` - and referencing them by factory name for lazy initialization instead. - Returns: Self: The WorkflowBuilder instance for method chaining. @@ -426,39 +212,13 @@ class WorkflowBuilder: await ctx.yield_output(f"Processed {count} characters") - # Connect executors with an edge - workflow = ( - WorkflowBuilder(start_executor="ProcessorA") - .register_executor(lambda: ProcessorA(id="a"), name="ProcessorA") - .register_executor(lambda: ProcessorB(id="b"), name="ProcessorB") - .add_edge("ProcessorA", "ProcessorB") - .build() - ) + a = ProcessorA(id="a") + b = ProcessorB(id="b") - workflow = ( - WorkflowBuilder(start_executor="ProcessorA") - .register_executor(lambda: ProcessorA(id="a"), name="ProcessorA") - .register_executor(lambda: ProcessorB(id="b"), name="ProcessorB") - .add_edge("ProcessorA", "ProcessorB", condition=only_large_numbers) - .build() - ) + workflow = WorkflowBuilder(start_executor=a).add_edge(a, b).build() """ - if (isinstance(source, str) and not isinstance(target, str)) or ( - not isinstance(source, str) and isinstance(target, str) - ): - raise ValueError( - "Both source and target must be either registered factory names (str) or " - "Executor/SupportsAgentRun instances." - ) - - if isinstance(source, str) and isinstance(target, str): - # Both are names; defer resolution to build time - self._edge_registry.append(_EdgeRegistration(source=source, target=target, condition=condition)) - return self - - # Both are Executor/SupportsAgentRun instances; wrap and add now - source_exec = self._maybe_wrap_agent(source) # type: ignore[arg-type] - target_exec = self._maybe_wrap_agent(target) # type: ignore[arg-type] + source_exec = self._maybe_wrap_agent(source) + target_exec = self._maybe_wrap_agent(target) source_id = self._add_executor(source_exec) target_id = self._add_executor(target_exec) self._edge_groups.append(SingleEdgeGroup(source_id, target_id, condition)) @@ -466,8 +226,8 @@ class WorkflowBuilder: def add_fan_out_edges( self, - source: Executor | SupportsAgentRun | str, - targets: Sequence[Executor | SupportsAgentRun | str], + source: Executor | SupportsAgentRun, + targets: Sequence[Executor | SupportsAgentRun], ) -> Self: """Add multiple edges to the workflow where messages from the source will be sent to all targets. @@ -475,17 +235,12 @@ class WorkflowBuilder: Messages from the source will be broadcast to all target executors concurrently. Args: - source: The source executor or registered name of the source factory for the edges. - targets: A list of target executors or registered names of the target factories for the edges. + source: The source executor or agent for the edges. + targets: A list of target executors or agents for the edges. Returns: Self: The WorkflowBuilder instance for method chaining. - Note: If instances are provided for source and targets, they will be shared across - all workflow instances created from the built Workflow. To avoid this, consider - registering the executors and agents using `register_executor` and `register_agent` - and referencing them by factory name for lazy initialization instead. - Example: .. code-block:: python @@ -511,32 +266,14 @@ class WorkflowBuilder: print(f"ValidatorB: {data}") - # Broadcast to multiple validators - workflow = ( - WorkflowBuilder(start_executor="DataSource") - .register_executor(lambda: DataSource(id="source"), name="DataSource") - .register_executor(lambda: ValidatorA(id="val_a"), name="ValidatorA") - .register_executor(lambda: ValidatorB(id="val_b"), name="ValidatorB") - .add_fan_out_edges("DataSource", ["ValidatorA", "ValidatorB"]) - .build() - ) + source = DataSource(id="source") + val_a = ValidatorA(id="val_a") + val_b = ValidatorB(id="val_b") + + workflow = WorkflowBuilder(start_executor=source).add_fan_out_edges(source, [val_a, val_b]).build() """ - if (isinstance(source, str) and not all(isinstance(t, str) for t in targets)) or ( - not isinstance(source, str) and any(isinstance(t, str) for t in targets) - ): - raise ValueError( - "Both source and targets must be either registered factory names (str) or " - "Executor/SupportsAgentRun instances." - ) - - if isinstance(source, str) and all(isinstance(t, str) for t in targets): - # Both are names; defer resolution to build time - self._edge_registry.append(_FanOutEdgeRegistration(source=source, targets=list(targets))) # type: ignore - return self - - # Both are Executor/SupportsAgentRun instances; wrap and add now - source_exec = self._maybe_wrap_agent(source) # type: ignore[arg-type] - target_execs = [self._maybe_wrap_agent(t) for t in targets] # type: ignore[arg-type] + source_exec = self._maybe_wrap_agent(source) + target_execs = [self._maybe_wrap_agent(t) for t in targets] source_id = self._add_executor(source_exec) target_ids = [self._add_executor(t) for t in target_execs] self._edge_groups.append(FanOutEdgeGroup(source_id, target_ids)) # type: ignore[call-arg] @@ -545,7 +282,7 @@ class WorkflowBuilder: def add_switch_case_edge_group( self, - source: Executor | SupportsAgentRun | str, + source: Executor | SupportsAgentRun, cases: Sequence[Case | Default], ) -> Self: """Add an edge group that represents a switch-case statement. @@ -562,17 +299,12 @@ class WorkflowBuilder: (i.e., no condition matched). Args: - source: The source executor or registered name of the source factory for the edge group. + source: The source executor or agent for the edge group. cases: A list of case objects that determine the target executor for each message. Returns: Self: The WorkflowBuilder instance for method chaining. - Note: If instances are provided for source and case targets, they will be shared across - all workflow instances created from the built Workflow. To avoid this, consider - registering the executors and agents using `register_executor` and `register_agent` - and referencing them by factory name for lazy initialization instead. - Example: .. code-block:: python @@ -603,37 +335,23 @@ class WorkflowBuilder: print(f"Low score: {result.score}") - # Route based on score value + evaluator = Evaluator(id="eval") + high = HighScoreHandler(id="high") + low = LowScoreHandler(id="low") + workflow = ( - WorkflowBuilder(start_executor="Evaluator") - .register_executor(lambda: Evaluator(id="eval"), name="Evaluator") - .register_executor(lambda: HighScoreHandler(id="high"), name="HighScoreHandler") - .register_executor(lambda: LowScoreHandler(id="low"), name="LowScoreHandler") + WorkflowBuilder(start_executor=evaluator) .add_switch_case_edge_group( - "Evaluator", + evaluator, [ - Case(condition=lambda r: r.score > 10, target="HighScoreHandler"), - Default(target="LowScoreHandler"), + Case(condition=lambda r: r.score > 10, target=high), + Default(target=low), ], ) .build() ) """ - if (isinstance(source, str) and not all(isinstance(case.target, str) for case in cases)) or ( - not isinstance(source, str) and any(isinstance(case.target, str) for case in cases) - ): - raise ValueError( - "Both source and case targets must be either registered factory names (str) " - "or Executor/SupportsAgentRun instances." - ) - - if isinstance(source, str) and all(isinstance(case.target, str) for case in cases): - # Source is a name; defer resolution to build time - self._edge_registry.append(_SwitchCaseEdgeGroupRegistration(source=source, cases=list(cases))) # type: ignore - return self - - # Source is an Executor/SupportsAgentRun instance; wrap and add now - source_exec = self._maybe_wrap_agent(source) # type: ignore[arg-type] + source_exec = self._maybe_wrap_agent(source) source_id = self._add_executor(source_exec) # Convert case data types to internal types that only uses target_id. internal_cases: list[SwitchCaseEdgeGroupCase | SwitchCaseEdgeGroupDefault] = [] @@ -651,8 +369,8 @@ class WorkflowBuilder: def add_multi_selection_edge_group( self, - source: Executor | SupportsAgentRun | str, - targets: Sequence[Executor | SupportsAgentRun | str], + source: Executor | SupportsAgentRun, + targets: Sequence[Executor | SupportsAgentRun], selection_func: Callable[[Any, list[str]], list[str]], ) -> Self: """Add an edge group that represents a multi-selection execution model. @@ -665,19 +383,14 @@ class WorkflowBuilder: and return a list of executor IDs indicating which target executors should receive the message. Args: - source: The source executor or registered name of the source factory for the edge group. - targets: A list of target executors or registered names of the target factories for the edges. + source: The source executor or agent for the edge group. + targets: A list of target executors or agents for the edges. selection_func: A function that selects target executors for messages. Takes (message, list[executor_id]) and returns list[executor_id]. Returns: Self: The WorkflowBuilder instance for method chaining. - Note: If instances are provided for source and targets, they will be shared across - all workflow instances created from the built Workflow. To avoid this, consider - registering the executors and agents using `register_executor` and `register_agent` - and referencing them by factory name for lazy initialization instead. - Example: .. code-block:: python @@ -710,6 +423,11 @@ class WorkflowBuilder: print(f"WorkerB processing: {task.data}") + dispatcher = TaskDispatcher(id="dispatcher") + worker_a = WorkerA(id="worker_a") + worker_b = WorkerB(id="worker_b") + + # Select workers based on task priority def select_workers(task: Task, available: list[str]) -> list[str]: if task.priority == "high": @@ -718,40 +436,17 @@ class WorkflowBuilder: workflow = ( - WorkflowBuilder(start_executor="TaskDispatcher") - .register_executor(lambda: TaskDispatcher(id="dispatcher"), name="TaskDispatcher") - .register_executor(lambda: WorkerA(id="worker_a"), name="WorkerA") - .register_executor(lambda: WorkerB(id="worker_b"), name="WorkerB") + WorkflowBuilder(start_executor=dispatcher) .add_multi_selection_edge_group( - "TaskDispatcher", - ["WorkerA", "WorkerB"], + dispatcher, + [worker_a, worker_b], selection_func=select_workers, ) .build() ) """ - if (isinstance(source, str) and not all(isinstance(t, str) for t in targets)) or ( - not isinstance(source, str) and any(isinstance(t, str) for t in targets) - ): - raise ValueError( - "Both source and targets must be either registered factory names (str) or " - "Executor/SupportsAgentRun instances." - ) - - if isinstance(source, str) and all(isinstance(t, str) for t in targets): - # Both are names; defer resolution to build time - self._edge_registry.append( - _MultiSelectionEdgeGroupRegistration( - source=source, - targets=list(targets), # type: ignore - selection_func=selection_func, - ) - ) - return self - - # Both are Executor/SupportsAgentRun instances; wrap and add now - source_exec = self._maybe_wrap_agent(source) # type: ignore - target_execs = [self._maybe_wrap_agent(t) for t in targets] # type: ignore + source_exec = self._maybe_wrap_agent(source) + target_execs = [self._maybe_wrap_agent(t) for t in targets] source_id = self._add_executor(source_exec) target_ids = [self._add_executor(t) for t in target_execs] self._edge_groups.append(FanOutEdgeGroup(source_id, target_ids, selection_func)) # type: ignore[call-arg] @@ -760,8 +455,8 @@ class WorkflowBuilder: def add_fan_in_edges( self, - sources: Sequence[Executor | SupportsAgentRun | str], - target: Executor | SupportsAgentRun | str, + sources: Sequence[Executor | SupportsAgentRun], + target: Executor | SupportsAgentRun, ) -> Self: """Add multiple edges from sources to a single target executor. @@ -773,17 +468,12 @@ class WorkflowBuilder: types of the source executors. Args: - sources: A list of source executors or registered names of the source factories for the edges. - target: The target executor or registered name of the target factory for the edges. + sources: A list of source executors or agents for the edges. + target: The target executor or agent for the edges. Returns: Self: The WorkflowBuilder instance for method chaining. - Note: If instances are provided for sources and target, they will be shared across - all workflow instances created from the built Workflow. To avoid this, consider - registering the executors and agents using `register_executor` and `register_agent` - and referencing them by factory name for lazy initialization instead. - Example: .. code-block:: python @@ -804,39 +494,21 @@ class WorkflowBuilder: await ctx.yield_output(f"Combined: {combined}") - # Collect results from multiple producers - workflow = ( - WorkflowBuilder(start_executor="Producer1") - .register_executor(lambda: Producer(id="prod_1"), name="Producer1") - .register_executor(lambda: Producer(id="prod_2"), name="Producer2") - .register_executor(lambda: Aggregator(id="agg"), name="Aggregator") - .add_fan_in_edges(["Producer1", "Producer2"], "Aggregator") - .build() - ) + prod_1 = Producer(id="prod_1") + prod_2 = Producer(id="prod_2") + agg = Aggregator(id="agg") + + workflow = WorkflowBuilder(start_executor=prod_1).add_fan_in_edges([prod_1, prod_2], agg).build() """ - if (all(isinstance(s, str) for s in sources) and not isinstance(target, str)) or ( - not all(isinstance(s, str) for s in sources) and isinstance(target, str) - ): - raise ValueError( - "Both sources and target must be either registered factory names (str) or " - "Executor/SupportsAgentRun instances." - ) - - if all(isinstance(s, str) for s in sources) and isinstance(target, str): - # Both are names; defer resolution to build time - self._edge_registry.append(_FanInEdgeRegistration(sources=list(sources), target=target)) # type: ignore - return self - - # Both are Executor/SupportsAgentRun instances; wrap and add now - source_execs = [self._maybe_wrap_agent(s) for s in sources] # type: ignore - target_exec = self._maybe_wrap_agent(target) # type: ignore + source_execs = [self._maybe_wrap_agent(s) for s in sources] + target_exec = self._maybe_wrap_agent(target) source_ids = [self._add_executor(s) for s in source_execs] target_id = self._add_executor(target_exec) self._edge_groups.append(FanInEdgeGroup(source_ids, target_id)) # type: ignore[call-arg] return self - def add_chain(self, executors: Sequence[Executor | SupportsAgentRun | str]) -> Self: + def add_chain(self, executors: Sequence[Executor | SupportsAgentRun]) -> Self: """Add a chain of executors to the workflow. The output of each executor in the chain will be sent to the next executor in the chain. @@ -845,16 +517,11 @@ class WorkflowBuilder: Cycles in the chain are not allowed, meaning an executor cannot appear more than once in the chain. Args: - executors: A list of executors or registered names of the executor factories to chain together. + executors: A list of executors or agents to chain together. Returns: Self: The WorkflowBuilder instance for method chaining. - Note: If executor instances are provided, they will be shared across all workflow instances created - from the built Workflow. To avoid this, consider registering the executors and agents using - `register_executor` and `register_agent` and referencing them by factory name for lazy - initialization instead. - Example: .. code-block:: python @@ -880,148 +547,37 @@ class WorkflowBuilder: await ctx.yield_output(f"Final: {text}") - # Chain executors in sequence - workflow = ( - WorkflowBuilder(start_executor="step1") - .register_executor(lambda: Step1(id="step1"), name="step1") - .register_executor(lambda: Step2(id="step2"), name="step2") - .register_executor(lambda: Step3(id="step3"), name="step3") - .add_chain(["step1", "step2", "step3"]) - .build() - ) + step1 = Step1(id="step1") + step2 = Step2(id="step2") + step3 = Step3(id="step3") + + workflow = WorkflowBuilder(start_executor=step1).add_chain([step1, step2, step3]).build() """ if len(executors) < 2: raise ValueError("At least two executors are required to form a chain.") - if not all(isinstance(e, str) for e in executors) and any(isinstance(e, str) for e in executors): - raise ValueError( - "All executors in the chain must be either registered factory names (str) " - "or Executor/SupportsAgentRun instances." - ) - - if all(isinstance(e, str) for e in executors): - # All are names; defer resolution to build time - for i in range(len(executors) - 1): - self.add_edge(executors[i], executors[i + 1]) - return self - - # All are Executor/SupportsAgentRun instances; wrap and add now # Wrap each candidate first to ensure stable IDs before adding edges - wrapped: list[Executor] = [self._maybe_wrap_agent(e) for e in executors] # type: ignore[arg-type] + wrapped: list[Executor] = [self._maybe_wrap_agent(e) for e in executors] for i in range(len(wrapped) - 1): self.add_edge(wrapped[i], wrapped[i + 1]) return self - def _set_start_executor(self, executor: Executor | SupportsAgentRun | str) -> None: + def _set_start_executor(self, executor: Executor | SupportsAgentRun) -> None: """Set the starting executor for the workflow (internal method). Args: - executor: The starting executor, which can be an Executor instance, SupportsAgentRun instance, - or the name of a registered executor factory. + executor: The starting executor, which can be an Executor instance or SupportsAgentRun instance. """ if self._start_executor is not None: - start_id = self._start_executor if isinstance(self._start_executor, str) else self._start_executor.id - logger.warning(f"Overwriting existing start executor: {start_id} for the workflow.") + logger.warning(f"Overwriting existing start executor: {self._start_executor.id} for the workflow.") - if isinstance(executor, str): - self._start_executor = executor - else: - wrapped = self._maybe_wrap_agent(executor) # type: ignore[arg-type] - self._start_executor = wrapped - # Ensure the start executor is present in the executor map so validation succeeds - # even if no edges are added yet, or before edges wrap the same agent again. - existing = self._executors.get(wrapped.id) - if existing is not wrapped: - self._add_executor(wrapped) - - # Removed explicit set_agent_streaming() API; agents always stream updates. - - def _resolve_edge_registry(self) -> tuple[Executor, dict[str, Executor], list[EdgeGroup]]: - """Resolve deferred edge registrations into executors and edge groups. - - Returns: - tuple: A tuple containing: - - The starting Executor instance. - - A dictionary mapping registered factory names to resolved Executor instances. - - A list of EdgeGroup instances representing the workflow edges composed of resolved executors. - - Notes: - Non-factory executors (i.e., those added directly) are not included in the returned list, - as they are already part of the workflow builder's internal state. - """ - if not self._start_executor: - raise ValueError( - "Starting executor must be set via the start_executor constructor parameter before building." - ) - - start_executor: Executor | None = None - if isinstance(self._start_executor, Executor): - start_executor = self._start_executor - - # Maps registered factory names to created executor instances for edge resolution - factory_name_to_instance: dict[str, Executor] = {} - # Maps executor IDs to created executor instances to prevent duplicates - executor_id_to_instance: dict[str, Executor] = {} - deferred_edge_groups: list[EdgeGroup] = [] - for name, exec_factory in self._executor_registry.items(): - instance = exec_factory() - if instance.id in executor_id_to_instance: - raise ValueError(f"Executor with ID '{instance.id}' has already been registered.") - if instance.id in self._executors: - raise ValueError(f"Executor ID collision: An executor with ID '{instance.id}' already exists.") - executor_id_to_instance[instance.id] = instance - - if isinstance(self._start_executor, str) and name == self._start_executor: - start_executor = instance - - # All executors will get their own internal edge group for receiving system messages - deferred_edge_groups.append(InternalEdgeGroup(instance.id)) # type: ignore[call-arg] - factory_name_to_instance[name] = instance - - def _get_executor(name: str) -> Executor: - """Helper to get executor by the registered name. Raises if not found.""" - if name not in factory_name_to_instance: - raise ValueError(f"Factory '{name}' has not been registered.") - return factory_name_to_instance[name] - - for registration in self._edge_registry: - match registration: - case _EdgeRegistration(source, target, condition): - source_exec: Executor = _get_executor(source) - target_exec: Executor = _get_executor(target) - deferred_edge_groups.append(SingleEdgeGroup(source_exec.id, target_exec.id, condition)) # type: ignore[call-arg] - case _FanOutEdgeRegistration(source, targets): - source_exec = _get_executor(source) - target_execs = [_get_executor(t) for t in targets] - deferred_edge_groups.append(FanOutEdgeGroup(source_exec.id, [t.id for t in target_execs])) # type: ignore[call-arg] - case _SwitchCaseEdgeGroupRegistration(source, cases): - source_exec = _get_executor(source) - cases_converted: list[SwitchCaseEdgeGroupCase | SwitchCaseEdgeGroupDefault] = [] - for case in cases: - if not isinstance(case.target, str): - raise ValueError("Switch case target must be a registered factory name (str) if deferred.") - target_exec = _get_executor(case.target) - if isinstance(case, Default): - cases_converted.append(SwitchCaseEdgeGroupDefault(target_id=target_exec.id)) - else: - cases_converted.append( - SwitchCaseEdgeGroupCase(condition=case.condition, target_id=target_exec.id) - ) - deferred_edge_groups.append(SwitchCaseEdgeGroup(source_exec.id, cases_converted)) # type: ignore[call-arg] - case _MultiSelectionEdgeGroupRegistration(source, targets, selection_func): - source_exec = _get_executor(source) - target_execs = [_get_executor(t) for t in targets] - deferred_edge_groups.append( - FanOutEdgeGroup(source_exec.id, [t.id for t in target_execs], selection_func) # type: ignore[call-arg] - ) - case _FanInEdgeRegistration(sources, target): - source_execs = [_get_executor(s) for s in sources] - target_exec = _get_executor(target) - deferred_edge_groups.append(FanInEdgeGroup([s.id for s in source_execs], target_exec.id)) # type: ignore[call-arg] - if start_executor is None: - raise ValueError("Failed to resolve starting executor from registered factories.") - - return (start_executor, factory_name_to_instance, deferred_edge_groups) + wrapped = self._maybe_wrap_agent(executor) + self._start_executor = wrapped + # Ensure the start executor is present in the executor map so validation succeeds + # even if no edges are added yet, or before edges wrap the same agent again. + existing = self._executors.get(wrapped.id) + if existing is not wrapped: + self._add_executor(wrapped) def build(self) -> Workflow: """Build and return the constructed workflow. @@ -1053,12 +609,9 @@ class WorkflowBuilder: await ctx.yield_output(text.upper()) - # Build and execute a workflow - workflow = ( - WorkflowBuilder(start_executor="MyExecutor") - .register_executor(lambda: MyExecutor(id="executor"), name="MyExecutor") - .build() - ) + executor = MyExecutor(id="executor") + + workflow = WorkflowBuilder(start_executor=executor).build() # The workflow is now immutable and ready to run events = await workflow.run("hello") @@ -1074,23 +627,17 @@ class WorkflowBuilder: # Add workflow build started event span.add_event(OtelAttr.BUILD_STARTED) - # Resolve lazy edge registrations - start_executor, deferred_executors, deferred_edge_groups = self._resolve_edge_registry() - executors = self._executors | {exe.id: exe for exe in deferred_executors.values()} - edge_groups = self._edge_groups + deferred_edge_groups - output_executors = ( - [ - deferred_executors[factory_name].id - for factory_name in self._output_executors - if isinstance(factory_name, str) - ] - + [ex.id for ex in self._output_executors if isinstance(ex, Executor)] - + [ - resolve_agent_id(agent) - for agent in self._output_executors - if isinstance(agent, SupportsAgentRun) - ] - ) + if not self._start_executor: + raise ValueError( + "Starting executor must be set via the start_executor constructor parameter before building." + ) + + start_executor = self._start_executor + executors = self._executors + edge_groups = self._edge_groups + output_executors = [ex.id for ex in self._output_executors if isinstance(ex, Executor)] + [ + resolve_agent_id(agent) for agent in self._output_executors if isinstance(agent, SupportsAgentRun) + ] # Perform validation before creating the workflow validate_workflow_graph( diff --git a/python/packages/core/agent_framework/_workflows/_workflow_context.py b/python/packages/core/agent_framework/_workflows/_workflow_context.py index 2bdd81ef41..51add07a5c 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_context.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_context.py @@ -19,7 +19,7 @@ from ._events import ( WorkflowEventSource, _framework_event_origin, # type: ignore ) -from ._runner_context import Message, RunnerContext +from ._runner_context import RunnerContext, WorkflowMessage from ._state import State if TYPE_CHECKING: @@ -321,7 +321,7 @@ class WorkflowContext(Generic[OutT, W_OutT]): attributes[OtelAttr.MESSAGE_DESTINATION_EXECUTOR_ID] = target_id with create_workflow_span(OtelAttr.MESSAGE_SEND_SPAN, attributes, kind=SpanKind.PRODUCER) as span: # Create Message wrapper - msg = Message(data=message, source_id=self._executor_id, target_id=target_id) + msg = WorkflowMessage(data=message, source_id=self._executor_id, target_id=target_id) # Track sent message for executor_completed event (type='executor_completed') self._sent_messages.append(message) diff --git a/python/packages/core/agent_framework/_workflows/_workflow_executor.py b/python/packages/core/agent_framework/_workflows/_workflow_executor.py index 319af46076..3e5fd449bc 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_executor.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_executor.py @@ -19,7 +19,7 @@ from ._events import ( ) from ._executor import Executor, handler from ._request_info_mixin import response_handler -from ._runner_context import Message +from ._runner_context import WorkflowMessage from ._typing_utils import is_instance_of from ._workflow import WorkflowRunResult from ._workflow_context import WorkflowContext @@ -340,7 +340,7 @@ class WorkflowExecutor(Executor): data["workflow"] = self.workflow.to_dict() return data - def can_handle(self, message: Message) -> bool: + def can_handle(self, message: WorkflowMessage) -> bool: """Override can_handle to only accept messages that the wrapped workflow can handle. This prevents the WorkflowExecutor from accepting messages that should go to other diff --git a/python/packages/core/agent_framework/devui/__init__.py b/python/packages/core/agent_framework/devui/__init__.py index 3e3312f10c..cd18b0c5da 100644 --- a/python/packages/core/agent_framework/devui/__init__.py +++ b/python/packages/core/agent_framework/devui/__init__.py @@ -14,6 +14,7 @@ _IMPORTS = [ "OpenAIResponse", "ResponseStreamEvent", "main", + "register_cleanup", "serve", "__version__", ] diff --git a/python/packages/core/agent_framework/devui/__init__.pyi b/python/packages/core/agent_framework/devui/__init__.pyi index 3c1cac827f..9396af54bb 100644 --- a/python/packages/core/agent_framework/devui/__init__.pyi +++ b/python/packages/core/agent_framework/devui/__init__.pyi @@ -10,6 +10,7 @@ from agent_framework_devui import ( ResponseStreamEvent, __version__, main, + register_cleanup, serve, ) @@ -23,5 +24,6 @@ __all__ = [ "ResponseStreamEvent", "__version__", "main", + "register_cleanup", "serve", ] diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index 34c58b3b1a..c97ae0168a 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -39,18 +39,18 @@ if TYPE_CHECKING: # pragma: no cover from pydantic import BaseModel from ._agents import SupportsAgentRun - from ._clients import ChatClientProtocol + from ._clients import SupportsChatGetResponse from ._threads import AgentThread from ._tools import FunctionTool from ._types import ( AgentResponse, AgentResponseUpdate, - ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, Content, FinishReason, + Message, ResponseStream, ) @@ -71,7 +71,7 @@ __all__ = [ AgentT = TypeVar("AgentT", bound="SupportsAgentRun") -ChatClientT = TypeVar("ChatClientT", bound="ChatClientProtocol[Any]") +ChatClientT = TypeVar("ChatClientT", bound="SupportsChatGetResponse[Any]") logger = get_logger() @@ -122,7 +122,7 @@ OPERATION_DURATION_BUCKET_BOUNDARIES: Final[tuple[float, ...]] = ( # # This is a workaround, we'll find a generic and better solution - see # https://github.com/open-telemetry/semantic-conventions/issues/1701 -class ChatMessageListTimestampFilter(logging.Filter): +class MessageListTimestampFilter(logging.Filter): """A filter to increment the timestamp of INFO logs by 1 microsecond.""" INDEX_KEY: ClassVar[str] = "chat_message_index" @@ -135,7 +135,7 @@ class ChatMessageListTimestampFilter(logging.Filter): return True -logger.addFilter(ChatMessageListTimestampFilter()) +logger.addFilter(MessageListTimestampFilter()) class OtelAttr(str, Enum): @@ -1070,7 +1070,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: ChatOptions[ResponseModelBoundT], @@ -1080,7 +1080,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[False] = ..., options: OptionsCoT | ChatOptions[None] | None = None, @@ -1090,7 +1090,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): @overload def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: Literal[True], options: OptionsCoT | ChatOptions[Any] | None = None, @@ -1099,7 +1099,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): def get_response( self, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], *, stream: bool = False, options: OptionsCoT | ChatOptions[Any] | None = None, @@ -1139,8 +1139,14 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): else: raise RuntimeError("Streaming telemetry requires a ResponseStream result.") - span_cm = _get_span(attributes=attributes, span_name_attribute=SpanAttributes.LLM_REQUEST_MODEL) - span = span_cm.__enter__() + # Create span directly without trace.use_span() context attachment. + # Streaming spans are closed asynchronously in cleanup hooks, which run + # in a different async context than creation — using use_span() would + # cause "Failed to detach context" errors from OpenTelemetry. + operation = attributes.get(OtelAttr.OPERATION, "operation") + span_name = attributes.get(SpanAttributes.LLM_REQUEST_MODEL, "unknown") + span = get_tracer().start_span(f"{operation} {span_name}") + span.set_attributes(attributes) if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages: _capture_messages( span=span, @@ -1157,7 +1163,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): if span_state["closed"]: return span_state["closed"] = True - span_cm.__exit__(None, None, None) + span.end() def _record_duration() -> None: duration_state["duration"] = perf_counter() - start_time @@ -1257,7 +1263,7 @@ class AgentTelemetryLayer: @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[False] = ..., thread: AgentThread | None = None, @@ -1267,7 +1273,7 @@ class AgentTelemetryLayer: @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[True], thread: AgentThread | None = None, @@ -1276,7 +1282,7 @@ class AgentTelemetryLayer: def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -1326,8 +1332,14 @@ class AgentTelemetryLayer: else: raise RuntimeError("Streaming telemetry requires a ResponseStream result.") - span_cm = _get_span(attributes=attributes, span_name_attribute=OtelAttr.AGENT_NAME) - span = span_cm.__enter__() + # Create span directly without trace.use_span() context attachment. + # Streaming spans are closed asynchronously in cleanup hooks, which run + # in a different async context than creation — using use_span() would + # cause "Failed to detach context" errors from OpenTelemetry. + operation = attributes.get(OtelAttr.OPERATION, "operation") + span_name = attributes.get(OtelAttr.AGENT_NAME, "unknown") + span = get_tracer().start_span(f"{operation} {span_name}") + span.set_attributes(attributes) if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages: _capture_messages( span=span, @@ -1344,7 +1356,7 @@ class AgentTelemetryLayer: if span_state["closed"]: return span_state["closed"] = True - span_cm.__exit__(None, None, None) + span.end() def _record_duration() -> None: duration_state["duration"] = perf_counter() - start_time @@ -1588,7 +1600,7 @@ def capture_exception(span: trace.Span, exception: Exception, timestamp: int | N def _capture_messages( span: trace.Span, provider_name: str, - messages: str | ChatMessage | Sequence[str | ChatMessage], + messages: str | Message | Sequence[str | Message], system_instructions: str | list[str] | None = None, output: bool = False, finish_reason: FinishReason | None = None, @@ -1608,7 +1620,7 @@ def _capture_messages( extra={ OtelAttr.EVENT_NAME: OtelAttr.CHOICE if output else ROLE_EVENT_MAP.get(message.role), OtelAttr.PROVIDER_NAME: provider_name, - ChatMessageListTimestampFilter.INDEX_KEY: index, + MessageListTimestampFilter.INDEX_KEY: index, }, ) if finish_reason: @@ -1621,7 +1633,7 @@ def _capture_messages( span.set_attribute(OtelAttr.SYSTEM_INSTRUCTIONS, json.dumps(otel_sys_instructions)) -def _to_otel_message(message: ChatMessage) -> dict[str, Any]: +def _to_otel_message(message: Message) -> dict[str, Any]: """Create a otel representation of a message.""" return {"role": message.role, "parts": [_to_otel_part(content) for content in message.contents]} diff --git a/python/packages/core/agent_framework/openai/_assistant_provider.py b/python/packages/core/agent_framework/openai/_assistant_provider.py index 7b662e4c2a..8095f04fe1 100644 --- a/python/packages/core/agent_framework/openai/_assistant_provider.py +++ b/python/packages/core/agent_framework/openai/_assistant_provider.py @@ -10,10 +10,10 @@ from openai import AsyncOpenAI from openai.types.beta.assistant import Assistant from pydantic import BaseModel, SecretStr, ValidationError -from .._agents import ChatAgent +from .._agents import Agent from .._memory import ContextProvider from .._middleware import MiddlewareTypes -from .._tools import FunctionTool, ToolProtocol +from .._tools import FunctionTool from .._types import normalize_tools from ..exceptions import ServiceInitializationError from ._assistants_client import OpenAIAssistantsClient @@ -43,18 +43,18 @@ OptionsCoT = TypeVar( ) _ToolsType = ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] ) class OpenAIAssistantProvider(Generic[OptionsCoT]): - """Provider for creating ChatAgent instances from OpenAI Assistants API. + """Provider for creating Agent instances from OpenAI Assistants API. This provider allows you to create, retrieve, and wrap OpenAI Assistants - as ChatAgent instances for use in the agent framework. + as Agent instances for use in the agent framework. Examples: Basic usage with automatic client creation: @@ -208,11 +208,11 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Create a new assistant on OpenAI and return a ChatAgent. + ) -> Agent[OptionsCoT]: + """Create a new assistant on OpenAI and return a Agent. This method creates a new assistant on the OpenAI service and wraps it - in a ChatAgent instance. The assistant will persist on OpenAI until deleted. + in a Agent instance. The assistant will persist on OpenAI until deleted. Keyword Args: name: The name of the assistant (required). @@ -221,18 +221,18 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): description: A description of the assistant. tools: Tools available to the assistant. Can include: - FunctionTool instances or callables decorated with @tool - - HostedCodeInterpreterTool for code execution - - HostedFileSearchTool for vector store search + - Dict-based tools from OpenAIAssistantsClient.get_code_interpreter_tool() + - Dict-based tools from OpenAIAssistantsClient.get_file_search_tool() - Raw tool dictionaries metadata: Metadata to attach to the assistant (max 16 key-value pairs). default_options: A TypedDict containing default chat options for the agent. These options are applied to every run unless overridden. Include ``response_format`` here for structured output responses. - middleware: MiddlewareTypes for the ChatAgent. - context_provider: Context provider for the ChatAgent. + middleware: MiddlewareTypes for the Agent. + context_provider: Context provider for the Agent. Returns: - A ChatAgent instance wrapping the created assistant. + A Agent instance wrapping the created assistant. Raises: ServiceInitializationError: If assistant creation fails. @@ -297,7 +297,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): assistant = await self._client.beta.assistants.create(**create_params) - # Create ChatAgent - pass default_options which contains response_format + # Create Agent - pass default_options which contains response_format return self._create_chat_agent_from_assistant( assistant=assistant, tools=normalized_tools, @@ -316,11 +316,11 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Retrieve an existing assistant by ID and return a ChatAgent. + ) -> Agent[OptionsCoT]: + """Retrieve an existing assistant by ID and return a Agent. This method fetches an existing assistant from OpenAI by its ID - and wraps it in a ChatAgent instance. + and wraps it in a Agent instance. Args: assistant_id: The ID of the assistant to retrieve (e.g., "asst_123"). @@ -333,11 +333,11 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): instructions: Override the assistant's instructions (optional). default_options: A TypedDict containing default chat options for the agent. These options are applied to every run unless overridden. - middleware: MiddlewareTypes for the ChatAgent. - context_provider: Context provider for the ChatAgent. + middleware: MiddlewareTypes for the Agent. + context_provider: Context provider for the Agent. Returns: - A ChatAgent instance wrapping the retrieved assistant. + A Agent instance wrapping the retrieved assistant. Raises: ServiceInitializationError: If the assistant cannot be retrieved. @@ -382,11 +382,11 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): default_options: OptionsCoT | None = None, middleware: Sequence[MiddlewareTypes] | None = None, context_provider: ContextProvider | None = None, - ) -> ChatAgent[OptionsCoT]: - """Wrap an existing SDK Assistant object as a ChatAgent. + ) -> Agent[OptionsCoT]: + """Wrap an existing SDK Assistant object as a Agent. This method does NOT make any HTTP calls. It simply wraps an already- - fetched Assistant object in a ChatAgent. + fetched Assistant object in a Agent. Args: assistant: The OpenAI Assistant SDK object to wrap. @@ -398,11 +398,11 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): instructions: Override the assistant's instructions (optional). default_options: A TypedDict containing default chat options for the agent. These options are applied to every run unless overridden. - middleware: MiddlewareTypes for the ChatAgent. - context_provider: Context provider for the ChatAgent. + middleware: MiddlewareTypes for the Agent. + context_provider: Context provider for the Agent. Returns: - A ChatAgent instance wrapping the assistant. + A Agent instance wrapping the assistant. Raises: ValueError: If required function tools are missing. @@ -429,7 +429,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): # Merge hosted tools with user-provided function tools merged_tools = self._merge_tools(assistant.tools or [], tools) - # Create ChatAgent + # Create Agent return self._create_chat_agent_from_assistant( assistant=assistant, tools=merged_tools, @@ -494,7 +494,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): self, assistant_tools: list[Any], user_tools: _ToolsType | None, - ) -> list[ToolProtocol | MutableMapping[str, Any]]: + ) -> list[FunctionTool | MutableMapping[str, Any]]: """Merge hosted tools from assistant with user-provided function tools. Args: @@ -504,7 +504,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): Returns: A list of all tools (hosted tools + user function implementations). """ - merged: list[ToolProtocol | MutableMapping[str, Any]] = [] + merged: list[FunctionTool | MutableMapping[str, Any]] = [] # Add hosted tools from assistant using shared conversion hosted_tools = from_assistant_tools(assistant_tools) @@ -520,14 +520,14 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): def _create_chat_agent_from_assistant( self, assistant: Assistant, - tools: list[ToolProtocol | MutableMapping[str, Any]] | None, + tools: list[FunctionTool | MutableMapping[str, Any]] | None, instructions: str | None, middleware: Sequence[MiddlewareTypes] | None, context_provider: ContextProvider | None, default_options: OptionsCoT | None = None, **kwargs: Any, - ) -> ChatAgent[OptionsCoT]: - """Create a ChatAgent from an Assistant. + ) -> Agent[OptionsCoT]: + """Create a Agent from an Assistant. Args: assistant: The OpenAI Assistant object. @@ -536,13 +536,13 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): middleware: MiddlewareTypes for the agent. context_provider: Context provider for the agent. default_options: Default chat options for the agent (may include response_format). - **kwargs: Additional arguments passed to ChatAgent. + **kwargs: Additional arguments passed to Agent. Returns: - A configured ChatAgent instance. + A configured Agent instance. """ # Create the chat client with the assistant - chat_client = OpenAIAssistantsClient( + client = OpenAIAssistantsClient( model_id=assistant.model, assistant_id=assistant.id, assistant_name=assistant.name, @@ -553,9 +553,9 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): # Use instructions from assistant if not overridden final_instructions = instructions if instructions is not None else assistant.instructions - # Create and return ChatAgent - return ChatAgent( - chat_client=chat_client, + # Create and return Agent + return Agent( + client=client, id=assistant.id, name=assistant.name, description=assistant.description, diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py index 914109827b..2c243fbb04 100644 --- a/python/packages/core/agent_framework/openai/_assistants_client.py +++ b/python/packages/core/agent_framework/openai/_assistants_client.py @@ -35,15 +35,13 @@ from .._tools import ( FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, - HostedCodeInterpreterTool, - HostedFileSearchTool, ) from .._types import ( - ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, Content, + Message, ResponseStream, UsageDetails, prepare_function_call_results, @@ -214,6 +212,62 @@ class OpenAIAssistantsClient( # type: ignore[misc] ): """OpenAI Assistants client with middleware, telemetry, and function invocation support.""" + # region Hosted Tool Factory Methods + + @staticmethod + def get_code_interpreter_tool() -> dict[str, Any]: + """Create a code interpreter tool configuration for the Assistants API. + + Returns: + A dict tool configuration ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.openai import OpenAIAssistantsClient + + # Enable code interpreter + tool = OpenAIAssistantsClient.get_code_interpreter_tool() + + agent = ChatAgent(client, tools=[tool]) + """ + return {"type": "code_interpreter"} + + @staticmethod + def get_file_search_tool( + *, + max_num_results: int | None = None, + ) -> dict[str, Any]: + """Create a file search tool configuration for the Assistants API. + + Keyword Args: + max_num_results: Maximum number of results to return from file search. + + Returns: + A dict tool configuration ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.openai import OpenAIAssistantsClient + + # Basic file search + tool = OpenAIAssistantsClient.get_file_search_tool() + + # With result limit + tool = OpenAIAssistantsClient.get_file_search_tool(max_num_results=10) + + agent = ChatAgent(client, tools=[tool]) + """ + tool: dict[str, Any] = {"type": "file_search"} + + if max_num_results is not None: + tool["file_search"] = {"max_num_results": max_num_results} + + return tool + + # endregion + def __init__( self, *, @@ -352,7 +406,7 @@ class OpenAIAssistantsClient( # type: ignore[misc] def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], stream: bool = False, **kwargs: Any, @@ -605,7 +659,7 @@ class OpenAIAssistantsClient( # type: ignore[misc] def _prepare_options( self, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], **kwargs: Any, ) -> tuple[dict[str, Any], list[Content] | None]: @@ -643,16 +697,8 @@ class OpenAIAssistantsClient( # type: ignore[misc] for tool in tools: if isinstance(tool, FunctionTool): tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType] - elif isinstance(tool, HostedCodeInterpreterTool): - tool_definitions.append({"type": "code_interpreter"}) - elif isinstance(tool, HostedFileSearchTool): - params: dict[str, Any] = { - "type": "file_search", - } - if tool.max_results is not None: - params["max_num_results"] = tool.max_results - tool_definitions.append(params) elif isinstance(tool, MutableMapping): + # Pass through dict-based tools directly (from static factory methods) tool_definitions.append(tool) if len(tool_definitions) > 0: diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index b3d54f251e..f2335ff6be 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -16,6 +16,7 @@ from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_chunk import ChatCompletionChunk from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice from openai.types.chat.chat_completion_message_custom_tool_call import ChatCompletionMessageCustomToolCall +from openai.types.chat.completion_create_params import WebSearchOptions from pydantic import BaseModel, ValidationError from .._clients import BaseChatClient @@ -25,16 +26,14 @@ from .._tools import ( FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, - HostedWebSearchTool, - ToolProtocol, ) from .._types import ( - ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, Content, FinishReason, + Message, ResponseStream, UsageDetails, prepare_function_call_results, @@ -154,11 +153,63 @@ class RawOpenAIChatClient( # type: ignore[misc] Use ``OpenAIChatClient`` instead for a fully-featured client with all layers applied. """ + # region Hosted Tool Factory Methods + + @staticmethod + def get_web_search_tool( + *, + web_search_options: WebSearchOptions | None = None, + ) -> dict[str, Any]: + """Create a web search tool configuration for the Chat Completions API. + + Note: For the Chat Completions API, web search is passed via the `web_search_options` + parameter rather than in the `tools` array. This method returns a dict that can be + passed as a tool to ChatAgent, which will handle it appropriately. + + Keyword Args: + web_search_options: The full WebSearchOptions configuration. This TypedDict includes: + - user_location: Location context with "type" and "approximate" containing + "city", "country", "region", "timezone". + - search_context_size: One of "low", "medium", "high". + + Returns: + A dict configuration that enables web search when passed to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.openai import OpenAIChatClient + + # Basic web search + tool = OpenAIChatClient.get_web_search_tool() + + # With location context + tool = OpenAIChatClient.get_web_search_tool( + web_search_options={ + "user_location": { + "type": "approximate", + "approximate": {"city": "Seattle", "country": "US"}, + }, + "search_context_size": "medium", + } + ) + + agent = ChatAgent(client, tools=[tool]) + """ + tool: dict[str, Any] = {"type": "web_search"} + + if web_search_options: + tool.update(web_search_options) + + return tool + + # endregion + @override def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], stream: bool = False, **kwargs: Any, @@ -222,37 +273,37 @@ class RawOpenAIChatClient( # type: ignore[misc] # region content creation - def _prepare_tools_for_openai(self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]]) -> dict[str, Any]: - chat_tools: list[dict[str, Any]] = [] + def _prepare_tools_for_openai(self, tools: Sequence[Any]) -> dict[str, Any]: + """Prepare tools for the OpenAI Chat Completions API. + + Converts FunctionTool to JSON schema format. Web search tools are routed + to web_search_options parameter. All other tools pass through unchanged. + + Args: + tools: Sequence of tools to prepare. + + Returns: + Dict containing tools and optionally web_search_options. + """ + chat_tools: list[Any] = [] web_search_options: dict[str, Any] | None = None for tool in tools: - if isinstance(tool, ToolProtocol): - match tool: - case FunctionTool(): - chat_tools.append(tool.to_json_schema_spec()) - case HostedWebSearchTool(): - web_search_options = ( - { - "user_location": { - "approximate": tool.additional_properties.get("user_location", None), - "type": "approximate", - } - } - if tool.additional_properties and "user_location" in tool.additional_properties - else {} - ) - case _: - logger.debug("Unsupported tool passed (type: %s), ignoring", type(tool)) + if isinstance(tool, FunctionTool): + chat_tools.append(tool.to_json_schema_spec()) + elif isinstance(tool, MutableMapping) and tool.get("type") == "web_search": + # Web search is handled via web_search_options, not tools array + web_search_options = {k: v for k, v in tool.items() if k != "type"} else: - chat_tools.append(tool) # type: ignore[arg-type] - ret_dict: dict[str, Any] = {} + # Pass through all other tools (dicts, SDK types) unchanged + chat_tools.append(tool) + result: dict[str, Any] = {} if chat_tools: - ret_dict["tools"] = chat_tools + result["tools"] = chat_tools if web_search_options is not None: - ret_dict["web_search_options"] = web_search_options - return ret_dict + result["web_search_options"] = web_search_options + return result - def _prepare_options(self, messages: Sequence[ChatMessage], options: Mapping[str, Any]) -> dict[str, Any]: + def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, Any]) -> dict[str, Any]: # Prepend instructions from options if they exist from .._types import prepend_instructions_to_messages, validate_tool_mode @@ -310,7 +361,7 @@ class RawOpenAIChatClient( # type: ignore[misc] def _parse_response_from_openai(self, response: ChatCompletion, options: Mapping[str, Any]) -> ChatResponse: """Parse a response from OpenAI into a ChatResponse.""" response_metadata = self._get_metadata_from_chat_response(response) - messages: list[ChatMessage] = [] + messages: list[Message] = [] finish_reason: FinishReason | None = None for choice in response.choices: response_metadata.update(self._get_metadata_from_chat_choice(choice)) @@ -323,7 +374,7 @@ class RawOpenAIChatClient( # type: ignore[misc] contents.extend(parsed_tool_calls) if reasoning_details := getattr(choice.message, "reasoning_details", None): contents.append(Content.from_text_reasoning(protected_data=json.dumps(reasoning_details))) - messages.append(ChatMessage(role="assistant", contents=contents)) + messages.append(Message(role="assistant", contents=contents)) return ChatResponse( response_id=response.id, created_at=datetime.fromtimestamp(response.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"), @@ -448,7 +499,7 @@ class RawOpenAIChatClient( # type: ignore[misc] def _prepare_messages_for_openai( self, - chat_messages: Sequence[ChatMessage], + chat_messages: Sequence[Message], role_key: str = "role", content_key: str = "content", ) -> list[dict[str, Any]]: @@ -476,7 +527,7 @@ class RawOpenAIChatClient( # type: ignore[misc] # region Parsers - def _prepare_message_for_openai(self, message: ChatMessage) -> list[dict[str, Any]]: + def _prepare_message_for_openai(self, message: Message) -> list[dict[str, Any]]: """Prepare a chat message for OpenAI.""" all_messages: list[dict[str, Any]] = [] for content in message.contents: diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index 74f835f310..5902ad0e46 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -29,8 +29,8 @@ from openai.types.responses.response_usage import ResponseUsage from openai.types.responses.tool_param import ( CodeInterpreter, CodeInterpreterContainerCodeInterpreterToolAuto, + ImageGeneration, Mcp, - ToolParam, ) from openai.types.responses.web_search_tool_param import WebSearchToolParam from pydantic import BaseModel, ValidationError @@ -42,20 +42,15 @@ from .._tools import ( FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, - HostedCodeInterpreterTool, - HostedFileSearchTool, - HostedImageGenerationTool, - HostedMCPTool, - HostedWebSearchTool, - ToolProtocol, ) from .._types import ( Annotation, - ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, Content, + ContinuationToken, + Message, ResponseStream, Role, TextSpanRegion, @@ -98,7 +93,14 @@ if TYPE_CHECKING: logger = get_logger("agent_framework.openai") -__all__ = ["OpenAIResponsesClient", "OpenAIResponsesOptions", "RawOpenAIResponsesClient"] +__all__ = ["OpenAIContinuationToken", "OpenAIResponsesClient", "OpenAIResponsesOptions", "RawOpenAIResponsesClient"] + + +class OpenAIContinuationToken(ContinuationToken): + """Continuation token for OpenAI Responses API background operations.""" + + response_id: str + """OpenAI Responses API response ID.""" # region OpenAI Responses Options TypedDict @@ -190,6 +192,17 @@ class OpenAIResponsesOptions(ChatOptions[ResponseFormatT], Generic[ResponseForma - 'auto': Truncate from beginning if exceeds context - 'disabled': Fail with 400 error if exceeds context""" + background: bool + """Whether to run the model response in the background. + When True, the response returns immediately with a continuation token + that can be used to poll for the result. + See: https://platform.openai.com/docs/guides/background""" + + continuation_token: OpenAIContinuationToken + """Token for resuming or polling a long-running background operation. + Pass the ``continuation_token`` from a previous response to poll for + completion or resume a streaming response.""" + OpenAIResponsesOptionsT = TypeVar( "OpenAIResponsesOptionsT", @@ -231,7 +244,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] async def _prepare_request( self, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], **kwargs: Any, ) -> tuple[AsyncOpenAI, dict[str, Any], dict[str, Any]]: @@ -261,38 +274,65 @@ class RawOpenAIResponsesClient( # type: ignore[misc] def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], stream: bool = False, **kwargs: Any, ) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]: + continuation_token: OpenAIContinuationToken | None = options.get("continuation_token") # type: ignore[assignment] + if stream: function_call_ids: dict[int, tuple[str, str]] = {} validated_options: dict[str, Any] | None = None async def _stream() -> AsyncIterable[ChatResponseUpdate]: nonlocal validated_options - client, run_options, validated_options = await self._prepare_request(messages, options, **kwargs) - try: - if "text_format" in run_options: - async with client.responses.stream(**run_options) as response: - async for chunk in response: - yield self._parse_chunk_from_openai( - chunk, options=validated_options, function_call_ids=function_call_ids - ) - else: - async for chunk in await client.responses.create(stream=True, **run_options): + if continuation_token is not None: + # Resume a background streaming response by retrieving with stream=True + client = await self._ensure_client() + validated_options = await self._validate_options(options) + try: + stream_response = await client.responses.retrieve( + continuation_token["response_id"], + stream=True, + ) + async for chunk in stream_response: yield self._parse_chunk_from_openai( chunk, options=validated_options, function_call_ids=function_call_ids ) - except Exception as ex: - self._handle_request_error(ex) + except Exception as ex: + self._handle_request_error(ex) + else: + client, run_options, validated_options = await self._prepare_request(messages, options, **kwargs) + try: + if "text_format" in run_options: + async with client.responses.stream(**run_options) as response: + async for chunk in response: + yield self._parse_chunk_from_openai( + chunk, options=validated_options, function_call_ids=function_call_ids + ) + else: + async for chunk in await client.responses.create(stream=True, **run_options): + yield self._parse_chunk_from_openai( + chunk, options=validated_options, function_call_ids=function_call_ids + ) + except Exception as ex: + self._handle_request_error(ex) response_format = validated_options.get("response_format") if validated_options else None return self._build_response_stream(_stream(), response_format=response_format) # Non-streaming async def _get_response() -> ChatResponse: + if continuation_token is not None: + # Poll a background response by retrieving without stream + client = await self._ensure_client() + validated_options = await self._validate_options(options) + try: + response = await client.responses.retrieve(continuation_token["response_id"]) + except Exception as ex: + self._handle_request_error(ex) + return self._parse_response_from_openai(response, options=validated_options) client, run_options, validated_options = await self._prepare_request(messages, options, **kwargs) try: if "text_format" in run_options: @@ -387,141 +427,337 @@ class RawOpenAIResponsesClient( # type: ignore[misc] # region Prep methods - def _prepare_tools_for_openai( - self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None - ) -> list[ToolParam | dict[str, Any]]: - response_tools: list[ToolParam | dict[str, Any]] = [] - if not tools: - return response_tools - for tool in tools: - if isinstance(tool, ToolProtocol): - match tool: - case HostedMCPTool(): - response_tools.append(self._prepare_mcp_tool(tool)) - case HostedCodeInterpreterTool(): - tool_args: CodeInterpreterContainerCodeInterpreterToolAuto = {"type": "auto"} - if tool.inputs: - tool_args["file_ids"] = [] - for tool_input in tool.inputs: - if tool_input.type == "hosted_file": - tool_args["file_ids"].append(tool_input.file_id) # type: ignore[attr-defined] - if not tool_args["file_ids"]: - tool_args.pop("file_ids") - response_tools.append( - CodeInterpreter( - type="code_interpreter", - container=tool_args, - ) - ) - case FunctionTool(): - params = tool.parameters() - params["additionalProperties"] = False - response_tools.append( - FunctionToolParam( - name=tool.name, - parameters=params, - strict=False, - type="function", - description=tool.description, - ) - ) - case HostedFileSearchTool(): - if not tool.inputs: - raise ValueError("HostedFileSearchTool requires inputs to be specified.") - inputs: list[str] = [ - inp.vector_store_id # type: ignore[misc] - for inp in tool.inputs - if inp.type == "hosted_vector_store" # type: ignore[attr-defined] - ] - if not inputs: - raise ValueError( - "HostedFileSearchTool requires inputs to be of type `HostedVectorStoreContent`." - ) + def _prepare_tools_for_openai(self, tools: Sequence[Any] | None) -> list[Any]: + """Prepare tools for the OpenAI Responses API. - response_tools.append( - FileSearchToolParam( - type="file_search", - vector_store_ids=inputs, - max_num_results=tool.max_results - or self.FILE_SEARCH_MAX_RESULTS, # default to max results if not specified - ) - ) - case HostedWebSearchTool(): - web_search_tool = WebSearchToolParam(type="web_search") - if location := ( - tool.additional_properties.get("user_location", None) - if tool.additional_properties - else None - ): - web_search_tool["user_location"] = { - "type": "approximate", - "city": location.get("city", None), - "country": location.get("country", None), - "region": location.get("region", None), - "timezone": location.get("timezone", None), - } - if filters := ( - tool.additional_properties.get("filters", None) if tool.additional_properties else None - ): - web_search_tool["filters"] = filters - if search_context_size := ( - tool.additional_properties.get("search_context_size", None) - if tool.additional_properties - else None - ): - web_search_tool["search_context_size"] = search_context_size - response_tools.append(web_search_tool) - case HostedImageGenerationTool(): - mapped_tool: dict[str, Any] = {"type": "image_generation"} - if tool.options: - option_mapping = { - "image_size": "size", - "media_type": "output_format", - "model_id": "model", - "streaming_count": "partial_images", - } - # count and response_format are not supported by Responses API - for key, value in tool.options.items(): - mapped_key = option_mapping.get(key, key) - mapped_tool[mapped_key] = value - if tool.additional_properties: - mapped_tool.update(tool.additional_properties) - response_tools.append(mapped_tool) - case _: - logger.debug("Unsupported tool passed (type: %s)", type(tool)) + Converts FunctionTool to Responses API format. All other tools pass through unchanged. + + Args: + tools: Sequence of tools to prepare. + + Returns: + List of tool parameters ready for the OpenAI API. + """ + if not tools: + return [] + response_tools: list[Any] = [] + for tool in tools: + if isinstance(tool, FunctionTool): + params = tool.parameters() + params["additionalProperties"] = False + response_tools.append( + FunctionToolParam( + name=tool.name, + parameters=params, + strict=False, + type="function", + description=tool.description, + ) + ) else: - # Handle raw dictionary tools - tool_dict = tool if isinstance(tool, dict) else dict(tool) - response_tools.append(tool_dict) + # Pass through all other tools (dicts, SDK types) unchanged + response_tools.append(tool) return response_tools + # region Hosted Tool Factory Methods + @staticmethod - def _prepare_mcp_tool(tool: HostedMCPTool) -> Mcp: - """Get MCP tool from HostedMCPTool.""" + def get_code_interpreter_tool( + *, + file_ids: list[str] | None = None, + container: Literal["auto"] | CodeInterpreterContainerCodeInterpreterToolAuto = "auto", + ) -> Any: + """Create a code interpreter tool configuration for the Responses API. + + Keyword Args: + file_ids: List of file IDs to make available to the code interpreter. + container: Container configuration. Use "auto" for automatic container management, + or provide a TypedDict with custom container settings. + + Returns: + A CodeInterpreter tool parameter ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.openai import OpenAIResponsesClient + + # Basic code interpreter + tool = OpenAIResponsesClient.get_code_interpreter_tool() + + # With file access + tool = OpenAIResponsesClient.get_code_interpreter_tool(file_ids=["file-abc123"]) + + # Use with agent + agent = ChatAgent(client, tools=[tool]) + """ + container_config: CodeInterpreterContainerCodeInterpreterToolAuto = ( + container if isinstance(container, dict) else {"type": "auto"} + ) + + if file_ids: + container_config["file_ids"] = file_ids + + return CodeInterpreter(type="code_interpreter", container=container_config) + + @staticmethod + def get_web_search_tool( + *, + user_location: dict[str, str] | None = None, + search_context_size: Literal["low", "medium", "high"] | None = None, + filters: dict[str, Any] | None = None, + ) -> Any: + """Create a web search tool configuration for the Responses API. + + Keyword Args: + user_location: Location context for search results. Dict with keys like + "city", "country", "region", "timezone". + search_context_size: Amount of context to include from search results. + One of "low", "medium", or "high". + filters: Additional search filters. + + Returns: + A WebSearchToolParam dict ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.openai import OpenAIResponsesClient + + # Basic web search + tool = OpenAIResponsesClient.get_web_search_tool() + + # With location context + tool = OpenAIResponsesClient.get_web_search_tool( + user_location={"city": "Seattle", "country": "US"}, + search_context_size="medium", + ) + + agent = ChatAgent(client, tools=[tool]) + """ + web_search_tool = WebSearchToolParam(type="web_search") + + if user_location: + web_search_tool["user_location"] = { + "type": "approximate", + "city": user_location.get("city"), + "country": user_location.get("country"), + "region": user_location.get("region"), + "timezone": user_location.get("timezone"), + } + + if search_context_size: + web_search_tool["search_context_size"] = search_context_size + + if filters: + web_search_tool["filters"] = filters # type: ignore[typeddict-item] + + return web_search_tool + + @staticmethod + def get_image_generation_tool( + *, + size: Literal["1024x1024", "1024x1536", "1536x1024", "auto"] | None = None, + output_format: Literal["png", "jpeg", "webp"] | None = None, + model: Literal["gpt-image-1", "gpt-image-1-mini"] | str | None = None, + quality: Literal["low", "medium", "high", "auto"] | None = None, + partial_images: int | None = None, + background: Literal["transparent", "opaque", "auto"] | None = None, + moderation: Literal["auto", "low"] | None = None, + output_compression: int | None = None, + ) -> Any: + """Create an image generation tool configuration for the Responses API. + + Keyword Args: + size: Image dimensions. One of "1024x1024", "1024x1536", "1536x1024", or "auto". + output_format: Output image format. One of "png", "jpeg", or "webp". + model: Model to use for image generation. One of "gpt-image-1" or "gpt-image-1-mini". + quality: Image quality level. One of "low", "medium", "high", or "auto". + partial_images: Number of partial images to stream during generation. + background: Background type. One of "transparent", "opaque", or "auto". + moderation: Moderation level. One of "auto" or "low". + output_compression: Compression level for output (0-100). + + Returns: + An ImageGeneration tool parameter dict ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.openai import OpenAIResponsesClient + + # Basic image generation + tool = OpenAIResponsesClient.get_image_generation_tool() + + # High quality large image + tool = OpenAIResponsesClient.get_image_generation_tool( + size="1536x1024", + quality="high", + output_format="png", + ) + + agent = ChatAgent(client, tools=[tool]) + """ + tool: ImageGeneration = {"type": "image_generation"} + + if size: + tool["size"] = size + if output_format: + tool["output_format"] = output_format + if model: + tool["model"] = model + if quality: + tool["quality"] = quality + if partial_images is not None: + tool["partial_images"] = partial_images + if background: + tool["background"] = background + if moderation: + tool["moderation"] = moderation + if output_compression is not None: + tool["output_compression"] = output_compression + + return tool + + @staticmethod + def get_mcp_tool( + *, + name: str, + url: str, + description: str | None = None, + approval_mode: Literal["always_require", "never_require"] | dict[str, list[str]] | None = None, + allowed_tools: list[str] | None = None, + headers: dict[str, str] | None = None, + ) -> Any: + """Create a hosted MCP (Model Context Protocol) tool configuration for the Responses API. + + This configures an MCP server that will be called by OpenAI's service. + The tools from this MCP server are executed remotely by OpenAI, + not locally by your application. + + Note: + For local MCP execution where your application calls the MCP server + directly, use the MCP client tools instead of this method. + + Keyword Args: + name: A label/name for the MCP server. + url: The URL of the MCP server. + description: A description of what the MCP server provides. + approval_mode: Tool approval mode. Use "always_require" or "never_require" for all tools, + or provide a dict with "always_require_approval" and/or "never_require_approval" + keys mapping to lists of tool names. + allowed_tools: List of tool names that are allowed to be used from this MCP server. + headers: HTTP headers to include in requests to the MCP server. + + Returns: + An Mcp tool parameter dict ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.openai import OpenAIResponsesClient + + # Basic MCP tool + tool = OpenAIResponsesClient.get_mcp_tool( + name="my_mcp", + url="https://mcp.example.com", + ) + + # With approval settings + tool = OpenAIResponsesClient.get_mcp_tool( + name="github_mcp", + url="https://mcp.github.com", + description="GitHub MCP server", + approval_mode="always_require", + headers={"Authorization": "Bearer token"}, + ) + + # With specific tool approvals + tool = OpenAIResponsesClient.get_mcp_tool( + name="tools_mcp", + url="https://tools.example.com", + approval_mode={ + "always_require_approval": ["dangerous_tool"], + "never_require_approval": ["safe_tool"], + }, + ) + + agent = ChatAgent(client, tools=[tool]) + """ mcp: Mcp = { "type": "mcp", - "server_label": tool.name.replace(" ", "_"), - "server_url": str(tool.url), - "server_description": tool.description, - "headers": tool.headers, + "server_label": name.replace(" ", "_"), + "server_url": url, } - if tool.allowed_tools: - mcp["allowed_tools"] = list(tool.allowed_tools) - if tool.approval_mode: - match tool.approval_mode: - case str(): - mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never" - case _: - if always_require_approvals := tool.approval_mode.get("always_require_approval"): - mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}} - if never_require_approvals := tool.approval_mode.get("never_require_approval"): - mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}} + + if description: + mcp["server_description"] = description + + if headers: + mcp["headers"] = headers + + if allowed_tools: + mcp["allowed_tools"] = allowed_tools + + if approval_mode: + if isinstance(approval_mode, str): + mcp["require_approval"] = "always" if approval_mode == "always_require" else "never" + else: + if always_require := approval_mode.get("always_require_approval"): + mcp["require_approval"] = {"always": {"tool_names": always_require}} + if never_require := approval_mode.get("never_require_approval"): + mcp["require_approval"] = {"never": {"tool_names": never_require}} return mcp + @staticmethod + def get_file_search_tool( + *, + vector_store_ids: list[str], + max_num_results: int | None = None, + ) -> Any: + """Create a file search tool configuration for the Responses API. + + Keyword Args: + vector_store_ids: List of vector store IDs to search within. + max_num_results: Maximum number of results to return. Defaults to 50 if not specified. + + Returns: + A FileSearchToolParam dict ready to pass to ChatAgent. + + Examples: + .. code-block:: python + + from agent_framework.openai import OpenAIResponsesClient + + # Basic file search + tool = OpenAIResponsesClient.get_file_search_tool( + vector_store_ids=["vs_abc123"], + ) + + # With result limit + tool = OpenAIResponsesClient.get_file_search_tool( + vector_store_ids=["vs_abc123", "vs_def456"], + max_num_results=10, + ) + + agent = ChatAgent(client, tools=[tool]) + """ + tool = FileSearchToolParam( + type="file_search", + vector_store_ids=vector_store_ids, + ) + + if max_num_results is not None: + tool["max_num_results"] = max_num_results + + return tool + + # endregion + async def _prepare_options( self, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], **kwargs: Any, ) -> dict[str, Any]: @@ -538,6 +774,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] "response_format", # handled separately "conversation_id", # handled separately "tool_choice", # handled separately + "continuation_token", # handled separately in _inner_get_response } run_options: dict[str, Any] = {k: v for k, v in options.items() if k not in exclude_keys and v is not None} @@ -626,7 +863,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] """ return kwargs.get("conversation_id") or options.get("conversation_id") - def _prepare_messages_for_openai(self, chat_messages: Sequence[ChatMessage]) -> list[dict[str, Any]]: + def _prepare_messages_for_openai(self, chat_messages: Sequence[Message]) -> list[dict[str, Any]]: """Prepare the chat messages for a request. Allowing customization of the key names for role/author, and optionally overriding the role. @@ -658,7 +895,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] def _prepare_message_for_openai( self, - message: ChatMessage, + message: Message, call_id_to_id: dict[str, str], ) -> list[dict[str, Any]]: """Prepare a chat message for the OpenAI Responses API format.""" @@ -857,7 +1094,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] for annotation in message_content.annotations: match annotation.type: case "file_path": - text_content.annotations.append( + text_content.annotations.append( # pyright: ignore[reportUnknownMemberType] Annotation( type="citation", file_id=annotation.file_id, @@ -868,7 +1105,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] ) ) case "file_citation": - text_content.annotations.append( + text_content.annotations.append( # pyright: ignore[reportUnknownMemberType] Annotation( type="citation", url=annotation.filename, @@ -880,7 +1117,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] ) ) case "url_citation": - text_content.annotations.append( + text_content.annotations.append( # pyright: ignore[reportUnknownMemberType] Annotation( type="citation", title=annotation.title, @@ -896,7 +1133,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] ) ) case "container_file_citation": - text_content.annotations.append( + text_content.annotations.append( # pyright: ignore[reportUnknownMemberType] Annotation( type="citation", file_id=annotation.file_id, @@ -1048,7 +1285,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] ) case _: logger.debug("Unparsed output of type: %s: %s", item.type, item) - response_message = ChatMessage(role="assistant", contents=contents) + response_message = Message(role="assistant", contents=contents) args: dict[str, Any] = { "response_id": response.id, "created_at": datetime.fromtimestamp(response.created_at, tz=timezone.utc).strftime( @@ -1060,7 +1297,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] "raw_representation": response, } - if conversation_id := self._get_conversation_id(response, options.get("store")): + if conversation_id := self._get_conversation_id(response, options.get("store")): # pyright: ignore[reportUnknownArgumentType] args["conversation_id"] = conversation_id if response.usage and (usage_details := self._parse_usage_from_openai(response.usage)): args["usage_details"] = usage_details @@ -1070,6 +1307,9 @@ class RawOpenAIResponsesClient( # type: ignore[misc] # Only pass response_format to ChatResponse if it's a Pydantic model type, # not a runtime JSON schema dict args["response_format"] = response_format + # Set continuation_token when background operation is still in progress + if response.status and response.status in ("in_progress", "queued"): + args["continuation_token"] = OpenAIContinuationToken(response_id=response.id) return ChatResponse(**args) def _parse_chunk_from_openai( @@ -1083,6 +1323,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] contents: list[Content] = [] conversation_id: str | None = None response_id: str | None = None + continuation_token: OpenAIContinuationToken | None = None model = self.model_id match event.type: # types: @@ -1211,9 +1452,12 @@ class RawOpenAIResponsesClient( # type: ignore[misc] case "response.created": response_id = event.response.id conversation_id = self._get_conversation_id(event.response, options.get("store")) + if event.response.status and event.response.status in ("in_progress", "queued"): + continuation_token = OpenAIContinuationToken(response_id=event.response.id) case "response.in_progress": response_id = event.response.id conversation_id = self._get_conversation_id(event.response, options.get("store")) + continuation_token = OpenAIContinuationToken(response_id=event.response.id) case "response.completed": response_id = event.response.id conversation_id = self._get_conversation_id(event.response, options.get("store")) @@ -1275,13 +1519,13 @@ class RawOpenAIResponsesClient( # type: ignore[misc] ) parsed_output: list[Content] | None = None if result_output: - normalized = ( + normalized = ( # pyright: ignore[reportUnknownVariableType] result_output if isinstance(result_output, Sequence) and not isinstance(result_output, (str, bytes, MutableMapping)) else [result_output] ) - parsed_output = [Content.from_dict(output_item) for output_item in normalized] + parsed_output = [Content.from_dict(output_item) for output_item in normalized] # pyright: ignore[reportArgumentType,reportUnknownVariableType] contents.append( Content.from_mcp_server_tool_result( call_id=call_id, @@ -1454,6 +1698,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] response_id=response_id, role="assistant", model_id=model, + continuation_token=continuation_token, additional_properties=metadata, raw_representation=event, ) diff --git a/python/packages/core/agent_framework/openai/_shared.py b/python/packages/core/agent_framework/openai/_shared.py index dbf0d9f6f6..008bd6ac12 100644 --- a/python/packages/core/agent_framework/openai/_shared.py +++ b/python/packages/core/agent_framework/openai/_shared.py @@ -26,7 +26,7 @@ from .._logging import get_logger from .._pydantic import AFBaseSettings from .._serialization import SerializationMixin from .._telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent -from .._tools import FunctionTool, HostedCodeInterpreterTool, HostedFileSearchTool, ToolProtocol +from .._tools import FunctionTool from ..exceptions import ServiceInitializationError logger: logging.Logger = get_logger("agent_framework.openai") @@ -284,12 +284,14 @@ class OpenAIConfigMixin(OpenAIBase): def to_assistant_tools( - tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None, + tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None, ) -> list[dict[str, Any]]: """Convert Agent Framework tools to OpenAI Assistants API format. + Handles FunctionTool instances and dict-based tools from static factory methods. + Args: - tools: Normalized tools (from ChatOptions.tools). + tools: Sequence of Agent Framework tools. Returns: List of tool definitions for OpenAI Assistants API. @@ -302,15 +304,8 @@ def to_assistant_tools( for tool in tools: if isinstance(tool, FunctionTool): tool_definitions.append(tool.to_json_schema_spec()) - elif isinstance(tool, HostedCodeInterpreterTool): - tool_definitions.append({"type": "code_interpreter"}) - elif isinstance(tool, HostedFileSearchTool): - params: dict[str, Any] = {"type": "file_search"} - if tool.max_results is not None: - params["file_search"] = {"max_num_results": tool.max_results} - tool_definitions.append(params) elif isinstance(tool, MutableMapping): - # Pass through raw dict definitions + # Pass through dict-based tools directly (from static factory methods) tool_definitions.append(dict(tool)) return tool_definitions @@ -318,11 +313,11 @@ def to_assistant_tools( def from_assistant_tools( assistant_tools: list[Any] | None, -) -> list[ToolProtocol]: - """Convert OpenAI Assistant tools to Agent Framework format. +) -> list[dict[str, Any]]: + """Convert OpenAI Assistant tools to dict-based format. This converts hosted tools (code_interpreter, file_search) from an OpenAI - Assistant definition back to Agent Framework tool instances. + Assistant definition back to dict-based tool definitions. Note: Function tools are skipped - user must provide implementations separately. @@ -330,12 +325,12 @@ def from_assistant_tools( assistant_tools: Tools from OpenAI Assistant object (assistant.tools). Returns: - List of Agent Framework tool instances for hosted tools. + List of dict-based tool definitions for hosted tools. """ if not assistant_tools: return [] - tools: list[ToolProtocol] = [] + tools: list[dict[str, Any]] = [] for tool in assistant_tools: if hasattr(tool, "type"): @@ -346,9 +341,9 @@ def from_assistant_tools( tool_type = None if tool_type == "code_interpreter": - tools.append(HostedCodeInterpreterTool()) + tools.append({"type": "code_interpreter"}) elif tool_type == "file_search": - tools.append(HostedFileSearchTool()) + tools.append({"type": "file_search"}) # Skip function tools - user must provide implementations return tools diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml index 726c1cdcb4..f4f28c898a 100644 --- a/python/packages/core/pyproject.toml +++ b/python/packages/core/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/core/tests/azure/conftest.py b/python/packages/core/tests/azure/conftest.py index a9c03cd664..9d8ce0cebb 100644 --- a/python/packages/core/tests/azure/conftest.py +++ b/python/packages/core/tests/azure/conftest.py @@ -3,7 +3,7 @@ from typing import Any from pytest import fixture -from agent_framework import ChatMessage +from agent_framework import Message # region: Connector Settings fixtures @@ -58,5 +58,5 @@ def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dic @fixture(scope="function") -def chat_history() -> list[ChatMessage]: +def chat_history() -> list[Message]: return [] diff --git a/python/packages/core/tests/azure/test_azure_assistants_client.py b/python/packages/core/tests/azure/test_azure_assistants_client.py index 9c95bed1c1..bd940b13e5 100644 --- a/python/packages/core/tests/azure/test_azure_assistants_client.py +++ b/python/packages/core/tests/azure/test_azure_assistants_client.py @@ -9,15 +9,14 @@ from azure.identity import AzureCliCredential from pydantic import Field from agent_framework import ( + Agent, AgentResponse, AgentResponseUpdate, AgentThread, - ChatAgent, - ChatClientProtocol, - ChatMessage, ChatResponse, ChatResponseUpdate, - HostedCodeInterpreterTool, + Message, + SupportsChatGetResponse, tool, ) from agent_framework.azure import AzureOpenAIAssistantsClient @@ -83,19 +82,19 @@ def mock_async_azure_openai() -> MagicMock: def test_azure_assistants_client_init_with_client(mock_async_azure_openai: MagicMock) -> None: """Test AzureOpenAIAssistantsClient initialization with existing client.""" - chat_client = create_test_azure_assistants_client( + client = create_test_azure_assistants_client( mock_async_azure_openai, deployment_name="test_chat_deployment", assistant_id="existing-assistant-id", thread_id="test-thread-id", ) - assert chat_client.client is mock_async_azure_openai - assert chat_client.model_id == "test_chat_deployment" - assert chat_client.assistant_id == "existing-assistant-id" - assert chat_client.thread_id == "test-thread-id" - assert not chat_client._should_delete_assistant # type: ignore - assert isinstance(chat_client, ChatClientProtocol) + assert client.client is mock_async_azure_openai + assert client.model_id == "test_chat_deployment" + assert client.assistant_id == "existing-assistant-id" + assert client.thread_id == "test-thread-id" + assert not client._should_delete_assistant # type: ignore + assert isinstance(client, SupportsChatGetResponse) def test_azure_assistants_client_init_auto_create_client( @@ -103,7 +102,7 @@ def test_azure_assistants_client_init_auto_create_client( mock_async_azure_openai: MagicMock, ) -> None: """Test AzureOpenAIAssistantsClient initialization with auto-created client.""" - chat_client = AzureOpenAIAssistantsClient( + client = AzureOpenAIAssistantsClient( deployment_name=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], assistant_name="TestAssistant", api_key=azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"], @@ -111,11 +110,11 @@ def test_azure_assistants_client_init_auto_create_client( async_client=mock_async_azure_openai, ) - assert chat_client.client is mock_async_azure_openai - assert chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] - assert chat_client.assistant_id is None - assert chat_client.assistant_name == "TestAssistant" - assert not chat_client._should_delete_assistant # type: ignore + assert client.client is mock_async_azure_openai + assert client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + assert client.assistant_id is None + assert client.assistant_name == "TestAssistant" + assert not client._should_delete_assistant # type: ignore def test_azure_assistants_client_init_validation_fail() -> None: @@ -138,32 +137,32 @@ def test_azure_assistants_client_init_with_default_headers(azure_openai_unit_tes """Test AzureOpenAIAssistantsClient initialization with default headers.""" default_headers = {"X-Unit-Test": "test-guid"} - chat_client = AzureOpenAIAssistantsClient( + client = AzureOpenAIAssistantsClient( deployment_name="test_chat_deployment", api_key=azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"], endpoint=azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"], default_headers=default_headers, ) - assert chat_client.model_id == "test_chat_deployment" - assert isinstance(chat_client, ChatClientProtocol) + assert client.model_id == "test_chat_deployment" + assert isinstance(client, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers for key, value in default_headers.items(): - assert key in chat_client.client.default_headers - assert chat_client.client.default_headers[key] == value + assert key in client.client.default_headers + assert client.client.default_headers[key] == value async def test_azure_assistants_client_get_assistant_id_or_create_existing_assistant( mock_async_azure_openai: MagicMock, ) -> None: """Test _get_assistant_id_or_create when assistant_id is already provided.""" - chat_client = create_test_azure_assistants_client(mock_async_azure_openai, assistant_id="existing-assistant-id") + client = create_test_azure_assistants_client(mock_async_azure_openai, assistant_id="existing-assistant-id") - assistant_id = await chat_client._get_assistant_id_or_create() # type: ignore + assistant_id = await client._get_assistant_id_or_create() # type: ignore assert assistant_id == "existing-assistant-id" - assert not chat_client._should_delete_assistant # type: ignore + assert not client._should_delete_assistant # type: ignore mock_async_azure_openai.beta.assistants.create.assert_not_called() @@ -171,14 +170,14 @@ async def test_azure_assistants_client_get_assistant_id_or_create_create_new( mock_async_azure_openai: MagicMock, ) -> None: """Test _get_assistant_id_or_create when creating a new assistant.""" - chat_client = create_test_azure_assistants_client( + client = create_test_azure_assistants_client( mock_async_azure_openai, deployment_name="test_chat_deployment", assistant_name="TestAssistant" ) - assistant_id = await chat_client._get_assistant_id_or_create() # type: ignore + assistant_id = await client._get_assistant_id_or_create() # type: ignore assert assistant_id == "test-assistant-id" - assert chat_client._should_delete_assistant # type: ignore + assert client._should_delete_assistant # type: ignore mock_async_azure_openai.beta.assistants.create.assert_called_once() @@ -186,38 +185,38 @@ async def test_azure_assistants_client_aclose_should_not_delete( mock_async_azure_openai: MagicMock, ) -> None: """Test close when assistant should not be deleted.""" - chat_client = create_test_azure_assistants_client( + client = create_test_azure_assistants_client( mock_async_azure_openai, assistant_id="assistant-to-keep", should_delete_assistant=False ) - await chat_client.close() # type: ignore + await client.close() # type: ignore # Verify assistant deletion was not called mock_async_azure_openai.beta.assistants.delete.assert_not_called() - assert not chat_client._should_delete_assistant # type: ignore + assert not client._should_delete_assistant # type: ignore async def test_azure_assistants_client_aclose_should_delete(mock_async_azure_openai: MagicMock) -> None: """Test close method calls cleanup.""" - chat_client = create_test_azure_assistants_client( + client = create_test_azure_assistants_client( mock_async_azure_openai, assistant_id="assistant-to-delete", should_delete_assistant=True ) - await chat_client.close() + await client.close() # Verify assistant deletion was called mock_async_azure_openai.beta.assistants.delete.assert_called_once_with("assistant-to-delete") - assert not chat_client._should_delete_assistant # type: ignore + assert not client._should_delete_assistant # type: ignore async def test_azure_assistants_client_async_context_manager(mock_async_azure_openai: MagicMock) -> None: """Test async context manager functionality.""" - chat_client = create_test_azure_assistants_client( + client = create_test_azure_assistants_client( mock_async_azure_openai, assistant_id="assistant-to-delete", should_delete_assistant=True ) # Test context manager - async with chat_client: + async with client: pass # Just test that we can enter and exit # Verify cleanup was called on exit @@ -229,7 +228,7 @@ def test_azure_assistants_client_serialize(azure_openai_unit_test_env: dict[str, default_headers = {"X-Unit-Test": "test-guid"} # Test basic initialization and to_dict - chat_client = AzureOpenAIAssistantsClient( + client = AzureOpenAIAssistantsClient( deployment_name="test_chat_deployment", assistant_id="test-assistant-id", assistant_name="TestAssistant", @@ -239,7 +238,7 @@ def test_azure_assistants_client_serialize(azure_openai_unit_test_env: dict[str, default_headers=default_headers, ) - dumped_settings = chat_client.to_dict() + dumped_settings = client.to_dict() assert dumped_settings["model_id"] == "test_chat_deployment" assert dumped_settings["assistant_id"] == "test-assistant-id" @@ -267,17 +266,17 @@ def get_weather( async def test_azure_assistants_client_get_response() -> None: """Test Azure Assistants Client response.""" async with AzureOpenAIAssistantsClient(credential=AzureCliCredential()) as azure_assistants_client: - assert isinstance(azure_assistants_client, ChatClientProtocol) + assert isinstance(azure_assistants_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] + messages: list[Message] = [] messages.append( - ChatMessage( + Message( role="user", text="The weather in Seattle is currently sunny with a high of 25°C. " "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(Message(role="user", text="What's the weather like today?")) # Test that the client can be used to get a response response = await azure_assistants_client.get_response(messages=messages) @@ -292,10 +291,10 @@ async def test_azure_assistants_client_get_response() -> None: async def test_azure_assistants_client_get_response_tools() -> None: """Test Azure Assistants Client response with tools.""" async with AzureOpenAIAssistantsClient(credential=AzureCliCredential()) as azure_assistants_client: - assert isinstance(azure_assistants_client, ChatClientProtocol) + assert isinstance(azure_assistants_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages: list[Message] = [] + messages.append(Message(role="user", text="What's the weather like in Seattle?")) # Test that the client can be used to get a response response = await azure_assistants_client.get_response( @@ -313,17 +312,17 @@ async def test_azure_assistants_client_get_response_tools() -> None: async def test_azure_assistants_client_streaming() -> None: """Test Azure Assistants Client streaming response.""" async with AzureOpenAIAssistantsClient(credential=AzureCliCredential()) as azure_assistants_client: - assert isinstance(azure_assistants_client, ChatClientProtocol) + assert isinstance(azure_assistants_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] + messages: list[Message] = [] messages.append( - ChatMessage( + Message( role="user", text="The weather in Seattle is currently sunny with a high of 25°C. " "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(Message(role="user", text="What's the weather like today?")) # Test that the client can be used to get a response response = azure_assistants_client.get_response(messages=messages, stream=True) @@ -344,10 +343,10 @@ async def test_azure_assistants_client_streaming() -> None: async def test_azure_assistants_client_streaming_tools() -> None: """Test Azure Assistants Client streaming response with tools.""" async with AzureOpenAIAssistantsClient(credential=AzureCliCredential()) as azure_assistants_client: - assert isinstance(azure_assistants_client, ChatClientProtocol) + assert isinstance(azure_assistants_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages: list[Message] = [] + messages.append(Message(role="user", text="What's the weather like in Seattle?")) # Test that the client can be used to get a response response = azure_assistants_client.get_response( @@ -373,7 +372,7 @@ async def test_azure_assistants_client_with_existing_assistant() -> None: # First create an assistant to use in the test async with AzureOpenAIAssistantsClient(credential=AzureCliCredential()) as temp_client: # Get the assistant ID by triggering assistant creation - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] await temp_client.get_response(messages=messages) assistant_id = temp_client.assistant_id @@ -381,10 +380,10 @@ async def test_azure_assistants_client_with_existing_assistant() -> None: async with AzureOpenAIAssistantsClient( assistant_id=assistant_id, credential=AzureCliCredential() ) as azure_assistants_client: - assert isinstance(azure_assistants_client, ChatClientProtocol) + assert isinstance(azure_assistants_client, SupportsChatGetResponse) assert azure_assistants_client.assistant_id == assistant_id - messages = [ChatMessage(role="user", text="What can you do?")] + messages = [Message(role="user", text="What can you do?")] # Test that the client can be used to get a response response = await azure_assistants_client.get_response(messages=messages) @@ -397,9 +396,9 @@ async def test_azure_assistants_client_with_existing_assistant() -> None: @pytest.mark.flaky @skip_if_azure_integration_tests_disabled async def test_azure_assistants_agent_basic_run(): - """Test ChatAgent basic run functionality with AzureOpenAIAssistantsClient.""" - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + """Test Agent basic run functionality with AzureOpenAIAssistantsClient.""" + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), ) as agent: # Run a simple query response = await agent.run("Hello! Please respond with 'Hello World' exactly.") @@ -414,9 +413,9 @@ async def test_azure_assistants_agent_basic_run(): @pytest.mark.flaky @skip_if_azure_integration_tests_disabled async def test_azure_assistants_agent_basic_run_streaming(): - """Test ChatAgent basic streaming functionality with AzureOpenAIAssistantsClient.""" - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + """Test Agent basic streaming functionality with AzureOpenAIAssistantsClient.""" + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), ) as agent: # Run streaming query full_message: str = "" @@ -434,9 +433,9 @@ async def test_azure_assistants_agent_basic_run_streaming(): @pytest.mark.flaky @skip_if_azure_integration_tests_disabled async def test_azure_assistants_agent_thread_persistence(): - """Test ChatAgent thread persistence across runs with AzureOpenAIAssistantsClient.""" - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + """Test Agent thread persistence across runs with AzureOpenAIAssistantsClient.""" + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), instructions="You are a helpful assistant with good memory.", ) as agent: # Create a new thread that will be reused @@ -463,12 +462,12 @@ async def test_azure_assistants_agent_thread_persistence(): @pytest.mark.flaky @skip_if_azure_integration_tests_disabled async def test_azure_assistants_agent_existing_thread_id(): - """Test ChatAgent with existing thread ID to continue conversations across agent instances.""" + """Test Agent with existing thread ID to continue conversations across agent instances.""" # First, create a conversation and capture the thread ID existing_thread_id = None - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=[get_weather], ) as agent: @@ -487,8 +486,8 @@ async def test_azure_assistants_agent_existing_thread_id(): # Now continue with the same thread ID in a new agent instance - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(thread_id=existing_thread_id, credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(thread_id=existing_thread_id, credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=[get_weather], ) as agent: @@ -508,12 +507,12 @@ async def test_azure_assistants_agent_existing_thread_id(): @pytest.mark.flaky @skip_if_azure_integration_tests_disabled async def test_azure_assistants_agent_code_interpreter(): - """Test ChatAgent with code interpreter through AzureOpenAIAssistantsClient.""" + """Test Agent with code interpreter through AzureOpenAIAssistantsClient.""" - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that can write and execute Python code.", - tools=[HostedCodeInterpreterTool()], + tools=[AzureOpenAIAssistantsClient.get_code_interpreter_tool()], ) as agent: # Request code execution response = await agent.run("Write Python code to calculate the factorial of 5 and show the result.") @@ -530,8 +529,8 @@ async def test_azure_assistants_agent_code_interpreter(): async def test_azure_assistants_client_agent_level_tool_persistence(): """Test that agent-level tools persist across multiple runs with Azure Assistants Client.""" - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that uses available tools.", tools=[get_weather], # Agent-level tool ) as agent: diff --git a/python/packages/core/tests/azure/test_azure_chat_client.py b/python/packages/core/tests/azure/test_azure_chat_client.py index f434b55fd1..f0b34cc13d 100644 --- a/python/packages/core/tests/azure/test_azure_chat_client.py +++ b/python/packages/core/tests/azure/test_azure_chat_client.py @@ -17,13 +17,13 @@ from openai.types.chat.chat_completion_chunk import ChoiceDelta as ChunkChoiceDe from openai.types.chat.chat_completion_message import ChatCompletionMessage from agent_framework import ( + Agent, AgentResponse, AgentResponseUpdate, - ChatAgent, - ChatClientProtocol, - ChatMessage, ChatResponse, ChatResponseUpdate, + Message, + SupportsChatGetResponse, tool, ) from agent_framework._telemetry import USER_AGENT_KEY @@ -52,7 +52,7 @@ def test_init(azure_openai_unit_test_env: dict[str, str]) -> None: assert azure_chat_client.client is not None assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] - assert isinstance(azure_chat_client, ChatClientProtocol) + assert isinstance(azure_chat_client, SupportsChatGetResponse) def test_init_client(azure_openai_unit_test_env: dict[str, str]) -> None: @@ -75,7 +75,7 @@ def test_init_base_url(azure_openai_unit_test_env: dict[str, str]) -> None: assert azure_chat_client.client is not None assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] - assert isinstance(azure_chat_client, ChatClientProtocol) + assert isinstance(azure_chat_client, SupportsChatGetResponse) for key, value in default_headers.items(): assert key in azure_chat_client.client.default_headers assert azure_chat_client.client.default_headers[key] == value @@ -88,7 +88,7 @@ def test_init_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None: assert azure_chat_client.client is not None assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] - assert isinstance(azure_chat_client, ChatClientProtocol) + assert isinstance(azure_chat_client, SupportsChatGetResponse) @pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]], indirect=True) @@ -178,11 +178,11 @@ def mock_streaming_chat_completion_response() -> AsyncStream[ChatCompletionChunk async def test_cmc( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, ) -> None: mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(text="hello world", role="user")) + chat_history.append(Message(text="hello world", role="user")) azure_chat_client = AzureOpenAIChatClient() await azure_chat_client.get_response( @@ -199,12 +199,12 @@ async def test_cmc( async def test_cmc_with_logit_bias( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, ) -> None: mock_create.return_value = mock_chat_completion_response prompt = "hello world" - chat_history.append(ChatMessage(text=prompt, role="user")) + chat_history.append(Message(text=prompt, role="user")) token_bias: dict[str | int, float] = {"1": -100} @@ -224,12 +224,12 @@ async def test_cmc_with_logit_bias( async def test_cmc_with_stop( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, ) -> None: mock_create.return_value = mock_chat_completion_response prompt = "hello world" - chat_history.append(ChatMessage(text=prompt, role="user")) + chat_history.append(Message(text=prompt, role="user")) stop = ["!"] @@ -249,7 +249,7 @@ async def test_cmc_with_stop( async def test_azure_on_your_data( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, ) -> None: mock_chat_completion_response.choices = [ @@ -277,9 +277,9 @@ async def test_azure_on_your_data( mock_create.return_value = mock_chat_completion_response prompt = "hello world" messages_in = chat_history - chat_history.append(ChatMessage(text=prompt, role="user")) - messages_out: list[ChatMessage] = [] - messages_out.append(ChatMessage(text=prompt, role="user")) + chat_history.append(Message(text=prompt, role="user")) + messages_out: list[Message] = [] + messages_out.append(Message(text=prompt, role="user")) expected_data_settings = { "data_sources": [ @@ -319,7 +319,7 @@ async def test_azure_on_your_data( async def test_azure_on_your_data_string( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, ) -> None: mock_chat_completion_response.choices = [ @@ -347,9 +347,9 @@ async def test_azure_on_your_data_string( mock_create.return_value = mock_chat_completion_response prompt = "hello world" messages_in = chat_history - messages_in.append(ChatMessage(text=prompt, role="user")) - messages_out: list[ChatMessage] = [] - messages_out.append(ChatMessage(text=prompt, role="user")) + messages_in.append(Message(text=prompt, role="user")) + messages_out: list[Message] = [] + messages_out.append(Message(text=prompt, role="user")) expected_data_settings = { "data_sources": [ @@ -389,7 +389,7 @@ async def test_azure_on_your_data_string( async def test_azure_on_your_data_fail( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, ) -> None: mock_chat_completion_response.choices = [ @@ -406,9 +406,9 @@ async def test_azure_on_your_data_fail( mock_create.return_value = mock_chat_completion_response prompt = "hello world" messages_in = chat_history - messages_in.append(ChatMessage(text=prompt, role="user")) - messages_out: list[ChatMessage] = [] - messages_out.append(ChatMessage(text=prompt, role="user")) + messages_in.append(Message(text=prompt, role="user")) + messages_out: list[Message] = [] + messages_out.append(Message(text=prompt, role="user")) expected_data_settings = { "data_sources": [ @@ -459,10 +459,10 @@ CONTENT_FILTERED_ERROR_FULL_MESSAGE = ( async def test_content_filtering_raises_correct_exception( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], ) -> None: prompt = "some prompt that would trigger the content filtering" - chat_history.append(ChatMessage(text=prompt, role="user")) + chat_history.append(Message(text=prompt, role="user")) test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") assert test_endpoint is not None @@ -504,10 +504,10 @@ async def test_content_filtering_raises_correct_exception( async def test_content_filtering_without_response_code_raises_with_default_code( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], ) -> None: prompt = "some prompt that would trigger the content filtering" - chat_history.append(ChatMessage(text=prompt, role="user")) + chat_history.append(Message(text=prompt, role="user")) test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") assert test_endpoint is not None @@ -543,10 +543,10 @@ async def test_content_filtering_without_response_code_raises_with_default_code( async def test_bad_request_non_content_filter( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], ) -> None: prompt = "some prompt that would trigger the content filtering" - chat_history.append(ChatMessage(text=prompt, role="user")) + chat_history.append(Message(text=prompt, role="user")) test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") assert test_endpoint is not None @@ -566,11 +566,11 @@ async def test_bad_request_non_content_filter( async def test_get_streaming( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_streaming_chat_completion_response: AsyncStream[ChatCompletionChunk], ) -> None: mock_create.return_value = mock_streaming_chat_completion_response - chat_history.append(ChatMessage(text="hello world", role="user")) + chat_history.append(Message(text="hello world", role="user")) azure_chat_client = AzureOpenAIChatClient() async for msg in azure_chat_client.get_response( @@ -595,7 +595,7 @@ async def test_get_streaming( async def test_streaming_with_none_delta( mock_create: AsyncMock, azure_openai_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], ) -> None: """Test streaming handles None delta from async content filtering.""" # First chunk has None delta (simulates async filtering) @@ -619,7 +619,7 @@ async def test_streaming_with_none_delta( stream.__aiter__.return_value = [chunk_with_none_delta, chunk_with_content] mock_create.return_value = stream - chat_history.append(ChatMessage(text="hello world", role="user")) + chat_history.append(Message(text="hello world", role="user")) azure_chat_client = AzureOpenAIChatClient() results: list[ChatResponseUpdate] = [] @@ -653,11 +653,11 @@ def get_weather(location: str) -> str: async def test_azure_openai_chat_client_response() -> None: """Test Azure OpenAI chat completion responses.""" azure_chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - assert isinstance(azure_chat_client, ChatClientProtocol) + assert isinstance(azure_chat_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] + messages: list[Message] = [] messages.append( - ChatMessage( + Message( role="user", text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. " "Bonded by their love for the natural world and shared curiosity, they uncovered a " @@ -665,7 +665,7 @@ async def test_azure_openai_chat_client_response() -> None: "of climate change.", ) ) - messages.append(ChatMessage(role="user", text="who are Emily and David?")) + messages.append(Message(role="user", text="who are Emily and David?")) # Test that the client can be used to get a response response = await azure_chat_client.get_response(messages=messages) @@ -683,10 +683,10 @@ async def test_azure_openai_chat_client_response() -> None: async def test_azure_openai_chat_client_response_tools() -> None: """Test AzureOpenAI chat completion responses.""" azure_chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - assert isinstance(azure_chat_client, ChatClientProtocol) + assert isinstance(azure_chat_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="who are Emily and David?")) + messages: list[Message] = [] + messages.append(Message(role="user", text="who are Emily and David?")) # Test that the client can be used to get a response response = await azure_chat_client.get_response( @@ -704,11 +704,11 @@ async def test_azure_openai_chat_client_response_tools() -> None: async def test_azure_openai_chat_client_streaming() -> None: """Test Azure OpenAI chat completion responses.""" azure_chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - assert isinstance(azure_chat_client, ChatClientProtocol) + assert isinstance(azure_chat_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] + messages: list[Message] = [] messages.append( - ChatMessage( + Message( role="user", text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. " "Bonded by their love for the natural world and shared curiosity, they uncovered a " @@ -716,7 +716,7 @@ async def test_azure_openai_chat_client_streaming() -> None: "of climate change.", ) ) - messages.append(ChatMessage(role="user", text="who are Emily and David?")) + messages.append(Message(role="user", text="who are Emily and David?")) # Test that the client can be used to get a response response = azure_chat_client.get_response(messages=messages, stream=True) @@ -739,10 +739,10 @@ async def test_azure_openai_chat_client_streaming() -> None: async def test_azure_openai_chat_client_streaming_tools() -> None: """Test AzureOpenAI chat completion responses.""" azure_chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - assert isinstance(azure_chat_client, ChatClientProtocol) + assert isinstance(azure_chat_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="who are Emily and David?")) + messages: list[Message] = [] + messages.append(Message(role="user", text="who are Emily and David?")) # Test that the client can be used to get a response response = azure_chat_client.get_response( @@ -765,8 +765,8 @@ async def test_azure_openai_chat_client_streaming_tools() -> None: @skip_if_azure_integration_tests_disabled async def test_azure_openai_chat_client_agent_basic_run(): """Test Azure OpenAI chat client agent basic run functionality with AzureOpenAIChatClient.""" - async with ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), ) as agent: # Test basic run response = await agent.run("Please respond with exactly: 'This is a response test.'") @@ -781,8 +781,8 @@ async def test_azure_openai_chat_client_agent_basic_run(): @skip_if_azure_integration_tests_disabled async def test_azure_openai_chat_client_agent_basic_run_streaming(): """Test Azure OpenAI chat client agent basic streaming functionality with AzureOpenAIChatClient.""" - async with ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), ) as agent: # Test streaming run full_text = "" @@ -799,8 +799,8 @@ async def test_azure_openai_chat_client_agent_basic_run_streaming(): @skip_if_azure_integration_tests_disabled async def test_azure_openai_chat_client_agent_thread_persistence(): """Test Azure OpenAI chat client agent thread persistence across runs with AzureOpenAIChatClient.""" - async with ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant with good memory.", ) as agent: # Create a new thread that will be reused @@ -827,8 +827,8 @@ async def test_azure_openai_chat_client_agent_existing_thread(): # First conversation - capture the thread preserved_thread = None - async with ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant with good memory.", ) as first_agent: # Start a conversation and capture the thread @@ -843,8 +843,8 @@ async def test_azure_openai_chat_client_agent_existing_thread(): # Second conversation - reuse the thread in a new agent instance if preserved_thread: - async with ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant with good memory.", ) as second_agent: # Reuse the preserved thread @@ -860,8 +860,8 @@ async def test_azure_openai_chat_client_agent_existing_thread(): async def test_azure_chat_client_agent_level_tool_persistence(): """Test that agent-level tools persist across multiple runs with Azure Chat Client.""" - async with ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that uses available tools.", tools=[get_weather], # Agent-level tool ) as agent: diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/core/tests/azure/test_azure_responses_client.py index e8e9e9e089..434674d50c 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/core/tests/azure/test_azure_responses_client.py @@ -10,16 +10,12 @@ from pydantic import BaseModel from pytest import param from agent_framework import ( + Agent, AgentResponse, - ChatAgent, - ChatClientProtocol, - ChatMessage, ChatResponse, Content, - HostedCodeInterpreterTool, - HostedFileSearchTool, - HostedMCPTool, - HostedWebSearchTool, + Message, + SupportsChatGetResponse, tool, ) from agent_framework.azure import AzureOpenAIResponsesClient @@ -76,7 +72,7 @@ def test_init(azure_openai_unit_test_env: dict[str, str]) -> None: azure_responses_client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) assert azure_responses_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] - assert isinstance(azure_responses_client, ChatClientProtocol) + assert isinstance(azure_responses_client, SupportsChatGetResponse) def test_init_validation_fail() -> None: @@ -91,7 +87,7 @@ def test_init_model_id_constructor(azure_openai_unit_test_env: dict[str, str]) - azure_responses_client = AzureOpenAIResponsesClient(deployment_name=model_id) assert azure_responses_client.model_id == model_id - assert isinstance(azure_responses_client, ChatClientProtocol) + assert isinstance(azure_responses_client, SupportsChatGetResponse) def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) -> None: @@ -103,7 +99,7 @@ def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) -> ) assert azure_responses_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] - assert isinstance(azure_responses_client, ChatClientProtocol) + assert isinstance(azure_responses_client, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers for key, value in default_headers.items(): @@ -221,14 +217,14 @@ async def test_integration_options( # Prepare test message if option_name == "tools" or option_name == "tool_choice": # Use weather-related prompt for tool tests - messages = [ChatMessage(role="user", text="What is the weather in Seattle?")] + messages = [Message(role="user", text="What is the weather in Seattle?")] elif option_name == "response_format": # Use prompt that works well with structured output - messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")] - messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) + messages = [Message(role="user", text="The weather in Seattle is sunny")] + messages.append(Message(role="user", text="What is the weather in Seattle?")) else: # Generic prompt for simple options - messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")] + messages = [Message(role="user", text="Say 'Hello World' briefly.")] # Build options dict options: dict[str, Any] = {option_name: option_value} @@ -289,7 +285,7 @@ async def test_integration_web_search() -> None: "messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.", "options": { "tool_choice": "auto", - "tools": [HostedWebSearchTool()], + "tools": [AzureOpenAIResponsesClient.get_web_search_tool()], }, "stream": streaming, } @@ -305,17 +301,13 @@ async def test_integration_web_search() -> None: assert "Zoey" in response.text # Test that the client will use the web search tool with location - additional_properties = { - "user_location": { - "country": "US", - "city": "Seattle", - } - } content = { "messages": "What is the current weather? Do not ask for my current location.", "options": { "tool_choice": "auto", - "tools": [HostedWebSearchTool(additional_properties=additional_properties)], + "tools": [ + AzureOpenAIResponsesClient.get_web_search_tool(user_location={"country": "US", "city": "Seattle"}) + ], }, "stream": streaming, } @@ -336,12 +328,17 @@ async def test_integration_client_file_search() -> None: # Test that the client will use the file search tool response = await azure_responses_client.get_response( messages=[ - ChatMessage( + Message( role="user", text="What is the weather today? Do a file search to find the answer.", ) ], - options={"tools": [HostedFileSearchTool(inputs=vector_store)], "tool_choice": "auto"}, + options={ + "tools": [ + AzureOpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) + ], + "tool_choice": "auto", + }, ) assert "sunny" in response.text.lower() @@ -360,13 +357,18 @@ async def test_integration_client_file_search_streaming() -> None: try: response_stream = azure_responses_client.get_response( messages=[ - ChatMessage( + Message( role="user", text="What is the weather today? Do a file search to find the answer.", ) ], stream=True, - options={"tools": [HostedFileSearchTool(inputs=vector_store)], "tool_choice": "auto"}, + options={ + "tools": [ + AzureOpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) + ], + "tool_choice": "auto", + }, ) full_response = await response_stream.get_final_response() @@ -379,23 +381,23 @@ async def test_integration_client_file_search_streaming() -> None: @pytest.mark.flaky @skip_if_azure_integration_tests_disabled async def test_integration_client_agent_hosted_mcp_tool() -> None: - """Integration test for HostedMCPTool with Azure Response Agent using Microsoft Learn MCP.""" + """Integration test for MCP tool with Azure Response Agent using Microsoft Learn MCP.""" client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) response = await client.get_response( "How to create an Azure storage account using az cli?", options={ # this needs to be high enough to handle the full MCP tool response. "max_tokens": 5000, - "tools": HostedMCPTool( + "tools": AzureOpenAIResponsesClient.get_mcp_tool( name="Microsoft Learn MCP", url="https://learn.microsoft.com/api/mcp", - description="A Microsoft Learn MCP server for documentation questions", - approval_mode="never_require", ), }, ) assert isinstance(response, ChatResponse) - assert response.text + # MCP server may return empty response intermittently - skip test rather than fail + if not response.text: + pytest.skip("MCP server returned empty response - service-side issue") # Should contain Azure-related content since it's asking about Azure CLI assert any(term in response.text.lower() for term in ["azure", "storage", "account", "cli"]) @@ -403,13 +405,13 @@ async def test_integration_client_agent_hosted_mcp_tool() -> None: @pytest.mark.flaky @skip_if_azure_integration_tests_disabled async def test_integration_client_agent_hosted_code_interpreter_tool(): - """Test Azure Responses Client agent with HostedCodeInterpreterTool through AzureOpenAIResponsesClient.""" + """Test Azure Responses Client agent with code interpreter tool.""" client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) response = await client.get_response( "Calculate the sum of numbers from 1 to 10 using Python code.", options={ - "tools": [HostedCodeInterpreterTool()], + "tools": [AzureOpenAIResponsesClient.get_code_interpreter_tool()], }, ) # Should contain calculation result (sum of 1-10 = 55) or code execution content @@ -426,8 +428,8 @@ async def test_integration_client_agent_existing_thread(): # First conversation - capture the thread preserved_thread = None - async with ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), instructions="You are a helpful assistant with good memory.", ) as first_agent: # Start a conversation and capture the thread @@ -442,8 +444,8 @@ async def test_integration_client_agent_existing_thread(): # Second conversation - reuse the thread in a new agent instance if preserved_thread: - async with ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), instructions="You are a helpful assistant with good memory.", ) as second_agent: # Reuse the preserved thread diff --git a/python/packages/core/tests/core/conftest.py b/python/packages/core/tests/core/conftest.py index 7cb5e63549..60df064d19 100644 --- a/python/packages/core/tests/core/conftest.py +++ b/python/packages/core/tests/core/conftest.py @@ -8,7 +8,6 @@ from typing import Any, Generic from unittest.mock import patch from uuid import uuid4 -from pydantic import BaseModel from pytest import fixture from agent_framework import ( @@ -16,15 +15,15 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseChatClient, - ChatMessage, ChatMiddlewareLayer, ChatResponse, ChatResponseUpdate, Content, FunctionInvocationLayer, + FunctionTool, + Message, ResponseStream, SupportsAgentRun, - ToolProtocol, tool, ) from agent_framework._clients import OptionsCoT @@ -40,7 +39,7 @@ logger = logging.getLogger(__name__) @fixture(scope="function") -def chat_history() -> list[ChatMessage]: +def chat_history() -> list[Message]: return [] @@ -48,26 +47,20 @@ def chat_history() -> list[ChatMessage]: @fixture -def ai_tool() -> ToolProtocol: - """Returns a generic ToolProtocol.""" +def ai_tool() -> FunctionTool: + """Returns a generic FunctionTool.""" - class GenericTool(BaseModel): - name: str - description: str - additional_properties: dict[str, Any] | None = None + @tool + def generic_tool(name: str) -> str: + """A generic tool that echoes the name.""" + return f"Hello, {name}" - def parameters(self) -> dict[str, Any]: - """Return the parameters of the tool as a JSON schema.""" - return { - "name": {"type": "string"}, - } - - return GenericTool(name="generic_tool", description="A generic tool") + return generic_tool @fixture -def tool_tool() -> ToolProtocol: - """Returns a executable ToolProtocol.""" +def tool_tool() -> FunctionTool: + """Returns a executable FunctionTool.""" @tool(approval_mode="never_require") def simple_function(x: int, y: int) -> int: @@ -90,7 +83,7 @@ class MockChatClient: def get_response( self, - messages: str | ChatMessage | list[str] | list[ChatMessage], + messages: str | Message | list[str] | list[Message], *, stream: bool = False, options: dict[str, Any] | None = None, @@ -105,14 +98,14 @@ class MockChatClient: self.call_count += 1 if self.responses: return self.responses.pop(0) - return ChatResponse(messages=ChatMessage(role="assistant", text="test response")) + return ChatResponse(messages=Message(role="assistant", text="test response")) return _get() def _get_streaming_response( self, *, - messages: str | ChatMessage | list[str] | list[ChatMessage], + messages: str | Message | list[str] | list[Message], options: dict[str, Any], **kwargs: Any, ) -> ResponseStream[ChatResponseUpdate, ChatResponse]: @@ -153,7 +146,7 @@ class MockBaseChatClient( def _inner_get_response( self, *, - messages: MutableSequence[ChatMessage], + messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any, @@ -180,7 +173,7 @@ class MockBaseChatClient( async def _get_non_streaming_response( self, *, - messages: MutableSequence[ChatMessage], + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any, ) -> ChatResponse: @@ -188,13 +181,13 @@ class MockBaseChatClient( logger.debug(f"Running base chat client inner, with: {messages=}, {options=}, {kwargs=}") self.call_count += 1 if not self.run_responses: - return ChatResponse(messages=ChatMessage(role="assistant", text=f"test response - {messages[-1].text}")) + return ChatResponse(messages=Message(role="assistant", text=f"test response - {messages[-1].text}")) response = self.run_responses.pop(0) if options.get("tool_choice") == "none": return ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", text="I broke out of the function invocation loop...", ), @@ -206,7 +199,7 @@ class MockBaseChatClient( def _get_streaming_response( self, *, - messages: MutableSequence[ChatMessage], + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any, ) -> ResponseStream[ChatResponseUpdate, ChatResponse]: @@ -251,7 +244,7 @@ def max_iterations(request: Any) -> int: @fixture -def chat_client(enable_function_calling: bool, max_iterations: int) -> MockChatClient: +def client(enable_function_calling: bool, max_iterations: int) -> MockChatClient: if enable_function_calling: with patch("agent_framework._tools.DEFAULT_MAX_ITERATIONS", max_iterations): return type("FunctionInvokingMockChatClient", (FunctionInvocationLayer, MockChatClient), {})() @@ -261,10 +254,10 @@ def chat_client(enable_function_calling: bool, max_iterations: int) -> MockChatC @fixture def chat_client_base(enable_function_calling: bool, max_iterations: int) -> MockBaseChatClient: with patch("agent_framework._tools.DEFAULT_MAX_ITERATIONS", max_iterations): - chat_client = MockBaseChatClient() + client = MockBaseChatClient() if not enable_function_calling: - chat_client.function_invocation_configuration["enabled"] = False - return chat_client + client.function_invocation_configuration["enabled"] = False + return client # region Agents @@ -289,7 +282,7 @@ class MockAgent(SupportsAgentRun): def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, stream: bool = False, @@ -301,17 +294,17 @@ class MockAgent(SupportsAgentRun): async def _run_impl( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, ) -> AgentResponse: logger.debug(f"Running mock agent, with: {messages=}, {thread=}, {kwargs=}") - return AgentResponse(messages=[ChatMessage(role="assistant", contents=[Content.from_text("Response")])]) + return AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("Response")])]) async def _run_stream_impl( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, diff --git a/python/packages/core/tests/core/test_agents.py b/python/packages/core/tests/core/test_agents.py index cbd8ea0469..fcb4542a24 100644 --- a/python/packages/core/tests/core/test_agents.py +++ b/python/packages/core/tests/core/test_agents.py @@ -10,21 +10,20 @@ import pytest from pytest import raises from agent_framework import ( + Agent, AgentResponse, AgentResponseUpdate, AgentThread, - ChatAgent, - ChatClientProtocol, - ChatMessage, ChatMessageStore, ChatOptions, ChatResponse, Content, Context, ContextProvider, - HostedCodeInterpreterTool, + FunctionTool, + Message, SupportsAgentRun, - ToolProtocol, + SupportsChatGetResponse, tool, ) from agent_framework._agents import _merge_options, _sanitize_agent_name @@ -55,60 +54,60 @@ async def test_agent_run_streaming(agent: SupportsAgentRun) -> None: assert updates[0].text == "Response" -def test_chat_client_agent_type(chat_client: ChatClientProtocol) -> None: - chat_client_agent = ChatAgent(chat_client=chat_client) +def test_chat_client_agent_type(client: SupportsChatGetResponse) -> None: + chat_client_agent = Agent(client=client) assert isinstance(chat_client_agent, SupportsAgentRun) -async def test_chat_client_agent_init(chat_client: ChatClientProtocol) -> None: +async def test_chat_client_agent_init(client: SupportsChatGetResponse) -> None: agent_id = str(uuid4()) - agent = ChatAgent(chat_client=chat_client, id=agent_id, description="Test") + agent = Agent(client=client, id=agent_id, description="Test") assert agent.id == agent_id assert agent.name is None assert agent.description == "Test" -async def test_chat_client_agent_init_with_name(chat_client: ChatClientProtocol) -> None: +async def test_chat_client_agent_init_with_name(client: SupportsChatGetResponse) -> None: agent_id = str(uuid4()) - agent = ChatAgent(chat_client=chat_client, id=agent_id, name="Test Agent", description="Test") + agent = Agent(client=client, id=agent_id, name="Test Agent", description="Test") assert agent.id == agent_id assert agent.name == "Test Agent" assert agent.description == "Test" -async def test_chat_client_agent_run(chat_client: ChatClientProtocol) -> None: - agent = ChatAgent(chat_client=chat_client) +async def test_chat_client_agent_run(client: SupportsChatGetResponse) -> None: + agent = Agent(client=client) result = await agent.run("Hello") assert result.text == "test response" -async def test_chat_client_agent_run_streaming(chat_client: ChatClientProtocol) -> None: - agent = ChatAgent(chat_client=chat_client) +async def test_chat_client_agent_run_streaming(client: SupportsChatGetResponse) -> None: + agent = Agent(client=client) result = await AgentResponse.from_update_generator(agent.run("Hello", stream=True)) assert result.text == "test streaming response another update" -async def test_chat_client_agent_get_new_thread(chat_client: ChatClientProtocol) -> None: - agent = ChatAgent(chat_client=chat_client) +async def test_chat_client_agent_get_new_thread(client: SupportsChatGetResponse) -> None: + agent = Agent(client=client) thread = agent.get_new_thread() assert isinstance(thread, AgentThread) -async def test_chat_client_agent_prepare_thread_and_messages(chat_client: ChatClientProtocol) -> None: - agent = ChatAgent(chat_client=chat_client) - message = ChatMessage(role="user", text="Hello") +async def test_chat_client_agent_prepare_thread_and_messages(client: SupportsChatGetResponse) -> None: + agent = Agent(client=client) + message = Message(role="user", text="Hello") thread = AgentThread(message_store=ChatMessageStore(messages=[message])) _, _, result_messages = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] thread=thread, - input_messages=[ChatMessage(role="user", text="Test")], + input_messages=[Message(role="user", text="Test")], ) assert len(result_messages) == 2 @@ -116,9 +115,9 @@ async def test_chat_client_agent_prepare_thread_and_messages(chat_client: ChatCl assert result_messages[1].text == "Test" -async def test_prepare_thread_does_not_mutate_agent_chat_options(chat_client: ChatClientProtocol) -> None: - tool = HostedCodeInterpreterTool() - agent = ChatAgent(chat_client=chat_client, tools=[tool]) +async def test_prepare_thread_does_not_mutate_agent_chat_options(client: SupportsChatGetResponse) -> None: + tool = {"type": "code_interpreter"} + agent = Agent(client=client, tools=[tool]) assert agent.default_options.get("tools") is not None base_tools = agent.default_options["tools"] @@ -126,25 +125,25 @@ async def test_prepare_thread_does_not_mutate_agent_chat_options(chat_client: Ch _, prepared_chat_options, _ = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] thread=thread, - input_messages=[ChatMessage(role="user", text="Test")], + input_messages=[Message(role="user", text="Test")], ) assert prepared_chat_options.get("tools") is not None assert base_tools is not prepared_chat_options["tools"] - prepared_chat_options["tools"].append(HostedCodeInterpreterTool()) # type: ignore[arg-type] + prepared_chat_options["tools"].append({"type": "code_interpreter"}) # type: ignore[arg-type] assert len(agent.default_options["tools"]) == 1 -async def test_chat_client_agent_update_thread_id(chat_client_base: ChatClientProtocol) -> None: +async def test_chat_client_agent_update_thread_id(chat_client_base: SupportsChatGetResponse) -> None: mock_response = ChatResponse( - messages=[ChatMessage(role="assistant", contents=[Content.from_text("test response")])], + messages=[Message(role="assistant", contents=[Content.from_text("test response")])], conversation_id="123", ) chat_client_base.run_responses = [mock_response] - agent = ChatAgent( - chat_client=chat_client_base, - tools=HostedCodeInterpreterTool(), + agent = Agent( + client=chat_client_base, + tools={"type": "code_interpreter"}, ) thread = agent.get_new_thread() @@ -154,8 +153,8 @@ async def test_chat_client_agent_update_thread_id(chat_client_base: ChatClientPr assert thread.service_thread_id == "123" -async def test_chat_client_agent_update_thread_messages(chat_client: ChatClientProtocol) -> None: - agent = ChatAgent(chat_client=chat_client) +async def test_chat_client_agent_update_thread_messages(client: SupportsChatGetResponse) -> None: + agent = Agent(client=client) thread = agent.get_new_thread() result = await agent.run("Hello", thread=thread) @@ -164,7 +163,7 @@ async def test_chat_client_agent_update_thread_messages(chat_client: ChatClientP assert thread.service_thread_id is None assert thread.message_store is not None - chat_messages: list[ChatMessage] = await thread.message_store.list_messages() + chat_messages: list[Message] = await thread.message_store.list_messages() assert chat_messages is not None assert len(chat_messages) == 2 @@ -172,42 +171,42 @@ async def test_chat_client_agent_update_thread_messages(chat_client: ChatClientP assert chat_messages[1].text == "test response" -async def test_chat_client_agent_update_thread_conversation_id_missing(chat_client: ChatClientProtocol) -> None: - agent = ChatAgent(chat_client=chat_client) +async def test_chat_client_agent_update_thread_conversation_id_missing(client: SupportsChatGetResponse) -> None: + agent = Agent(client=client) thread = AgentThread(service_thread_id="123") with raises(AgentExecutionException, match="Service did not return a valid conversation id"): await agent._update_thread_with_type_and_conversation_id(thread, None) # type: ignore[reportPrivateUsage] -async def test_chat_client_agent_default_author_name(chat_client: ChatClientProtocol) -> None: +async def test_chat_client_agent_default_author_name(client: SupportsChatGetResponse) -> None: # Name is not specified here, so default name should be used - agent = ChatAgent(chat_client=chat_client) + agent = Agent(client=client) result = await agent.run("Hello") assert result.text == "test response" assert result.messages[0].author_name == "UnnamedAgent" -async def test_chat_client_agent_author_name_as_agent_name(chat_client: ChatClientProtocol) -> None: +async def test_chat_client_agent_author_name_as_agent_name(client: SupportsChatGetResponse) -> None: # Name is specified here, so it should be used as author name - agent = ChatAgent(chat_client=chat_client, name="TestAgent") + agent = Agent(client=client, name="TestAgent") result = await agent.run("Hello") assert result.text == "test response" assert result.messages[0].author_name == "TestAgent" -async def test_chat_client_agent_author_name_is_used_from_response(chat_client_base: ChatClientProtocol) -> None: +async def test_chat_client_agent_author_name_is_used_from_response(chat_client_base: SupportsChatGetResponse) -> None: chat_client_base.run_responses = [ ChatResponse( messages=[ - ChatMessage(role="assistant", contents=[Content.from_text("test response")], author_name="TestAuthor") + Message(role="assistant", contents=[Content.from_text("test response")], author_name="TestAuthor") ] ) ] - agent = ChatAgent(chat_client=chat_client_base, tools=HostedCodeInterpreterTool()) + agent = Agent(client=chat_client_base, tools={"type": "code_interpreter"}) result = await agent.run("Hello") assert result.text == "test response" @@ -216,14 +215,14 @@ async def test_chat_client_agent_author_name_is_used_from_response(chat_client_b # Mock context provider for testing class MockContextProvider(ContextProvider): - def __init__(self, messages: list[ChatMessage] | None = None) -> None: + def __init__(self, messages: list[Message] | None = None) -> None: self.context_messages = messages self.thread_created_called = False self.invoked_called = False self.invoking_called = False self.thread_created_thread_id = None self.invoked_thread_id = None - self.new_messages: list[ChatMessage] = [] + self.new_messages: list[Message] = [] async def thread_created(self, thread_id: str | None) -> None: self.thread_created_called = True @@ -231,47 +230,47 @@ class MockContextProvider(ContextProvider): async def invoked( self, - request_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage] | None = None, + request_messages: Message | Sequence[Message], + response_messages: Message | Sequence[Message] | None = None, invoke_exception: Any = None, **kwargs: Any, ) -> None: self.invoked_called = True - if isinstance(request_messages, ChatMessage): + if isinstance(request_messages, Message): self.new_messages.append(request_messages) else: self.new_messages.extend(request_messages) - if isinstance(response_messages, ChatMessage): + if isinstance(response_messages, Message): self.new_messages.append(response_messages) else: self.new_messages.extend(response_messages) - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: self.invoking_called = True return Context(messages=self.context_messages) -async def test_chat_agent_context_providers_model_invoking(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_context_providers_model_invoking(client: SupportsChatGetResponse) -> None: """Test that context providers' invoking is called during agent run.""" - mock_provider = MockContextProvider(messages=[ChatMessage(role="system", text="Test context instructions")]) - agent = ChatAgent(chat_client=chat_client, context_provider=mock_provider) + mock_provider = MockContextProvider(messages=[Message(role="system", text="Test context instructions")]) + agent = Agent(client=client, context_provider=mock_provider) await agent.run("Hello") assert mock_provider.invoking_called -async def test_chat_agent_context_providers_thread_created(chat_client_base: ChatClientProtocol) -> None: +async def test_chat_agent_context_providers_thread_created(chat_client_base: SupportsChatGetResponse) -> None: """Test that context providers' thread_created is called during agent run.""" mock_provider = MockContextProvider() chat_client_base.run_responses = [ ChatResponse( - messages=[ChatMessage(role="assistant", contents=[Content.from_text("test response")])], + messages=[Message(role="assistant", contents=[Content.from_text("test response")])], conversation_id="test-thread-id", ) ] - agent = ChatAgent(chat_client=chat_client_base, context_provider=mock_provider) + agent = Agent(client=chat_client_base, context_provider=mock_provider) await agent.run("Hello") @@ -279,10 +278,10 @@ async def test_chat_agent_context_providers_thread_created(chat_client_base: Cha assert mock_provider.thread_created_thread_id == "test-thread-id" -async def test_chat_agent_context_providers_messages_adding(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_context_providers_messages_adding(client: SupportsChatGetResponse) -> None: """Test that context providers' invoked is called during agent run.""" mock_provider = MockContextProvider() - agent = ChatAgent(chat_client=chat_client, context_provider=mock_provider) + agent = Agent(client=client, context_provider=mock_provider) await agent.run("Hello") @@ -291,14 +290,14 @@ async def test_chat_agent_context_providers_messages_adding(chat_client: ChatCli assert len(mock_provider.new_messages) >= 2 -async def test_chat_agent_context_instructions_in_messages(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_context_instructions_in_messages(client: SupportsChatGetResponse) -> None: """Test that AI context instructions are included in messages.""" - mock_provider = MockContextProvider(messages=[ChatMessage(role="system", text="Context-specific instructions")]) - agent = ChatAgent(chat_client=chat_client, instructions="Agent instructions", context_provider=mock_provider) + mock_provider = MockContextProvider(messages=[Message(role="system", text="Context-specific instructions")]) + agent = Agent(client=client, instructions="Agent instructions", context_provider=mock_provider) # We need to test the _prepare_thread_and_messages method directly _, _, messages = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] - thread=None, input_messages=[ChatMessage(role="user", text="Hello")] + thread=None, input_messages=[Message(role="user", text="Hello")] ) # Should have context instructions, and user message @@ -307,16 +306,16 @@ async def test_chat_agent_context_instructions_in_messages(chat_client: ChatClie assert messages[0].text == "Context-specific instructions" assert messages[1].role == "user" assert messages[1].text == "Hello" - # instructions system message is added by a chat_client + # instructions system message is added by a client -async def test_chat_agent_no_context_instructions(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_no_context_instructions(client: SupportsChatGetResponse) -> None: """Test behavior when AI context has no instructions.""" mock_provider = MockContextProvider() - agent = ChatAgent(chat_client=chat_client, instructions="Agent instructions", context_provider=mock_provider) + agent = Agent(client=client, instructions="Agent instructions", context_provider=mock_provider) _, _, messages = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] - thread=None, input_messages=[ChatMessage(role="user", text="Hello")] + thread=None, input_messages=[Message(role="user", text="Hello")] ) # Should have agent instructions and user message only @@ -325,10 +324,10 @@ async def test_chat_agent_no_context_instructions(chat_client: ChatClientProtoco assert messages[0].text == "Hello" -async def test_chat_agent_run_stream_context_providers(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_run_stream_context_providers(client: SupportsChatGetResponse) -> None: """Test that context providers work with run method.""" - mock_provider = MockContextProvider(messages=[ChatMessage(role="system", text="Stream context instructions")]) - agent = ChatAgent(chat_client=chat_client, context_provider=mock_provider) + mock_provider = MockContextProvider(messages=[Message(role="system", text="Stream context instructions")]) + agent = Agent(client=client, context_provider=mock_provider) # Collect all stream updates and get final response stream = agent.run("Hello", stream=True) @@ -345,17 +344,17 @@ async def test_chat_agent_run_stream_context_providers(chat_client: ChatClientPr assert mock_provider.invoked_called -async def test_chat_agent_context_providers_with_thread_service_id(chat_client_base: ChatClientProtocol) -> None: +async def test_chat_agent_context_providers_with_thread_service_id(chat_client_base: SupportsChatGetResponse) -> None: """Test context providers with service-managed thread.""" mock_provider = MockContextProvider() chat_client_base.run_responses = [ ChatResponse( - messages=[ChatMessage(role="assistant", contents=[Content.from_text("test response")])], + messages=[Message(role="assistant", contents=[Content.from_text("test response")])], conversation_id="service-thread-123", ) ] - agent = ChatAgent(chat_client=chat_client_base, context_provider=mock_provider) + agent = Agent(client=chat_client_base, context_provider=mock_provider) # Use existing service-managed thread thread = agent.get_new_thread(service_thread_id="existing-thread-id") @@ -366,9 +365,9 @@ async def test_chat_agent_context_providers_with_thread_service_id(chat_client_b # Tests for as_tool method -async def test_chat_agent_as_tool_basic(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_as_tool_basic(client: SupportsChatGetResponse) -> None: """Test basic as_tool functionality.""" - agent = ChatAgent(chat_client=chat_client, name="TestAgent", description="Test agent for as_tool") + agent = Agent(client=client, name="TestAgent", description="Test agent for as_tool") tool = agent.as_tool() @@ -378,9 +377,9 @@ async def test_chat_agent_as_tool_basic(chat_client: ChatClientProtocol) -> None assert hasattr(tool, "input_model") -async def test_chat_agent_as_tool_custom_parameters(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_as_tool_custom_parameters(client: SupportsChatGetResponse) -> None: """Test as_tool with custom parameters.""" - agent = ChatAgent(chat_client=chat_client, name="TestAgent", description="Original description") + agent = Agent(client=client, name="TestAgent", description="Original description") tool = agent.as_tool( name="CustomTool", @@ -398,10 +397,10 @@ async def test_chat_agent_as_tool_custom_parameters(chat_client: ChatClientProto assert schema["properties"]["query"]["description"] == "Custom input description" -async def test_chat_agent_as_tool_defaults(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_as_tool_defaults(client: SupportsChatGetResponse) -> None: """Test as_tool with default parameters.""" - agent = ChatAgent( - chat_client=chat_client, + agent = Agent( + client=client, name="TestAgent", # No description provided ) @@ -417,18 +416,18 @@ async def test_chat_agent_as_tool_defaults(chat_client: ChatClientProtocol) -> N assert "Task for TestAgent" in schema["properties"]["task"]["description"] -async def test_chat_agent_as_tool_no_name(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_as_tool_no_name(client: SupportsChatGetResponse) -> None: """Test as_tool when agent has no name (should raise ValueError).""" - agent = ChatAgent(chat_client=chat_client) # No name provided + agent = Agent(client=client) # No name provided # Should raise ValueError since agent has no name with raises(ValueError, match="Agent tool name cannot be None"): agent.as_tool() -async def test_chat_agent_as_tool_function_execution(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_as_tool_function_execution(client: SupportsChatGetResponse) -> None: """Test that the generated FunctionTool can be executed.""" - agent = ChatAgent(chat_client=chat_client, name="TestAgent", description="Test agent") + agent = Agent(client=client, name="TestAgent", description="Test agent") tool = agent.as_tool() @@ -440,9 +439,9 @@ async def test_chat_agent_as_tool_function_execution(chat_client: ChatClientProt assert result == "test response" # From mock chat client -async def test_chat_agent_as_tool_with_stream_callback(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_as_tool_with_stream_callback(client: SupportsChatGetResponse) -> None: """Test as_tool with stream callback functionality.""" - agent = ChatAgent(chat_client=chat_client, name="StreamingAgent") + agent = Agent(client=client, name="StreamingAgent") # Collect streaming updates collected_updates: list[AgentResponseUpdate] = [] @@ -463,9 +462,9 @@ async def test_chat_agent_as_tool_with_stream_callback(chat_client: ChatClientPr assert result == expected_text -async def test_chat_agent_as_tool_with_custom_arg_name(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_as_tool_with_custom_arg_name(client: SupportsChatGetResponse) -> None: """Test as_tool with custom argument name.""" - agent = ChatAgent(chat_client=chat_client, name="CustomArgAgent") + agent = Agent(client=client, name="CustomArgAgent") tool = agent.as_tool(arg_name="prompt", arg_description="Custom prompt input") @@ -474,9 +473,9 @@ async def test_chat_agent_as_tool_with_custom_arg_name(chat_client: ChatClientPr assert result == "test response" -async def test_chat_agent_as_tool_with_async_stream_callback(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_as_tool_with_async_stream_callback(client: SupportsChatGetResponse) -> None: """Test as_tool with async stream callback functionality.""" - agent = ChatAgent(chat_client=chat_client, name="AsyncStreamingAgent") + agent = Agent(client=client, name="AsyncStreamingAgent") # Collect streaming updates using an async callback collected_updates: list[AgentResponseUpdate] = [] @@ -497,7 +496,7 @@ async def test_chat_agent_as_tool_with_async_stream_callback(chat_client: ChatCl assert result == expected_text -async def test_chat_agent_as_tool_name_sanitization(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_as_tool_name_sanitization(client: SupportsChatGetResponse) -> None: """Test as_tool name sanitization.""" test_cases = [ ("Invoice & Billing Agent", "Invoice_Billing_Agent"), @@ -510,14 +509,14 @@ async def test_chat_agent_as_tool_name_sanitization(chat_client: ChatClientProto ] for agent_name, expected_tool_name in test_cases: - agent = ChatAgent(chat_client=chat_client, name=agent_name, description="Test agent") + agent = Agent(client=client, name=agent_name, description="Test agent") tool = agent.as_tool() assert tool.name == expected_tool_name, f"Expected {expected_tool_name}, got {tool.name} for input {agent_name}" -async def test_chat_agent_as_mcp_server_basic(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_as_mcp_server_basic(client: SupportsChatGetResponse) -> None: """Test basic as_mcp_server functionality.""" - agent = ChatAgent(chat_client=chat_client, name="TestAgent", description="Test agent for MCP") + agent = Agent(client=client, name="TestAgent", description="Test agent for MCP") # Create MCP server with default parameters server = agent.as_mcp_server() @@ -528,9 +527,9 @@ async def test_chat_agent_as_mcp_server_basic(chat_client: ChatClientProtocol) - assert hasattr(server, "version") -async def test_chat_agent_run_with_mcp_tools(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_run_with_mcp_tools(client: SupportsChatGetResponse) -> None: """Test run method with MCP tools to cover MCP tool handling code.""" - agent = ChatAgent(chat_client=chat_client, name="TestAgent", description="Test agent") + agent = Agent(client=client, name="TestAgent", description="Test agent") # Create a mock MCP tool mock_mcp_tool = MagicMock(spec=MCPTool) @@ -547,7 +546,7 @@ async def test_chat_agent_run_with_mcp_tools(chat_client: ChatClientProtocol) -> await agent.run(messages="Test message", tools=[mock_mcp_tool]) -async def test_chat_agent_with_local_mcp_tools(chat_client: ChatClientProtocol) -> None: +async def test_chat_agent_with_local_mcp_tools(client: SupportsChatGetResponse) -> None: """Test agent initialization with local MCP tools.""" # Create a mock MCP tool mock_mcp_tool = MagicMock(spec=MCPTool) @@ -557,7 +556,7 @@ async def test_chat_agent_with_local_mcp_tools(chat_client: ChatClientProtocol) # Test agent with MCP tools in constructor with contextlib.suppress(Exception): - agent = ChatAgent(chat_client=chat_client, name="TestAgent", description="Test agent", tools=[mock_mcp_tool]) + agent = Agent(client=client, name="TestAgent", description="Test agent", tools=[mock_mcp_tool]) # Test async context manager with MCP tools async with agent: pass @@ -578,19 +577,17 @@ async def test_agent_tool_receives_thread_in_kwargs(chat_client_base: Any) -> No # Make the base client emit a function call for our tool chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="echo_thread_info", arguments='{"text": "hello"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] - agent = ChatAgent( - chat_client=chat_client_base, tools=[echo_thread_info], chat_message_store_factory=ChatMessageStore - ) + agent = Agent(client=chat_client_base, tools=[echo_thread_info], chat_message_store_factory=ChatMessageStore) thread = agent.get_new_thread() result = await agent.run("hello", thread=thread, options={"additional_function_arguments": {"thread": thread}}) @@ -609,7 +606,7 @@ async def test_chat_agent_tool_choice_run_level_overrides_agent_level(chat_clien original_inner = chat_client_base._inner_get_response async def capturing_inner( - *, messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + *, messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> ChatResponse: captured_options.append(options) return await original_inner(messages=messages, options=options, **kwargs) @@ -617,8 +614,8 @@ async def test_chat_agent_tool_choice_run_level_overrides_agent_level(chat_clien chat_client_base._inner_get_response = capturing_inner # Create agent with agent-level tool_choice="auto" and a tool (tools required for tool_choice to be meaningful) - agent = ChatAgent( - chat_client=chat_client_base, + agent = Agent( + client=chat_client_base, tools=[tool_tool], options={"tool_choice": "auto"}, ) @@ -640,7 +637,7 @@ async def test_chat_agent_tool_choice_agent_level_used_when_run_level_not_specif original_inner = chat_client_base._inner_get_response async def capturing_inner( - *, messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + *, messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> ChatResponse: captured_options.append(options) return await original_inner(messages=messages, options=options, **kwargs) @@ -648,8 +645,8 @@ async def test_chat_agent_tool_choice_agent_level_used_when_run_level_not_specif chat_client_base._inner_get_response = capturing_inner # Create agent with agent-level tool_choice="required" and a tool - agent = ChatAgent( - chat_client=chat_client_base, + agent = Agent( + client=chat_client_base, tools=[tool_tool], default_options={"tool_choice": "required"}, ) @@ -671,7 +668,7 @@ async def test_chat_agent_tool_choice_none_at_run_preserves_agent_level(chat_cli original_inner = chat_client_base._inner_get_response async def capturing_inner( - *, messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + *, messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> ChatResponse: captured_options.append(options) return await original_inner(messages=messages, options=options, **kwargs) @@ -679,8 +676,8 @@ async def test_chat_agent_tool_choice_none_at_run_preserves_agent_level(chat_cli chat_client_base._inner_get_response = capturing_inner # Create agent with agent-level tool_choice="auto" and a tool - agent = ChatAgent( - chat_client=chat_client_base, + agent = Agent( + client=chat_client_base, tools=[tool_tool], default_options={"tool_choice": "auto"}, ) @@ -808,9 +805,9 @@ def test_sanitize_agent_name_replaces_invalid_chars(): @pytest.mark.asyncio -async def test_agent_get_new_thread(chat_client_base: ChatClientProtocol, tool_tool: ToolProtocol): +async def test_agent_get_new_thread(chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool): """Test that get_new_thread returns a new AgentThread.""" - agent = ChatAgent(chat_client=chat_client_base, tools=[tool_tool]) + agent = Agent(client=chat_client_base, tools=[tool_tool]) thread = agent.get_new_thread() @@ -820,7 +817,7 @@ async def test_agent_get_new_thread(chat_client_base: ChatClientProtocol, tool_t @pytest.mark.asyncio async def test_agent_get_new_thread_with_context_provider( - chat_client_base: ChatClientProtocol, tool_tool: ToolProtocol + chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool ): """Test that get_new_thread passes context_provider to the thread.""" @@ -829,7 +826,7 @@ async def test_agent_get_new_thread_with_context_provider( return Context() provider = TestContextProvider() - agent = ChatAgent(chat_client=chat_client_base, tools=[tool_tool], context_provider=provider) + agent = Agent(client=chat_client_base, tools=[tool_tool], context_provider=provider) thread = agent.get_new_thread() @@ -839,10 +836,10 @@ async def test_agent_get_new_thread_with_context_provider( @pytest.mark.asyncio async def test_agent_get_new_thread_with_service_thread_id( - chat_client_base: ChatClientProtocol, tool_tool: ToolProtocol + chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool ): """Test that get_new_thread passes kwargs like service_thread_id to the thread.""" - agent = ChatAgent(chat_client=chat_client_base, tools=[tool_tool]) + agent = Agent(client=chat_client_base, tools=[tool_tool]) thread = agent.get_new_thread(service_thread_id="test-thread-123") @@ -851,9 +848,9 @@ async def test_agent_get_new_thread_with_service_thread_id( @pytest.mark.asyncio -async def test_agent_deserialize_thread(chat_client_base: ChatClientProtocol, tool_tool: ToolProtocol): +async def test_agent_deserialize_thread(chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool): """Test deserialize_thread restores a thread from serialized state.""" - agent = ChatAgent(chat_client=chat_client_base, tools=[tool_tool]) + agent = Agent(client=chat_client_base, tools=[tool_tool]) # Create serialized thread state with messages serialized_state = { @@ -876,30 +873,30 @@ async def test_agent_deserialize_thread(chat_client_base: ChatClientProtocol, to # endregion -# region Test ChatAgent initialization edge cases +# region Test Agent initialization edge cases @pytest.mark.asyncio async def test_chat_agent_raises_with_both_conversation_id_and_store(): - """Test ChatAgent raises error with both conversation_id and chat_message_store_factory.""" + """Test Agent raises error with both conversation_id and chat_message_store_factory.""" mock_client = MagicMock() mock_store_factory = MagicMock() with pytest.raises(AgentInitializationError, match="Cannot specify both"): - ChatAgent( - chat_client=mock_client, + Agent( + client=mock_client, default_options={"conversation_id": "test_id"}, chat_message_store_factory=mock_store_factory, ) def test_chat_agent_calls_update_agent_name_on_client(): - """Test that ChatAgent calls _update_agent_name_and_description on client if available.""" + """Test that Agent calls _update_agent_name_and_description on client if available.""" mock_client = MagicMock() mock_client._update_agent_name_and_description = MagicMock() - ChatAgent( - chat_client=mock_client, + Agent( + client=mock_client, name="TestAgent", description="Test description", ) @@ -909,7 +906,7 @@ def test_chat_agent_calls_update_agent_name_on_client(): @pytest.mark.asyncio -async def test_chat_agent_context_provider_adds_tools_when_agent_has_none(chat_client_base: ChatClientProtocol): +async def test_chat_agent_context_provider_adds_tools_when_agent_has_none(chat_client_base: SupportsChatGetResponse): """Test that context provider tools are used when agent has no default tools.""" @tool @@ -922,14 +919,14 @@ async def test_chat_agent_context_provider_adds_tools_when_agent_has_none(chat_c return Context(tools=[context_tool]) provider = ToolContextProvider() - agent = ChatAgent(chat_client=chat_client_base, context_provider=provider) + agent = Agent(client=chat_client_base, context_provider=provider) # Agent starts with empty tools list assert agent.default_options.get("tools") == [] # Run the agent and verify context tools are added _, options, _ = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] - thread=None, input_messages=[ChatMessage(role="user", text="Hello")] + thread=None, input_messages=[Message(role="user", text="Hello")] ) # The context tools should now be in the options @@ -938,7 +935,9 @@ async def test_chat_agent_context_provider_adds_tools_when_agent_has_none(chat_c @pytest.mark.asyncio -async def test_chat_agent_context_provider_adds_instructions_when_agent_has_none(chat_client_base: ChatClientProtocol): +async def test_chat_agent_context_provider_adds_instructions_when_agent_has_none( + chat_client_base: SupportsChatGetResponse, +): """Test that context provider instructions are used when agent has no default instructions.""" class InstructionContextProvider(ContextProvider): @@ -946,14 +945,14 @@ async def test_chat_agent_context_provider_adds_instructions_when_agent_has_none return Context(instructions="Context-provided instructions") provider = InstructionContextProvider() - agent = ChatAgent(chat_client=chat_client_base, context_provider=provider) + agent = Agent(client=chat_client_base, context_provider=provider) # Verify agent has no default instructions assert agent.default_options.get("instructions") is None # Run the agent and verify context instructions are available _, options, _ = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] - thread=None, input_messages=[ChatMessage(role="user", text="Hello")] + thread=None, input_messages=[Message(role="user", text="Hello")] ) # The context instructions should now be in the options @@ -961,10 +960,10 @@ async def test_chat_agent_context_provider_adds_instructions_when_agent_has_none @pytest.mark.asyncio -async def test_chat_agent_raises_on_conversation_id_mismatch(chat_client_base: ChatClientProtocol): - """Test that ChatAgent raises when thread and agent have different conversation IDs.""" - agent = ChatAgent( - chat_client=chat_client_base, +async def test_chat_agent_raises_on_conversation_id_mismatch(chat_client_base: SupportsChatGetResponse): + """Test that Agent raises when thread and agent have different conversation IDs.""" + agent = Agent( + client=chat_client_base, default_options={"conversation_id": "agent-conversation-id"}, ) @@ -973,7 +972,7 @@ async def test_chat_agent_raises_on_conversation_id_mismatch(chat_client_base: C with pytest.raises(AgentExecutionException, match="conversation_id set on the agent is different"): await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage] - thread=thread, input_messages=[ChatMessage(role="user", text="Hello")] + thread=thread, input_messages=[Message(role="user", text="Hello")] ) diff --git a/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py b/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py index 4672b10e77..b34164b86b 100644 --- a/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py +++ b/python/packages/core/tests/core/test_as_tool_kwargs_propagation.py @@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable from typing import Any -from agent_framework import ChatAgent, ChatMessage, ChatResponse, Content, agent_middleware +from agent_framework import Agent, ChatResponse, Content, Message, agent_middleware from agent_framework._middleware import AgentContext from .conftest import MockChatClient @@ -14,7 +14,7 @@ from .conftest import MockChatClient class TestAsToolKwargsPropagation: """Test cases for kwargs propagation through as_tool() delegation.""" - async def test_as_tool_forwards_runtime_kwargs(self, chat_client: MockChatClient) -> None: + async def test_as_tool_forwards_runtime_kwargs(self, client: MockChatClient) -> None: """Test that runtime kwargs are forwarded through as_tool() to sub-agent.""" captured_kwargs: dict[str, Any] = {} @@ -27,13 +27,13 @@ class TestAsToolKwargsPropagation: await call_next(context) # Setup mock response - chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from sub-agent")]), + client.responses = [ + ChatResponse(messages=[Message(role="assistant", text="Response from sub-agent")]), ] # Create sub-agent with middleware - sub_agent = ChatAgent( - chat_client=chat_client, + sub_agent = Agent( + client=client, name="sub_agent", middleware=[capture_middleware], ) @@ -57,7 +57,7 @@ class TestAsToolKwargsPropagation: assert "session_id" in captured_kwargs assert captured_kwargs["session_id"] == "session-789" - async def test_as_tool_excludes_arg_name_from_forwarded_kwargs(self, chat_client: MockChatClient) -> None: + async def test_as_tool_excludes_arg_name_from_forwarded_kwargs(self, client: MockChatClient) -> None: """Test that the arg_name parameter is not forwarded as a kwarg.""" captured_kwargs: dict[str, Any] = {} @@ -69,12 +69,12 @@ class TestAsToolKwargsPropagation: await call_next(context) # Setup mock response - chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from sub-agent")]), + client.responses = [ + ChatResponse(messages=[Message(role="assistant", text="Response from sub-agent")]), ] - sub_agent = ChatAgent( - chat_client=chat_client, + sub_agent = Agent( + client=client, name="sub_agent", middleware=[capture_middleware], ) @@ -94,7 +94,7 @@ class TestAsToolKwargsPropagation: assert "api_token" in captured_kwargs assert captured_kwargs["api_token"] == "token-123" - async def test_as_tool_nested_delegation_propagates_kwargs(self, chat_client: MockChatClient) -> None: + async def test_as_tool_nested_delegation_propagates_kwargs(self, client: MockChatClient) -> None: """Test that kwargs propagate through multiple levels of delegation (A → B → C).""" captured_kwargs_list: list[dict[str, Any]] = [] @@ -107,10 +107,10 @@ class TestAsToolKwargsPropagation: await call_next(context) # Setup mock responses to trigger nested tool invocation: B calls tool C, then completes. - chat_client.responses = [ + client.responses = [ ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -122,20 +122,20 @@ class TestAsToolKwargsPropagation: ) ] ), - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from agent_c")]), - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from agent_b")]), + ChatResponse(messages=[Message(role="assistant", text="Response from agent_c")]), + ChatResponse(messages=[Message(role="assistant", text="Response from agent_b")]), ] # Create agent C (bottom level) - agent_c = ChatAgent( - chat_client=chat_client, + agent_c = Agent( + client=client, name="agent_c", middleware=[capture_middleware], ) # Create agent B (middle level) - delegates to C - agent_b = ChatAgent( - chat_client=chat_client, + agent_b = Agent( + client=client, name="agent_b", tools=[agent_c.as_tool(name="call_c")], middleware=[capture_middleware], @@ -157,7 +157,7 @@ class TestAsToolKwargsPropagation: assert captured_kwargs_list[0].get("trace_id") == "trace-abc-123" assert captured_kwargs_list[0].get("tenant_id") == "tenant-xyz" - async def test_as_tool_streaming_mode_forwards_kwargs(self, chat_client: MockChatClient) -> None: + async def test_as_tool_streaming_mode_forwards_kwargs(self, client: MockChatClient) -> None: """Test that kwargs are forwarded in streaming mode.""" captured_kwargs: dict[str, Any] = {} @@ -171,12 +171,12 @@ class TestAsToolKwargsPropagation: # Setup mock streaming responses from agent_framework import ChatResponseUpdate - chat_client.streaming_responses = [ + client.streaming_responses = [ [ChatResponseUpdate(contents=[Content.from_text(text="Streaming response")], role="assistant")], ] - sub_agent = ChatAgent( - chat_client=chat_client, + sub_agent = Agent( + client=client, name="sub_agent", middleware=[capture_middleware], ) @@ -199,15 +199,15 @@ class TestAsToolKwargsPropagation: assert captured_kwargs["api_key"] == "streaming-key-999" assert len(captured_updates) == 1 - async def test_as_tool_empty_kwargs_still_works(self, chat_client: MockChatClient) -> None: + async def test_as_tool_empty_kwargs_still_works(self, client: MockChatClient) -> None: """Test that as_tool works correctly when no extra kwargs are provided.""" # Setup mock response - chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from agent")]), + client.responses = [ + ChatResponse(messages=[Message(role="assistant", text="Response from agent")]), ] - sub_agent = ChatAgent( - chat_client=chat_client, + sub_agent = Agent( + client=client, name="sub_agent", ) @@ -219,7 +219,7 @@ class TestAsToolKwargsPropagation: # Verify tool executed successfully assert result is not None - async def test_as_tool_kwargs_with_chat_options(self, chat_client: MockChatClient) -> None: + async def test_as_tool_kwargs_with_chat_options(self, client: MockChatClient) -> None: """Test that kwargs including chat_options are properly forwarded.""" captured_kwargs: dict[str, Any] = {} @@ -231,12 +231,12 @@ class TestAsToolKwargsPropagation: await call_next(context) # Setup mock response - chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="Response with options")]), + client.responses = [ + ChatResponse(messages=[Message(role="assistant", text="Response with options")]), ] - sub_agent = ChatAgent( - chat_client=chat_client, + sub_agent = Agent( + client=client, name="sub_agent", middleware=[capture_middleware], ) @@ -259,7 +259,7 @@ class TestAsToolKwargsPropagation: assert "custom_param" in captured_kwargs assert captured_kwargs["custom_param"] == "custom_value" - async def test_as_tool_kwargs_isolated_per_invocation(self, chat_client: MockChatClient) -> None: + async def test_as_tool_kwargs_isolated_per_invocation(self, client: MockChatClient) -> None: """Test that kwargs are isolated per invocation and don't leak between calls.""" first_call_kwargs: dict[str, Any] = {} second_call_kwargs: dict[str, Any] = {} @@ -278,13 +278,13 @@ class TestAsToolKwargsPropagation: await call_next(context) # Setup mock responses for both calls - chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="First response")]), - ChatResponse(messages=[ChatMessage(role="assistant", text="Second response")]), + client.responses = [ + ChatResponse(messages=[Message(role="assistant", text="First response")]), + ChatResponse(messages=[Message(role="assistant", text="Second response")]), ] - sub_agent = ChatAgent( - chat_client=chat_client, + sub_agent = Agent( + client=client, name="sub_agent", middleware=[capture_middleware], ) @@ -313,7 +313,7 @@ class TestAsToolKwargsPropagation: assert second_call_kwargs.get("session_id") == "session-2" assert second_call_kwargs.get("api_token") == "token-2" - async def test_as_tool_excludes_conversation_id_from_forwarded_kwargs(self, chat_client: MockChatClient) -> None: + async def test_as_tool_excludes_conversation_id_from_forwarded_kwargs(self, client: MockChatClient) -> None: """Test that conversation_id is not forwarded to sub-agent.""" captured_kwargs: dict[str, Any] = {} @@ -325,12 +325,12 @@ class TestAsToolKwargsPropagation: await call_next(context) # Setup mock response - chat_client.responses = [ - ChatResponse(messages=[ChatMessage(role="assistant", text="Response from sub-agent")]), + client.responses = [ + ChatResponse(messages=[Message(role="assistant", text="Response from sub-agent")]), ] - sub_agent = ChatAgent( - chat_client=chat_client, + sub_agent = Agent( + client=client, name="sub_agent", middleware=[capture_middleware], ) diff --git a/python/packages/core/tests/core/test_clients.py b/python/packages/core/tests/core/test_clients.py index e0c3da64da..0f87828baa 100644 --- a/python/packages/core/tests/core/test_clients.py +++ b/python/packages/core/tests/core/test_clients.py @@ -5,49 +5,54 @@ from unittest.mock import patch from agent_framework import ( BaseChatClient, - ChatClientProtocol, - ChatMessage, ChatResponse, + Message, + SupportsChatGetResponse, + SupportsCodeInterpreterTool, + SupportsFileSearchTool, + SupportsImageGenerationTool, + SupportsMCPTool, + SupportsWebSearchTool, ) -def test_chat_client_type(chat_client: ChatClientProtocol): - assert isinstance(chat_client, ChatClientProtocol) +def test_chat_client_type(client: SupportsChatGetResponse): + assert isinstance(client, SupportsChatGetResponse) -async def test_chat_client_get_response(chat_client: ChatClientProtocol): - response = await chat_client.get_response(ChatMessage(role="user", text="Hello")) +async def test_chat_client_get_response(client: SupportsChatGetResponse): + response = await client.get_response(Message(role="user", text="Hello")) assert response.text == "test response" assert response.messages[0].role == "assistant" -async def test_chat_client_get_response_streaming(chat_client: ChatClientProtocol): - async for update in chat_client.get_response(ChatMessage(role="user", text="Hello"), stream=True): +async def test_chat_client_get_response_streaming(client: SupportsChatGetResponse): + async for update in client.get_response(Message(role="user", text="Hello"), stream=True): assert update.text == "test streaming response " or update.text == "another update" assert update.role == "assistant" -def test_base_client(chat_client_base: ChatClientProtocol): +def test_base_client(chat_client_base: SupportsChatGetResponse): assert isinstance(chat_client_base, BaseChatClient) - assert isinstance(chat_client_base, ChatClientProtocol) + assert isinstance(chat_client_base, SupportsChatGetResponse) -async def test_base_client_get_response(chat_client_base: ChatClientProtocol): - response = await chat_client_base.get_response(ChatMessage(role="user", text="Hello")) +async def test_base_client_get_response(chat_client_base: SupportsChatGetResponse): + response = await chat_client_base.get_response(Message(role="user", text="Hello")) assert response.messages[0].role == "assistant" assert response.messages[0].text == "test response - Hello" -async def test_base_client_get_response_streaming(chat_client_base: ChatClientProtocol): - async for update in chat_client_base.get_response(ChatMessage(role="user", text="Hello"), stream=True): +async def test_base_client_get_response_streaming(chat_client_base: SupportsChatGetResponse): + async for update in chat_client_base.get_response(Message(role="user", text="Hello"), stream=True): assert update.text == "update - Hello" or update.text == "another update" -async def test_chat_client_instructions_handling(chat_client_base: ChatClientProtocol): +async def test_chat_client_instructions_handling(chat_client_base: SupportsChatGetResponse): instructions = "You are a helpful assistant." async def fake_inner_get_response(**kwargs): - return ChatResponse(messages=[ChatMessage(role="assistant", text="ok")]) + return ChatResponse(messages=[Message(role="assistant", text="ok")]) with patch.object( chat_client_base, @@ -65,7 +70,7 @@ async def test_chat_client_instructions_handling(chat_client_base: ChatClientPro from agent_framework._types import prepend_instructions_to_messages appended_messages = prepend_instructions_to_messages( - [ChatMessage(role="user", text="hello")], + [Message(role="user", text="hello")], instructions, ) assert len(appended_messages) == 2 @@ -73,3 +78,66 @@ async def test_chat_client_instructions_handling(chat_client_base: ChatClientPro assert appended_messages[0].text == "You are a helpful assistant." assert appended_messages[1].role == "user" assert appended_messages[1].text == "hello" + + +# region Tool Support Protocol Tests + + +def test_openai_responses_client_supports_all_tool_protocols(): + """Test that OpenAIResponsesClient supports all hosted tool protocols.""" + from agent_framework.openai import OpenAIResponsesClient + + assert isinstance(OpenAIResponsesClient, SupportsCodeInterpreterTool) + assert isinstance(OpenAIResponsesClient, SupportsWebSearchTool) + assert isinstance(OpenAIResponsesClient, SupportsImageGenerationTool) + assert isinstance(OpenAIResponsesClient, SupportsMCPTool) + assert isinstance(OpenAIResponsesClient, SupportsFileSearchTool) + + +def test_openai_chat_client_supports_web_search_only(): + """Test that OpenAIChatClient only supports web search tool.""" + from agent_framework.openai import OpenAIChatClient + + assert not isinstance(OpenAIChatClient, SupportsCodeInterpreterTool) + assert isinstance(OpenAIChatClient, SupportsWebSearchTool) + assert not isinstance(OpenAIChatClient, SupportsImageGenerationTool) + assert not isinstance(OpenAIChatClient, SupportsMCPTool) + assert not isinstance(OpenAIChatClient, SupportsFileSearchTool) + + +def test_openai_assistants_client_supports_code_interpreter_and_file_search(): + """Test that OpenAIAssistantsClient supports code interpreter and file search.""" + from agent_framework.openai import OpenAIAssistantsClient + + assert isinstance(OpenAIAssistantsClient, SupportsCodeInterpreterTool) + assert not isinstance(OpenAIAssistantsClient, SupportsWebSearchTool) + assert not isinstance(OpenAIAssistantsClient, SupportsImageGenerationTool) + assert not isinstance(OpenAIAssistantsClient, SupportsMCPTool) + assert isinstance(OpenAIAssistantsClient, SupportsFileSearchTool) + + +def test_protocol_isinstance_with_client_instance(): + """Test that protocol isinstance works with client instances.""" + from agent_framework.openai import OpenAIResponsesClient + + # Create mock client instance (won't connect to API) + client = OpenAIResponsesClient.__new__(OpenAIResponsesClient) + + assert isinstance(client, SupportsCodeInterpreterTool) + assert isinstance(client, SupportsWebSearchTool) + + +def test_protocol_tool_methods_return_dict(): + """Test that static tool methods return dict[str, Any].""" + from agent_framework.openai import OpenAIResponsesClient + + code_tool = OpenAIResponsesClient.get_code_interpreter_tool() + assert isinstance(code_tool, dict) + assert code_tool.get("type") == "code_interpreter" + + web_tool = OpenAIResponsesClient.get_web_search_tool() + assert isinstance(web_tool, dict) + assert web_tool.get("type") == "web_search" + + +# endregion diff --git a/python/packages/core/tests/core/test_function_invocation_logic.py b/python/packages/core/tests/core/test_function_invocation_logic.py index 946bb89724..dcc28958f5 100644 --- a/python/packages/core/tests/core/test_function_invocation_logic.py +++ b/python/packages/core/tests/core/test_function_invocation_logic.py @@ -7,18 +7,18 @@ from typing import Any import pytest from agent_framework import ( - ChatAgent, - ChatClientProtocol, - ChatMessage, + Agent, ChatResponse, ChatResponseUpdate, Content, + Message, + SupportsChatGetResponse, tool, ) from agent_framework._middleware import FunctionInvocationContext, FunctionMiddleware, MiddlewareTermination -async def test_base_client_with_function_calling(chat_client_base: ChatClientProtocol): +async def test_base_client_with_function_calling(chat_client_base: SupportsChatGetResponse): exec_counter = 0 @tool(name="test_function", approval_mode="never_require") @@ -29,14 +29,14 @@ async def test_base_client_with_function_calling(chat_client_base: ChatClientPro chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="test_function", arguments='{"arg1": "value1"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [ai_func]}) assert exec_counter == 1 @@ -55,7 +55,7 @@ async def test_base_client_with_function_calling(chat_client_base: ChatClientPro @pytest.mark.parametrize("max_iterations", [3]) -async def test_base_client_with_function_calling_resets(chat_client_base: ChatClientProtocol): +async def test_base_client_with_function_calling_resets(chat_client_base: SupportsChatGetResponse): exec_counter = 0 @tool(name="test_function", approval_mode="never_require") @@ -66,7 +66,7 @@ async def test_base_client_with_function_calling_resets(chat_client_base: ChatCl chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="test_function", arguments='{"arg1": "value1"}') @@ -74,14 +74,14 @@ async def test_base_client_with_function_calling_resets(chat_client_base: ChatCl ) ), ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="2", name="test_function", arguments='{"arg1": "value1"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [ai_func]}) assert exec_counter == 2 @@ -97,7 +97,7 @@ async def test_base_client_with_function_calling_resets(chat_client_base: ChatCl assert response.messages[3].contents[0].type == "function_result" -async def test_base_client_with_streaming_function_calling(chat_client_base: ChatClientProtocol): +async def test_base_client_with_streaming_function_calling(chat_client_base: SupportsChatGetResponse): exec_counter = 0 @tool(name="test_function", approval_mode="never_require") @@ -137,7 +137,7 @@ async def test_base_client_with_streaming_function_calling(chat_client_base: Cha assert exec_counter == 1 -async def test_function_invocation_inside_aiohttp_server(chat_client_base: ChatClientProtocol): +async def test_function_invocation_inside_aiohttp_server(chat_client_base: SupportsChatGetResponse): import aiohttp from aiohttp import web @@ -151,7 +151,7 @@ async def test_function_invocation_inside_aiohttp_server(chat_client_base: ChatC chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call( @@ -162,10 +162,10 @@ async def test_function_invocation_inside_aiohttp_server(chat_client_base: ChatC ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] - agent = ChatAgent(chat_client=chat_client_base, tools=[ai_func]) + agent = Agent(client=chat_client_base, tools=[ai_func]) async def handler(request: web.Request) -> web.Response: thread = agent.get_new_thread() @@ -190,7 +190,7 @@ async def test_function_invocation_inside_aiohttp_server(chat_client_base: ChatC assert exec_counter == 1 -async def test_function_invocation_in_threaded_aiohttp_app(chat_client_base: ChatClientProtocol): +async def test_function_invocation_in_threaded_aiohttp_app(chat_client_base: SupportsChatGetResponse): import asyncio import threading from queue import Queue @@ -208,7 +208,7 @@ async def test_function_invocation_in_threaded_aiohttp_app(chat_client_base: Cha chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call( @@ -219,10 +219,10 @@ async def test_function_invocation_in_threaded_aiohttp_app(chat_client_base: Cha ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] - agent = ChatAgent(chat_client=chat_client_base, tools=[ai_func]) + agent = Agent(client=chat_client_base, tools=[ai_func]) ready_event = threading.Event() port_queue: Queue[int] = Queue() @@ -297,7 +297,7 @@ async def test_function_invocation_in_threaded_aiohttp_app(chat_client_base: Cha ) @pytest.mark.parametrize("streaming", [False, True], ids=["non-streaming", "streaming"]) async def test_function_invocation_scenarios( - chat_client_base: ChatClientProtocol, + chat_client_base: SupportsChatGetResponse, streaming: bool, thread_type: str | None, approval_required: bool | str, @@ -339,11 +339,11 @@ async def test_function_invocation_scenarios( # Single function call content func_call = Content.from_function_call(call_id="1", name=function_name, arguments='{"arg1": "value1"}') - completion = ChatMessage(role="assistant", text="done") + completion = Message(role="assistant", text="done") - chat_client_base.run_responses = [ - ChatResponse(messages=ChatMessage(role="assistant", contents=[func_call])) - ] + ([] if approval_required else [ChatResponse(messages=completion)]) + chat_client_base.run_responses = [ChatResponse(messages=Message(role="assistant", contents=[func_call]))] + ( + [] if approval_required else [ChatResponse(messages=completion)] + ) chat_client_base.streaming_responses = [ [ @@ -371,7 +371,7 @@ async def test_function_invocation_scenarios( Content.from_function_call(call_id="2", name="approval_func", arguments='{"arg1": "value2"}'), ] - chat_client_base.run_responses = [ChatResponse(messages=ChatMessage(role="assistant", contents=func_calls))] + chat_client_base.run_responses = [ChatResponse(messages=Message(role="assistant", contents=func_calls))] chat_client_base.streaming_responses = [ [ @@ -468,7 +468,7 @@ async def test_function_invocation_scenarios( assert exec_counter == 0 # Neither function executed yet -async def test_rejected_approval(chat_client_base: ChatClientProtocol): +async def test_rejected_approval(chat_client_base: SupportsChatGetResponse): """Test that rejecting an approval alongside an approved one is handled correctly.""" exec_counter_approved = 0 @@ -489,7 +489,7 @@ async def test_rejected_approval(chat_client_base: ChatClientProtocol): # Setup: two function calls that require approval chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="approved_func", arguments='{"arg1": "value1"}'), @@ -497,7 +497,7 @@ async def test_rejected_approval(chat_client_base: ChatClientProtocol): ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Get the response with approval requests @@ -527,7 +527,7 @@ async def test_rejected_approval(chat_client_base: ChatClientProtocol): ) # Continue conversation with one approved and one rejected - all_messages = response.messages + [ChatMessage(role="user", contents=[approved_response, rejected_response])] + all_messages = response.messages + [Message(role="user", contents=[approved_response, rejected_response])] # Call get_response which will process the approvals await chat_client_base.get_response( @@ -564,7 +564,7 @@ async def test_rejected_approval(chat_client_base: ChatClientProtocol): assert msg.role == "tool", f"Message with FunctionResultContent must have role='tool', got '{msg.role}'" -async def test_approval_requests_in_assistant_message(chat_client_base: ChatClientProtocol): +async def test_approval_requests_in_assistant_message(chat_client_base: SupportsChatGetResponse): """Approval requests should be added to the assistant message that contains the function call.""" exec_counter = 0 @@ -576,7 +576,7 @@ async def test_approval_requests_in_assistant_message(chat_client_base: ChatClie chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="test_func", arguments='{"arg1": "value1"}'), @@ -598,7 +598,7 @@ async def test_approval_requests_in_assistant_message(chat_client_base: ChatClie assert exec_counter == 0 -async def test_persisted_approval_messages_replay_correctly(chat_client_base: ChatClientProtocol): +async def test_persisted_approval_messages_replay_correctly(chat_client_base: SupportsChatGetResponse): """Approval flow should work when messages are persisted and sent back (thread scenario).""" exec_counter = 0 @@ -611,14 +611,14 @@ async def test_persisted_approval_messages_replay_correctly(chat_client_base: Ch chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="test_func", arguments='{"arg1": "value1"}'), ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Get approval request @@ -628,7 +628,7 @@ async def test_persisted_approval_messages_replay_correctly(chat_client_base: Ch # Store messages (like a thread would) persisted_messages = [ - ChatMessage(role="user", text="hello"), + Message(role="user", text="hello"), *response1.messages, ] @@ -639,7 +639,7 @@ async def test_persisted_approval_messages_replay_correctly(chat_client_base: Ch function_call=approval_req.function_call, approved=True, ) - persisted_messages.append(ChatMessage(role="user", contents=[approval_response])) + persisted_messages.append(Message(role="user", contents=[approval_response])) # Continue with all persisted messages response2 = await chat_client_base.get_response( @@ -651,7 +651,7 @@ async def test_persisted_approval_messages_replay_correctly(chat_client_base: Ch assert exec_counter == 1 -async def test_no_duplicate_function_calls_after_approval_processing(chat_client_base: ChatClientProtocol): +async def test_no_duplicate_function_calls_after_approval_processing(chat_client_base: SupportsChatGetResponse): """Processing approval should not create duplicate function calls in messages.""" @tool(name="test_func", approval_mode="always_require") @@ -660,14 +660,14 @@ async def test_no_duplicate_function_calls_after_approval_processing(chat_client chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="test_func", arguments='{"arg1": "value1"}'), ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] response1 = await chat_client_base.get_response( @@ -681,7 +681,7 @@ async def test_no_duplicate_function_calls_after_approval_processing(chat_client approved=True, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])] + all_messages = response1.messages + [Message(role="user", contents=[approval_response])] await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [func_with_approval]}) # Count function calls with the same call_id @@ -695,7 +695,7 @@ async def test_no_duplicate_function_calls_after_approval_processing(chat_client assert function_call_count == 1 -async def test_rejection_result_uses_function_call_id(chat_client_base: ChatClientProtocol): +async def test_rejection_result_uses_function_call_id(chat_client_base: SupportsChatGetResponse): """Rejection error result should use the function call's call_id, not the approval's id.""" @tool(name="test_func", approval_mode="always_require") @@ -704,14 +704,14 @@ async def test_rejection_result_uses_function_call_id(chat_client_base: ChatClie chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="call_123", name="test_func", arguments='{"arg1": "value1"}'), ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] response1 = await chat_client_base.get_response( @@ -725,7 +725,7 @@ async def test_rejection_result_uses_function_call_id(chat_client_base: ChatClie approved=False, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[rejection_response])] + all_messages = response1.messages + [Message(role="user", contents=[rejection_response])] await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [func_with_approval]}) # Find the rejection result @@ -741,7 +741,7 @@ async def test_rejection_result_uses_function_call_id(chat_client_base: ChatClie @pytest.mark.skip(reason="Failsafe behavior with max_iterations needs investigation in unified API") @pytest.mark.skip(reason="Failsafe behavior with max_iterations needs investigation in unified API") -async def test_max_iterations_limit(chat_client_base: ChatClientProtocol): +async def test_max_iterations_limit(chat_client_base: SupportsChatGetResponse): """Test that MAX_ITERATIONS in additional_properties limits function call loops.""" exec_counter = 0 @@ -754,7 +754,7 @@ async def test_max_iterations_limit(chat_client_base: ChatClientProtocol): # Set up multiple function call responses to create a loop chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="test_function", arguments='{"arg1": "value1"}') @@ -762,7 +762,7 @@ async def test_max_iterations_limit(chat_client_base: ChatClientProtocol): ) ), ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="2", name="test_function", arguments='{"arg1": "value2"}') @@ -770,7 +770,7 @@ async def test_max_iterations_limit(chat_client_base: ChatClientProtocol): ) ), # Failsafe response when tool_choice is set to "none" - ChatResponse(messages=ChatMessage(role="assistant", text="giving up on tools")), + ChatResponse(messages=Message(role="assistant", text="giving up on tools")), ] # Set max_iterations to 1 in additional_properties @@ -786,7 +786,7 @@ async def test_max_iterations_limit(chat_client_base: ChatClientProtocol): assert response.messages[-1].text == "I broke out of the function invocation loop..." # Failsafe response -async def test_function_invocation_config_enabled_false(chat_client_base: ChatClientProtocol): +async def test_function_invocation_config_enabled_false(chat_client_base: SupportsChatGetResponse): """Test that setting enabled=False disables function invocation.""" exec_counter = 0 @@ -797,7 +797,7 @@ async def test_function_invocation_config_enabled_false(chat_client_base: ChatCl return f"Processed {arg1}" chat_client_base.run_responses = [ - ChatResponse(messages=ChatMessage(role="assistant", text="response without function calling")), + ChatResponse(messages=Message(role="assistant", text="response without function calling")), ] # Disable function invocation @@ -812,7 +812,7 @@ async def test_function_invocation_config_enabled_false(chat_client_base: ChatCl @pytest.mark.skip(reason="Error handling and failsafe behavior needs investigation in unified API") -async def test_function_invocation_config_max_consecutive_errors(chat_client_base: ChatClientProtocol): +async def test_function_invocation_config_max_consecutive_errors(chat_client_base: SupportsChatGetResponse): """Test that max_consecutive_errors_per_request limits error retries.""" @tool(name="error_function", approval_mode="never_require") @@ -822,7 +822,7 @@ async def test_function_invocation_config_max_consecutive_errors(chat_client_bas # Set up multiple function call responses that will all error chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="error_function", arguments='{"arg1": "value1"}') @@ -830,7 +830,7 @@ async def test_function_invocation_config_max_consecutive_errors(chat_client_bas ) ), ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="2", name="error_function", arguments='{"arg1": "value2"}') @@ -838,7 +838,7 @@ async def test_function_invocation_config_max_consecutive_errors(chat_client_bas ) ), ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="3", name="error_function", arguments='{"arg1": "value3"}') @@ -846,14 +846,14 @@ async def test_function_invocation_config_max_consecutive_errors(chat_client_bas ) ), ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="4", name="error_function", arguments='{"arg1": "value4"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="final response")), + ChatResponse(messages=Message(role="assistant", text="final response")), ] # Set max_consecutive_errors to 2 @@ -879,7 +879,7 @@ async def test_function_invocation_config_max_consecutive_errors(chat_client_bas assert len(function_calls) <= 2 -async def test_function_invocation_config_terminate_on_unknown_calls_false(chat_client_base: ChatClientProtocol): +async def test_function_invocation_config_terminate_on_unknown_calls_false(chat_client_base: SupportsChatGetResponse): """Test that terminate_on_unknown_calls=False returns error message for unknown functions.""" exec_counter = 0 @@ -891,14 +891,14 @@ async def test_function_invocation_config_terminate_on_unknown_calls_false(chat_ chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="unknown_function", arguments='{"arg1": "value1"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Set terminate_on_unknown_calls to False (default) @@ -914,7 +914,7 @@ async def test_function_invocation_config_terminate_on_unknown_calls_false(chat_ assert exec_counter == 0 # Known function not executed -async def test_function_invocation_config_terminate_on_unknown_calls_true(chat_client_base: ChatClientProtocol): +async def test_function_invocation_config_terminate_on_unknown_calls_true(chat_client_base: SupportsChatGetResponse): """Test that terminate_on_unknown_calls=True stops execution on unknown functions.""" exec_counter = 0 @@ -926,7 +926,7 @@ async def test_function_invocation_config_terminate_on_unknown_calls_true(chat_c chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="unknown_function", arguments='{"arg1": "value1"}') @@ -945,7 +945,7 @@ async def test_function_invocation_config_terminate_on_unknown_calls_true(chat_c assert exec_counter == 0 -async def test_function_invocation_config_additional_tools(chat_client_base: ChatClientProtocol): +async def test_function_invocation_config_additional_tools(chat_client_base: SupportsChatGetResponse): """Test that additional_tools are available but treated as declaration_only.""" exec_counter_visible = 0 exec_counter_hidden = 0 @@ -964,14 +964,14 @@ async def test_function_invocation_config_additional_tools(chat_client_base: Cha chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="hidden_function", arguments='{"arg1": "value1"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Add hidden_func to additional_tools @@ -994,7 +994,7 @@ async def test_function_invocation_config_additional_tools(chat_client_base: Cha assert len(function_calls) >= 1 -async def test_function_invocation_config_include_detailed_errors_false(chat_client_base: ChatClientProtocol): +async def test_function_invocation_config_include_detailed_errors_false(chat_client_base: SupportsChatGetResponse): """Test that include_detailed_errors=False returns generic error messages.""" @tool(name="error_function", approval_mode="never_require") @@ -1003,14 +1003,14 @@ async def test_function_invocation_config_include_detailed_errors_false(chat_cli chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="error_function", arguments='{"arg1": "value1"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Set include_detailed_errors to False (default) @@ -1028,7 +1028,7 @@ async def test_function_invocation_config_include_detailed_errors_false(chat_cli assert "Error:" in error_result.result # Generic error prefix -async def test_function_invocation_config_include_detailed_errors_true(chat_client_base: ChatClientProtocol): +async def test_function_invocation_config_include_detailed_errors_true(chat_client_base: SupportsChatGetResponse): """Test that include_detailed_errors=True returns detailed error information.""" @tool(name="error_function", approval_mode="never_require") @@ -1037,14 +1037,14 @@ async def test_function_invocation_config_include_detailed_errors_true(chat_clie chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="error_function", arguments='{"arg1": "value1"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Set include_detailed_errors to True @@ -1098,7 +1098,7 @@ async def test_function_invocation_config_validation_max_consecutive_errors(): normalize_function_invocation_configuration({"max_consecutive_errors_per_request": -1}) -async def test_argument_validation_error_with_detailed_errors(chat_client_base: ChatClientProtocol): +async def test_argument_validation_error_with_detailed_errors(chat_client_base: SupportsChatGetResponse): """Test that argument validation errors include details when include_detailed_errors=True.""" @tool(name="typed_function", approval_mode="never_require") @@ -1107,14 +1107,14 @@ async def test_argument_validation_error_with_detailed_errors(chat_client_base: chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="typed_function", arguments='{"arg1": "not_an_int"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Set include_detailed_errors to True @@ -1132,7 +1132,7 @@ async def test_argument_validation_error_with_detailed_errors(chat_client_base: assert "Exception:" in error_result.result # Detailed error included -async def test_argument_validation_error_without_detailed_errors(chat_client_base: ChatClientProtocol): +async def test_argument_validation_error_without_detailed_errors(chat_client_base: SupportsChatGetResponse): """Test that argument validation errors are generic when include_detailed_errors=False.""" @tool(name="typed_function", approval_mode="never_require") @@ -1141,14 +1141,14 @@ async def test_argument_validation_error_without_detailed_errors(chat_client_bas chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="typed_function", arguments='{"arg1": "not_an_int"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Set include_detailed_errors to False (default) @@ -1166,7 +1166,7 @@ async def test_argument_validation_error_without_detailed_errors(chat_client_bas assert "Exception:" not in error_result.result # No detailed error -async def test_hosted_tool_approval_response(chat_client_base: ChatClientProtocol): +async def test_hosted_tool_approval_response(chat_client_base: SupportsChatGetResponse): """Test handling of approval responses for hosted tools (tools not in tool_map).""" @tool(name="local_function") @@ -1184,12 +1184,12 @@ async def test_hosted_tool_approval_response(chat_client_base: ChatClientProtoco ) chat_client_base.run_responses = [ - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Send the approval response response = await chat_client_base.get_response( - [ChatMessage(role="user", contents=[approval_response])], + [Message(role="user", contents=[approval_response])], tool_choice="auto", tools=[local_func], ) @@ -1199,7 +1199,7 @@ async def test_hosted_tool_approval_response(chat_client_base: ChatClientProtoco assert response is not None -async def test_unapproved_tool_execution_raises_exception(chat_client_base: ChatClientProtocol): +async def test_unapproved_tool_execution_raises_exception(chat_client_base: SupportsChatGetResponse): """Test that attempting to execute an unapproved tool raises ToolException.""" @tool(name="test_function", approval_mode="always_require") @@ -1208,14 +1208,14 @@ async def test_unapproved_tool_execution_raises_exception(chat_client_base: Chat chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="test_function", arguments='{"arg1": "value1"}'), ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Get approval request @@ -1231,7 +1231,7 @@ async def test_unapproved_tool_execution_raises_exception(chat_client_base: Chat ) # Continue conversation with rejection - all_messages = response1.messages + [ChatMessage(role="user", contents=[rejection_response])] + all_messages = response1.messages + [Message(role="user", contents=[rejection_response])] # This should handle the rejection gracefully (not raise ToolException to user) await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [test_func]}) @@ -1249,7 +1249,7 @@ async def test_unapproved_tool_execution_raises_exception(chat_client_base: Chat assert rejection_result is not None -async def test_approved_function_call_with_error_without_detailed_errors(chat_client_base: ChatClientProtocol): +async def test_approved_function_call_with_error_without_detailed_errors(chat_client_base: SupportsChatGetResponse): """Test that approved functions that raise errors return generic error messages. When include_detailed_errors=False. @@ -1265,12 +1265,12 @@ async def test_approved_function_call_with_error_without_detailed_errors(chat_cl chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[Content.from_function_call(call_id="1", name="error_func", arguments='{"arg1": "value1"}')], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Set include_detailed_errors to False (default) @@ -1288,7 +1288,7 @@ async def test_approved_function_call_with_error_without_detailed_errors(chat_cl approved=True, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])] + all_messages = response1.messages + [Message(role="user", contents=[approval_response])] # Execute the approved function (which will error) await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [error_func]}) @@ -1312,7 +1312,7 @@ async def test_approved_function_call_with_error_without_detailed_errors(chat_cl assert "Specific error from approved function" not in error_result.result # Detail not included -async def test_approved_function_call_with_error_with_detailed_errors(chat_client_base: ChatClientProtocol): +async def test_approved_function_call_with_error_with_detailed_errors(chat_client_base: SupportsChatGetResponse): """Test that approved functions that raise errors return detailed error messages. When include_detailed_errors=True. @@ -1328,12 +1328,12 @@ async def test_approved_function_call_with_error_with_detailed_errors(chat_clien chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[Content.from_function_call(call_id="1", name="error_func", arguments='{"arg1": "value1"}')], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Set include_detailed_errors to True @@ -1351,7 +1351,7 @@ async def test_approved_function_call_with_error_with_detailed_errors(chat_clien approved=True, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])] + all_messages = response1.messages + [Message(role="user", contents=[approval_response])] # Execute the approved function (which will error) await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [error_func]}) @@ -1376,7 +1376,7 @@ async def test_approved_function_call_with_error_with_detailed_errors(chat_clien assert "Specific error from approved function" in error_result.result # Detail included -async def test_approved_function_call_with_validation_error(chat_client_base: ChatClientProtocol): +async def test_approved_function_call_with_validation_error(chat_client_base: SupportsChatGetResponse): """Test that approved functions with validation errors are handled correctly.""" exec_counter = 0 @@ -1389,14 +1389,14 @@ async def test_approved_function_call_with_validation_error(chat_client_base: Ch chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="typed_func", arguments='{"arg1": "not_an_int"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Set include_detailed_errors to True to see validation details @@ -1414,7 +1414,7 @@ async def test_approved_function_call_with_validation_error(chat_client_base: Ch approved=True, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])] + all_messages = response1.messages + [Message(role="user", contents=[approval_response])] # Execute the approved function (which will fail validation) await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [typed_func]}) @@ -1437,7 +1437,7 @@ async def test_approved_function_call_with_validation_error(chat_client_base: Ch assert "Argument parsing failed" in error_result.result -async def test_approved_function_call_successful_execution(chat_client_base: ChatClientProtocol): +async def test_approved_function_call_successful_execution(chat_client_base: SupportsChatGetResponse): """Test that approved functions execute successfully when no errors occur.""" exec_counter = 0 @@ -1450,12 +1450,12 @@ async def test_approved_function_call_successful_execution(chat_client_base: Cha chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[Content.from_function_call(call_id="1", name="success_func", arguments='{"arg1": "value1"}')], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Get approval request @@ -1470,7 +1470,7 @@ async def test_approved_function_call_successful_execution(chat_client_base: Cha approved=True, ) - all_messages = response1.messages + [ChatMessage(role="user", contents=[approval_response])] + all_messages = response1.messages + [Message(role="user", contents=[approval_response])] # Execute the approved function await chat_client_base.get_response(all_messages, options={"tool_choice": "auto", "tools": [success_func]}) @@ -1492,7 +1492,7 @@ async def test_approved_function_call_successful_execution(chat_client_base: Cha assert success_result.result == "Success value1" -async def test_declaration_only_tool(chat_client_base: ChatClientProtocol): +async def test_declaration_only_tool(chat_client_base: SupportsChatGetResponse): """Test that declaration_only tools without implementation (func=None) are not executed.""" from agent_framework import FunctionTool @@ -1509,14 +1509,14 @@ async def test_declaration_only_tool(chat_client_base: ChatClientProtocol): chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="declaration_func", arguments='{"arg1": "value1"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] response = await chat_client_base.get_response( @@ -1542,7 +1542,7 @@ async def test_declaration_only_tool(chat_client_base: ChatClientProtocol): assert len(function_results) == 0 -async def test_multiple_function_calls_parallel_execution(chat_client_base: ChatClientProtocol): +async def test_multiple_function_calls_parallel_execution(chat_client_base: SupportsChatGetResponse): """Test that multiple function calls are executed in parallel.""" import asyncio @@ -1564,7 +1564,7 @@ async def test_multiple_function_calls_parallel_execution(chat_client_base: Chat chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="func1", arguments='{"arg1": "value1"}'), @@ -1572,7 +1572,7 @@ async def test_multiple_function_calls_parallel_execution(chat_client_base: Chat ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [func1, func2]}) @@ -1588,7 +1588,7 @@ async def test_multiple_function_calls_parallel_execution(chat_client_base: Chat assert len(results) == 2 -async def test_callable_function_converted_to_tool(chat_client_base: ChatClientProtocol): +async def test_callable_function_converted_to_tool(chat_client_base: SupportsChatGetResponse): """Test that plain callable functions are converted to FunctionTool.""" exec_counter = 0 @@ -1601,14 +1601,14 @@ async def test_callable_function_converted_to_tool(chat_client_base: ChatClientP chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="plain_function", arguments='{"arg1": "value1"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] # Pass plain function (will be auto-converted) @@ -1620,7 +1620,7 @@ async def test_callable_function_converted_to_tool(chat_client_base: ChatClientP assert result.result == "Plain value1" -async def test_conversation_id_handling(chat_client_base: ChatClientProtocol): +async def test_conversation_id_handling(chat_client_base: SupportsChatGetResponse): """Test that conversation_id is properly handled and messages are cleared.""" @tool(name="test_function", approval_mode="never_require") @@ -1630,7 +1630,7 @@ async def test_conversation_id_handling(chat_client_base: ChatClientProtocol): # Return a response with a conversation_id chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="test_function", arguments='{"arg1": "value1"}') @@ -1639,7 +1639,7 @@ async def test_conversation_id_handling(chat_client_base: ChatClientProtocol): conversation_id="conv_123", # Simulate service-side thread ), ChatResponse( - messages=ChatMessage(role="assistant", text="done"), + messages=Message(role="assistant", text="done"), conversation_id="conv_123", ), ] @@ -1652,7 +1652,7 @@ async def test_conversation_id_handling(chat_client_base: ChatClientProtocol): assert response.conversation_id == "conv_123" -async def test_function_result_appended_to_existing_assistant_message(chat_client_base: ChatClientProtocol): +async def test_function_result_appended_to_existing_assistant_message(chat_client_base: SupportsChatGetResponse): """Test that function results are appended to existing assistant message when appropriate.""" @tool(name="test_function", approval_mode="never_require") @@ -1661,14 +1661,14 @@ async def test_function_result_appended_to_existing_assistant_message(chat_clien chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="test_function", arguments='{"arg1": "value1"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [test_func]}) @@ -1683,7 +1683,7 @@ async def test_function_result_appended_to_existing_assistant_message(chat_clien @pytest.mark.parametrize("max_iterations", [3]) -async def test_error_recovery_resets_counter(chat_client_base: ChatClientProtocol): +async def test_error_recovery_resets_counter(chat_client_base: SupportsChatGetResponse): """Test that error counter resets after a successful function call.""" call_count = 0 @@ -1698,7 +1698,7 @@ async def test_error_recovery_resets_counter(chat_client_base: ChatClientProtoco chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="sometimes_fails", arguments='{"arg1": "value1"}') @@ -1706,14 +1706,14 @@ async def test_error_recovery_resets_counter(chat_client_base: ChatClientProtoco ) ), ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="2", name="sometimes_fails", arguments='{"arg1": "value2"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [sometimes_fails]}) @@ -1740,7 +1740,7 @@ async def test_error_recovery_resets_counter(chat_client_base: ChatClientProtoco # ==================== STREAMING SCENARIO TESTS ==================== -async def test_streaming_approval_request_generated(chat_client_base: ChatClientProtocol): +async def test_streaming_approval_request_generated(chat_client_base: SupportsChatGetResponse): """Test that approval requests are generated correctly in streaming mode.""" exec_counter = 0 @@ -1777,7 +1777,7 @@ async def test_streaming_approval_request_generated(chat_client_base: ChatClient @pytest.mark.skip(reason="Failsafe behavior with max_iterations needs investigation in unified API") -async def test_streaming_max_iterations_limit(chat_client_base: ChatClientProtocol): +async def test_streaming_max_iterations_limit(chat_client_base: SupportsChatGetResponse): """Test that MAX_ITERATIONS in streaming mode limits function call loops.""" exec_counter = 0 @@ -1829,7 +1829,7 @@ async def test_streaming_max_iterations_limit(chat_client_base: ChatClientProtoc assert "I broke out of the function invocation loop..." in last_text -async def test_streaming_function_invocation_config_enabled_false(chat_client_base: ChatClientProtocol): +async def test_streaming_function_invocation_config_enabled_false(chat_client_base: SupportsChatGetResponse): """Test that setting enabled=False disables function invocation in streaming mode.""" exec_counter = 0 @@ -1858,7 +1858,7 @@ async def test_streaming_function_invocation_config_enabled_false(chat_client_ba assert len(updates) > 0 -async def test_streaming_function_invocation_config_max_consecutive_errors(chat_client_base: ChatClientProtocol): +async def test_streaming_function_invocation_config_max_consecutive_errors(chat_client_base: SupportsChatGetResponse): """Test that max_consecutive_errors_per_request limits error retries in streaming mode.""" @tool(name="error_function", approval_mode="never_require") @@ -1919,7 +1919,7 @@ async def test_streaming_function_invocation_config_max_consecutive_errors(chat_ async def test_streaming_function_invocation_config_terminate_on_unknown_calls_false( - chat_client_base: ChatClientProtocol, + chat_client_base: SupportsChatGetResponse, ): """Test that terminate_on_unknown_calls=False returns error message for unknown functions in streaming mode.""" exec_counter = 0 @@ -1963,7 +1963,7 @@ async def test_streaming_function_invocation_config_terminate_on_unknown_calls_f @pytest.mark.skip(reason="Failsafe behavior needs investigation in unified API") async def test_streaming_function_invocation_config_terminate_on_unknown_calls_true( - chat_client_base: ChatClientProtocol, + chat_client_base: SupportsChatGetResponse, ): """Test that terminate_on_unknown_calls=True stops execution on unknown functions in streaming mode.""" exec_counter = 0 @@ -1996,7 +1996,9 @@ async def test_streaming_function_invocation_config_terminate_on_unknown_calls_t assert exec_counter == 0 -async def test_streaming_function_invocation_config_include_detailed_errors_true(chat_client_base: ChatClientProtocol): +async def test_streaming_function_invocation_config_include_detailed_errors_true( + chat_client_base: SupportsChatGetResponse, +): """Test that include_detailed_errors=True returns detailed error information in streaming mode.""" @tool(name="error_function", approval_mode="never_require") @@ -2035,7 +2037,7 @@ async def test_streaming_function_invocation_config_include_detailed_errors_true async def test_streaming_function_invocation_config_include_detailed_errors_false( - chat_client_base: ChatClientProtocol, + chat_client_base: SupportsChatGetResponse, ): """Test that include_detailed_errors=False returns generic error messages in streaming mode.""" @@ -2074,7 +2076,7 @@ async def test_streaming_function_invocation_config_include_detailed_errors_fals assert "Error:" in error_result.result # Generic error prefix -async def test_streaming_argument_validation_error_with_detailed_errors(chat_client_base: ChatClientProtocol): +async def test_streaming_argument_validation_error_with_detailed_errors(chat_client_base: SupportsChatGetResponse): """Test that argument validation errors include details when include_detailed_errors=True in streaming mode.""" @tool(name="typed_function", approval_mode="never_require") @@ -2112,7 +2114,7 @@ async def test_streaming_argument_validation_error_with_detailed_errors(chat_cli assert "Exception:" in error_result.result # Detailed error included -async def test_streaming_argument_validation_error_without_detailed_errors(chat_client_base: ChatClientProtocol): +async def test_streaming_argument_validation_error_without_detailed_errors(chat_client_base: SupportsChatGetResponse): """Test that argument validation errors are generic when include_detailed_errors=False in streaming mode.""" @tool(name="typed_function", approval_mode="never_require") @@ -2150,7 +2152,7 @@ async def test_streaming_argument_validation_error_without_detailed_errors(chat_ assert "Exception:" not in error_result.result # No detailed error -async def test_streaming_multiple_function_calls_parallel_execution(chat_client_base: ChatClientProtocol): +async def test_streaming_multiple_function_calls_parallel_execution(chat_client_base: SupportsChatGetResponse): """Test that multiple function calls are executed in parallel in streaming mode.""" exec_order = [] @@ -2200,7 +2202,7 @@ async def test_streaming_multiple_function_calls_parallel_execution(chat_client_ assert len(results) == 2 -async def test_streaming_approval_requests_in_assistant_message(chat_client_base: ChatClientProtocol): +async def test_streaming_approval_requests_in_assistant_message(chat_client_base: SupportsChatGetResponse): """Approval requests should be added to assistant updates in streaming mode.""" exec_counter = 0 @@ -2235,7 +2237,7 @@ async def test_streaming_approval_requests_in_assistant_message(chat_client_base assert exec_counter == 0 -async def test_streaming_error_recovery_resets_counter(chat_client_base: ChatClientProtocol): +async def test_streaming_error_recovery_resets_counter(chat_client_base: SupportsChatGetResponse): """Test that error counter resets after a successful function call in streaming mode.""" call_count = 0 @@ -2304,7 +2306,7 @@ class TerminateLoopMiddleware(FunctionMiddleware): raise MiddlewareTermination -async def test_terminate_loop_single_function_call(chat_client_base: ChatClientProtocol): +async def test_terminate_loop_single_function_call(chat_client_base: SupportsChatGetResponse): """Test that terminate_loop=True exits the function calling loop after single function call.""" exec_counter = 0 @@ -2318,14 +2320,14 @@ async def test_terminate_loop_single_function_call(chat_client_base: ChatClientP # If terminate_loop works, only the first response should be consumed chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="test_function", arguments='{"arg1": "value1"}') ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] response = await chat_client_base.get_response( @@ -2363,7 +2365,7 @@ class SelectiveTerminateMiddleware(FunctionMiddleware): await next_handler(context) -async def test_terminate_loop_multiple_function_calls_one_terminates(chat_client_base: ChatClientProtocol): +async def test_terminate_loop_multiple_function_calls_one_terminates(chat_client_base: SupportsChatGetResponse): """Test that any(terminate_loop=True) exits loop even with multiple function calls.""" normal_call_count = 0 terminating_call_count = 0 @@ -2383,7 +2385,7 @@ async def test_terminate_loop_multiple_function_calls_one_terminates(chat_client # Queue up two responses: parallel function calls, then final text chat_client_base.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[ Content.from_function_call(call_id="1", name="normal_function", arguments='{"arg1": "value1"}'), @@ -2393,7 +2395,7 @@ async def test_terminate_loop_multiple_function_calls_one_terminates(chat_client ], ) ), - ChatResponse(messages=ChatMessage(role="assistant", text="done")), + ChatResponse(messages=Message(role="assistant", text="done")), ] response = await chat_client_base.get_response( @@ -2420,7 +2422,7 @@ async def test_terminate_loop_multiple_function_calls_one_terminates(chat_client assert len(chat_client_base.run_responses) == 1 -async def test_terminate_loop_streaming_single_function_call(chat_client_base: ChatClientProtocol): +async def test_terminate_loop_streaming_single_function_call(chat_client_base: SupportsChatGetResponse): """Test that terminate_loop=True exits the streaming function calling loop.""" exec_counter = 0 @@ -2482,10 +2484,10 @@ async def test_conversation_id_updated_in_options_between_tool_iterations(): from agent_framework import ( BaseChatClient, - ChatMessage, ChatResponse, ChatResponseUpdate, Content, + Message, ResponseStream, tool, ) @@ -2509,7 +2511,7 @@ async def test_conversation_id_updated_in_options_between_tool_iterations(): def _inner_get_response( self, *, - messages: MutableSequence[ChatMessage], + messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any, @@ -2523,7 +2525,7 @@ async def test_conversation_id_updated_in_options_between_tool_iterations(): async def _get() -> ChatResponse: self.call_count += 1 if not self.run_responses: - return ChatResponse(messages=ChatMessage(role="assistant", text="done")) + return ChatResponse(messages=Message(role="assistant", text="done")) return self.run_responses.pop(0) return _get() @@ -2531,7 +2533,7 @@ async def test_conversation_id_updated_in_options_between_tool_iterations(): def _get_streaming_response( self, *, - messages: MutableSequence[ChatMessage], + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any, ) -> ResponseStream[ChatResponseUpdate, ChatResponse]: @@ -2563,14 +2565,14 @@ async def test_conversation_id_updated_in_options_between_tool_iterations(): # Second response (after tool execution) should receive the updated conversation_id client.run_responses = [ ChatResponse( - messages=ChatMessage( + messages=Message( role="assistant", contents=[Content.from_function_call(call_id="call_1", name="test_func", arguments='{"arg1": "v1"}')], ), conversation_id="conv_after_first_call", ), ChatResponse( - messages=ChatMessage(role="assistant", text="done"), + messages=Message(role="assistant", text="done"), conversation_id="conv_after_second_call", ), ] diff --git a/python/packages/core/tests/core/test_kwargs_propagation_to_ai_function.py b/python/packages/core/tests/core/test_kwargs_propagation_to_ai_function.py index cbbd4b69f7..cecd466d86 100644 --- a/python/packages/core/tests/core/test_kwargs_propagation_to_ai_function.py +++ b/python/packages/core/tests/core/test_kwargs_propagation_to_ai_function.py @@ -7,12 +7,12 @@ from typing import Any from agent_framework import ( BaseChatClient, - ChatMessage, ChatMiddlewareLayer, ChatResponse, ChatResponseUpdate, Content, FunctionInvocationLayer, + Message, ResponseStream, tool, ) @@ -31,7 +31,7 @@ class _MockBaseChatClient(BaseChatClient[Any]): def _inner_get_response( self, *, - messages: MutableSequence[ChatMessage], + messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any, @@ -47,19 +47,19 @@ class _MockBaseChatClient(BaseChatClient[Any]): async def _get_non_streaming_response( self, *, - messages: MutableSequence[ChatMessage], + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any, ) -> ChatResponse: self.call_count += 1 if self.run_responses: return self.run_responses.pop(0) - return ChatResponse(messages=ChatMessage(role="assistant", text="default response")) + return ChatResponse(messages=Message(role="assistant", text="default response")) def _get_streaming_response( self, *, - messages: MutableSequence[ChatMessage], + messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any, ) -> ResponseStream[ChatResponseUpdate, ChatResponse]: @@ -110,7 +110,7 @@ class TestKwargsPropagationToFunctionTool: # First response: function call ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -121,11 +121,11 @@ class TestKwargsPropagationToFunctionTool: ] ), # Second response: final answer - ChatResponse(messages=[ChatMessage(role="assistant", text="Done!")]), + ChatResponse(messages=[Message(role="assistant", text="Done!")]), ] result = await client.get_response( - messages=[ChatMessage(role="user", text="Test")], + messages=[Message(role="user", text="Test")], stream=False, options={ "tools": [capture_kwargs_tool], @@ -159,7 +159,7 @@ class TestKwargsPropagationToFunctionTool: client.run_responses = [ ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call(call_id="call_1", name="simple_tool", arguments='{"x": 99}') @@ -167,12 +167,12 @@ class TestKwargsPropagationToFunctionTool: ) ] ), - ChatResponse(messages=[ChatMessage(role="assistant", text="Completed!")]), + ChatResponse(messages=[Message(role="assistant", text="Completed!")]), ] # Call with additional_function_arguments - the tool should work but not receive them result = await client.get_response( - messages=[ChatMessage(role="user", text="Test")], + messages=[Message(role="user", text="Test")], stream=False, options={ "tools": [simple_tool], @@ -198,7 +198,7 @@ class TestKwargsPropagationToFunctionTool: # Two function calls in one response ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -211,11 +211,11 @@ class TestKwargsPropagationToFunctionTool: ) ] ), - ChatResponse(messages=[ChatMessage(role="assistant", text="All done!")]), + ChatResponse(messages=[Message(role="assistant", text="All done!")]), ] result = await client.get_response( - messages=[ChatMessage(role="user", text="Test")], + messages=[Message(role="user", text="Test")], stream=False, options={ "tools": [tracking_tool], @@ -270,7 +270,7 @@ class TestKwargsPropagationToFunctionTool: # Collect streaming updates updates: list[ChatResponseUpdate] = [] stream = client.get_response( - messages=[ChatMessage(role="user", text="Test")], + messages=[Message(role="user", text="Test")], stream=True, options={ "tools": [streaming_capture_tool], diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 7695affb5a..f3775a4f0a 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -13,12 +13,11 @@ from mcp.shared.exceptions import McpError from pydantic import AnyUrl, BaseModel, ValidationError from agent_framework import ( - ChatMessage, Content, MCPStdioTool, MCPStreamableHTTPTool, MCPWebsocketTool, - ToolProtocol, + Message, ) from agent_framework._mcp import ( MCPTool, @@ -61,7 +60,7 @@ def test_mcp_prompt_message_to_ai_content(): mcp_message = types.PromptMessage(role="user", content=types.TextContent(type="text", text="Hello, world!")) ai_content = _parse_message_from_mcp(mcp_message) - assert isinstance(ai_content, ChatMessage) + assert isinstance(ai_content, Message) assert ai_content.role == "user" assert len(ai_content.contents) == 1 assert ai_content.contents[0].type == "text" @@ -349,7 +348,7 @@ def test_ai_content_to_mcp_content_types_uri(): def test_prepare_message_for_mcp(): - message = ChatMessage( + message = Message( role="user", contents=[ Content.from_text(text="test"), @@ -744,7 +743,10 @@ def test_get_input_model_from_mcp_prompt(): async def test_local_mcp_server_initialization(): """Test MCPTool initialization.""" server = MCPTool(name="test_server") - assert isinstance(server, ToolProtocol) + # MCPTool has the same core attributes as FunctionTool + assert hasattr(server, "name") + assert hasattr(server, "description") + assert hasattr(server, "additional_properties") assert server.name == "test_server" assert server.session is None assert server.functions == [] @@ -795,7 +797,9 @@ async def test_local_mcp_server_load_functions(): return None server = TestServer(name="test_server") - assert isinstance(server, ToolProtocol) + # MCPTool has the same core attributes as FunctionTool + assert hasattr(server, "name") + assert hasattr(server, "description") async with server: await server.load_tools() assert len(server.functions) == 1 @@ -1054,7 +1058,7 @@ async def test_local_mcp_server_prompt_execution(): result = await prompt.invoke(arg="test_value") assert len(result) == 1 - assert isinstance(result[0], ChatMessage) + assert isinstance(result[0], Message) assert result[0].role == "user" assert len(result[0].contents) == 1 assert result[0].contents[0].text == "Test message" @@ -1391,7 +1395,7 @@ async def test_mcp_tool_sampling_callback_chat_client_exception(): mock_chat_client = AsyncMock() mock_chat_client.get_response.side_effect = RuntimeError("Chat client error") - tool.chat_client = mock_chat_client + tool.client = mock_chat_client # Create mock params params = Mock() @@ -1413,7 +1417,7 @@ async def test_mcp_tool_sampling_callback_chat_client_exception(): async def test_mcp_tool_sampling_callback_no_valid_content(): """Test sampling callback when response has no valid content types.""" - from agent_framework import ChatMessage + from agent_framework import Message tool = MCPStdioTool(name="test_tool", command="python") @@ -1421,7 +1425,7 @@ async def test_mcp_tool_sampling_callback_no_valid_content(): mock_chat_client = AsyncMock() mock_response = Mock() mock_response.messages = [ - ChatMessage( + Message( role="assistant", contents=[ Content.from_uri( @@ -1434,7 +1438,7 @@ async def test_mcp_tool_sampling_callback_no_valid_content(): mock_response.model_id = "test-model" mock_chat_client.get_response.return_value = mock_response - tool.chat_client = mock_chat_client + tool.client = mock_chat_client # Create mock params params = Mock() diff --git a/python/packages/core/tests/core/test_memory.py b/python/packages/core/tests/core/test_memory.py index ca28a01e8c..bd83933e54 100644 --- a/python/packages/core/tests/core/test_memory.py +++ b/python/packages/core/tests/core/test_memory.py @@ -4,14 +4,14 @@ import sys from collections.abc import MutableSequence from typing import Any -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework._memory import Context, ContextProvider class MockContextProvider(ContextProvider): """Mock ContextProvider for testing.""" - def __init__(self, messages: list[ChatMessage] | None = None) -> None: + def __init__(self, messages: list[Message] | None = None) -> None: self.context_messages = messages self.thread_created_called = False self.invoked_called = False @@ -36,7 +36,7 @@ class MockContextProvider(ContextProvider): self.invoked_called = True self.new_messages = request_messages - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: """Track invoking calls and return context.""" self.invoking_called = True self.model_invoking_messages = messages @@ -52,7 +52,7 @@ class MinimalContextProvider(ContextProvider): invoked, __aenter__, and __aexit__. """ - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: """Return empty context.""" return Context() @@ -69,7 +69,7 @@ class TestContext: def test_context_with_values(self) -> None: """Test Context can be initialized with values.""" - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] context = Context(instructions="Test instructions", messages=messages) assert context.instructions == "Test instructions" assert len(context.messages) == 1 @@ -89,15 +89,15 @@ class TestContextProvider: async def test_invoked(self) -> None: """Test invoked is called.""" provider = MockContextProvider() - message = ChatMessage(role="user", text="Test message") + message = Message(role="user", text="Test message") await provider.invoked(message) assert provider.invoked_called assert provider.new_messages == message async def test_invoking(self) -> None: """Test invoking is called and returns context.""" - provider = MockContextProvider(messages=[ChatMessage(role="user", text="Context message")]) - message = ChatMessage(role="user", text="Test message") + provider = MockContextProvider(messages=[Message(role="user", text="Context message")]) + message = Message(role="user", text="Test message") context = await provider.invoking(message) assert provider.invoking_called assert provider.model_invoking_messages == message @@ -114,7 +114,7 @@ class TestContextProvider: async def test_base_invoked_does_nothing(self) -> None: """Test that base ContextProvider.invoked does nothing by default.""" provider = MinimalContextProvider() - message = ChatMessage(role="user", text="Test") + message = Message(role="user", text="Test") await provider.invoked(message) await provider.invoked(message, response_messages=message) await provider.invoked(message, invoke_exception=Exception("test")) diff --git a/python/packages/core/tests/core/test_middleware.py b/python/packages/core/tests/core/test_middleware.py index ae84541df4..41c15b2c70 100644 --- a/python/packages/core/tests/core/test_middleware.py +++ b/python/packages/core/tests/core/test_middleware.py @@ -10,10 +10,10 @@ from pydantic import BaseModel, Field from agent_framework import ( AgentResponse, AgentResponseUpdate, - ChatMessage, ChatResponse, ChatResponseUpdate, Content, + Message, ResponseStream, SupportsAgentRun, ) @@ -37,7 +37,7 @@ class TestAgentContext: def test_init_with_defaults(self, mock_agent: SupportsAgentRun) -> None: """Test AgentContext initialization with default values.""" - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) assert context.agent is mock_agent @@ -47,7 +47,7 @@ class TestAgentContext: def test_init_with_custom_values(self, mock_agent: SupportsAgentRun) -> None: """Test AgentContext initialization with custom values.""" - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] metadata = {"key": "value"} context = AgentContext(agent=mock_agent, messages=messages, stream=True, metadata=metadata) @@ -60,7 +60,7 @@ class TestAgentContext: """Test AgentContext initialization with thread parameter.""" from agent_framework import AgentThread - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] thread = AgentThread() context = AgentContext(agent=mock_agent, messages=messages, thread=thread) @@ -99,11 +99,11 @@ class TestChatContext: def test_init_with_defaults(self, mock_chat_client: Any) -> None: """Test ChatContext initialization with default values.""" - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) - assert context.chat_client is mock_chat_client + assert context.client is mock_chat_client assert context.messages == messages assert context.options is chat_options assert context.stream is False @@ -112,19 +112,19 @@ class TestChatContext: def test_init_with_custom_values(self, mock_chat_client: Any) -> None: """Test ChatContext initialization with custom values.""" - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {"temperature": 0.5} metadata = {"key": "value"} context = ChatContext( - chat_client=mock_chat_client, + client=mock_chat_client, messages=messages, options=chat_options, stream=True, metadata=metadata, ) - assert context.chat_client is mock_chat_client + assert context.client is mock_chat_client assert context.messages == messages assert context.options is chat_options assert context.stream is True @@ -166,10 +166,10 @@ class TestAgentMiddlewarePipeline: async def test_execute_no_middleware(self, mock_agent: SupportsAgentRun) -> None: """Test pipeline execution with no middleware.""" pipeline = AgentMiddlewarePipeline() - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) - expected_response = AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + expected_response = AgentResponse(messages=[Message(role="assistant", text="response")]) async def final_handler(ctx: AgentContext) -> AgentResponse: return expected_response @@ -194,10 +194,10 @@ class TestAgentMiddlewarePipeline: middleware = OrderTrackingMiddleware("test") pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) - expected_response = AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + expected_response = AgentResponse(messages=[Message(role="assistant", text="response")]) async def final_handler(ctx: AgentContext) -> AgentResponse: execution_order.append("handler") @@ -210,7 +210,7 @@ class TestAgentMiddlewarePipeline: async def test_execute_stream_no_middleware(self, mock_agent: SupportsAgentRun) -> None: """Test pipeline streaming execution with no middleware.""" pipeline = AgentMiddlewarePipeline() - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages, stream=True) async def final_handler(ctx: AgentContext) -> ResponseStream[AgentResponseUpdate, AgentResponse]: @@ -247,7 +247,7 @@ class TestAgentMiddlewarePipeline: middleware = StreamOrderTrackingMiddleware("test") pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages, stream=True) async def final_handler(ctx: AgentContext) -> ResponseStream[AgentResponseUpdate, AgentResponse]: @@ -273,14 +273,14 @@ class TestAgentMiddlewarePipeline: """Test pipeline execution with termination before next().""" middleware = self.PreNextTerminateMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) execution_order: list[str] = [] async def final_handler(ctx: AgentContext) -> AgentResponse: # Handler should not be executed when terminated before next() execution_order.append("handler") - return AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + return AgentResponse(messages=[Message(role="assistant", text="response")]) response = await pipeline.execute(context, final_handler) assert response is None @@ -291,13 +291,13 @@ class TestAgentMiddlewarePipeline: """Test pipeline execution with termination after next().""" middleware = self.PostNextTerminateMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) execution_order: list[str] = [] async def final_handler(ctx: AgentContext) -> AgentResponse: execution_order.append("handler") - return AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + return AgentResponse(messages=[Message(role="assistant", text="response")]) response = await pipeline.execute(context, final_handler) assert response is not None @@ -309,7 +309,7 @@ class TestAgentMiddlewarePipeline: """Test pipeline streaming execution with termination before next().""" middleware = self.PreNextTerminateMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages, stream=True) execution_order: list[str] = [] @@ -337,7 +337,7 @@ class TestAgentMiddlewarePipeline: """Test pipeline streaming execution with termination after next().""" middleware = self.PostNextTerminateMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages, stream=True) execution_order: list[str] = [] @@ -376,11 +376,11 @@ class TestAgentMiddlewarePipeline: middleware = ThreadCapturingMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] thread = AgentThread() context = AgentContext(agent=mock_agent, messages=messages, thread=thread) - expected_response = AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + expected_response = AgentResponse(messages=[Message(role="assistant", text="response")]) async def final_handler(ctx: AgentContext) -> AgentResponse: return expected_response @@ -403,10 +403,10 @@ class TestAgentMiddlewarePipeline: middleware = ThreadCapturingMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages, thread=None) - expected_response = AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + expected_response = AgentResponse(messages=[Message(role="assistant", text="response")]) async def final_handler(ctx: AgentContext) -> AgentResponse: return expected_response @@ -572,11 +572,11 @@ class TestChatMiddlewarePipeline: async def test_execute_no_middleware(self, mock_chat_client: Any) -> None: """Test pipeline execution with no middleware.""" pipeline = ChatMiddlewarePipeline() - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) - expected_response = ChatResponse(messages=[ChatMessage(role="assistant", text="response")]) + expected_response = ChatResponse(messages=[Message(role="assistant", text="response")]) async def final_handler(ctx: ChatContext) -> ChatResponse: return expected_response @@ -599,11 +599,11 @@ class TestChatMiddlewarePipeline: middleware = OrderTrackingChatMiddleware("test") pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) - expected_response = ChatResponse(messages=[ChatMessage(role="assistant", text="response")]) + expected_response = ChatResponse(messages=[Message(role="assistant", text="response")]) async def final_handler(ctx: ChatContext) -> ChatResponse: execution_order.append("handler") @@ -616,9 +616,9 @@ class TestChatMiddlewarePipeline: async def test_execute_stream_no_middleware(self, mock_chat_client: Any) -> None: """Test pipeline streaming execution with no middleware.""" pipeline = ChatMiddlewarePipeline() - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, stream=True) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options, stream=True) def final_handler(ctx: ChatContext) -> ResponseStream[ChatResponseUpdate, ChatResponse]: async def _stream() -> AsyncIterable[ChatResponseUpdate]: @@ -651,9 +651,9 @@ class TestChatMiddlewarePipeline: middleware = StreamOrderTrackingChatMiddleware("test") pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, stream=True) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options, stream=True) def final_handler(ctx: ChatContext) -> ResponseStream[ChatResponseUpdate, ChatResponse]: async def _stream() -> AsyncIterable[ChatResponseUpdate]: @@ -678,15 +678,15 @@ class TestChatMiddlewarePipeline: """Test pipeline execution with termination before next().""" middleware = self.PreNextTerminateChatMiddleware() pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) execution_order: list[str] = [] async def final_handler(ctx: ChatContext) -> ChatResponse: # Handler should not be executed when terminated before next() execution_order.append("handler") - return ChatResponse(messages=[ChatMessage(role="assistant", text="response")]) + return ChatResponse(messages=[Message(role="assistant", text="response")]) response = await pipeline.execute(context, final_handler) assert response is None @@ -697,14 +697,14 @@ class TestChatMiddlewarePipeline: """Test pipeline execution with termination after next().""" middleware = self.PostNextTerminateChatMiddleware() pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) execution_order: list[str] = [] async def final_handler(ctx: ChatContext) -> ChatResponse: execution_order.append("handler") - return ChatResponse(messages=[ChatMessage(role="assistant", text="response")]) + return ChatResponse(messages=[Message(role="assistant", text="response")]) response = await pipeline.execute(context, final_handler) assert response is not None @@ -716,9 +716,9 @@ class TestChatMiddlewarePipeline: """Test pipeline streaming execution with termination before next().""" middleware = self.PreNextTerminateChatMiddleware() pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, stream=True) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options, stream=True) execution_order: list[str] = [] def final_handler(ctx: ChatContext) -> ResponseStream[ChatResponseUpdate, ChatResponse]: @@ -741,9 +741,9 @@ class TestChatMiddlewarePipeline: """Test pipeline streaming execution with termination after next().""" middleware = self.PostNextTerminateChatMiddleware() pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, stream=True) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options, stream=True) execution_order: list[str] = [] def final_handler(ctx: ChatContext) -> ResponseStream[ChatResponseUpdate, ChatResponse]: @@ -785,12 +785,12 @@ class TestClassBasedMiddleware: middleware = MetadataAgentMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentContext) -> AgentResponse: metadata_updates.append("handler") - return AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + return AgentResponse(messages=[Message(role="assistant", text="response")]) result = await pipeline.execute(context, final_handler) @@ -848,12 +848,12 @@ class TestFunctionBasedMiddleware: execution_order.append("function_after") pipeline = AgentMiddlewarePipeline(test_agent_middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentContext) -> AgentResponse: execution_order.append("handler") - return AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + return AgentResponse(messages=[Message(role="assistant", text="response")]) result = await pipeline.execute(context, final_handler) @@ -911,12 +911,12 @@ class TestMixedMiddleware: execution_order.append("function_after") pipeline = AgentMiddlewarePipeline(ClassMiddleware(), function_middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentContext) -> AgentResponse: execution_order.append("handler") - return AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + return AgentResponse(messages=[Message(role="assistant", text="response")]) result = await pipeline.execute(context, final_handler) @@ -975,13 +975,13 @@ class TestMixedMiddleware: execution_order.append("function_after") pipeline = ChatMiddlewarePipeline(ClassChatMiddleware(), function_chat_middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) async def final_handler(ctx: ChatContext) -> ChatResponse: execution_order.append("handler") - return ChatResponse(messages=[ChatMessage(role="assistant", text="response")]) + return ChatResponse(messages=[Message(role="assistant", text="response")]) result = await pipeline.execute(context, final_handler) @@ -1022,12 +1022,12 @@ class TestMultipleMiddlewareOrdering: middleware = [FirstMiddleware(), SecondMiddleware(), ThirdMiddleware()] pipeline = AgentMiddlewarePipeline(*middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentContext) -> AgentResponse: execution_order.append("handler") - return AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + return AgentResponse(messages=[Message(role="assistant", text="response")]) result = await pipeline.execute(context, final_handler) @@ -1106,13 +1106,13 @@ class TestMultipleMiddlewareOrdering: middleware = [FirstChatMiddleware(), SecondChatMiddleware(), ThirdChatMiddleware()] pipeline = ChatMiddlewarePipeline(*middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) async def final_handler(ctx: ChatContext) -> ChatResponse: execution_order.append("handler") - return ChatResponse(messages=[ChatMessage(role="assistant", text="response")]) + return ChatResponse(messages=[Message(role="assistant", text="response")]) result = await pipeline.execute(context, final_handler) @@ -1160,13 +1160,13 @@ class TestContextContentValidation: middleware = ContextValidationMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentContext) -> AgentResponse: # Verify metadata was set by middleware assert ctx.metadata.get("validated") is True - return AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + return AgentResponse(messages=[Message(role="assistant", text="response")]) result = await pipeline.execute(context, final_handler) assert result is not None @@ -1215,7 +1215,7 @@ class TestContextContentValidation: class ChatContextValidationMiddleware(ChatMiddleware): async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None: # Verify context has all expected attributes - assert hasattr(context, "chat_client") + assert hasattr(context, "client") assert hasattr(context, "messages") assert hasattr(context, "options") assert hasattr(context, "stream") @@ -1223,7 +1223,7 @@ class TestContextContentValidation: assert hasattr(context, "result") # Verify context content - assert context.chat_client is mock_chat_client + assert context.client is mock_chat_client assert len(context.messages) == 1 assert context.messages[0].role == "user" assert context.messages[0].text == "test" @@ -1239,14 +1239,14 @@ class TestContextContentValidation: middleware = ChatContextValidationMiddleware() pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {"temperature": 0.5} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) async def final_handler(ctx: ChatContext) -> ChatResponse: # Verify metadata was set by middleware assert ctx.metadata.get("validated") is True - return ChatResponse(messages=[ChatMessage(role="assistant", text="response")]) + return ChatResponse(messages=[Message(role="assistant", text="response")]) result = await pipeline.execute(context, final_handler) assert result is not None @@ -1268,14 +1268,14 @@ class TestStreamingScenarios: middleware = StreamingFlagMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] # Test non-streaming context = AgentContext(agent=mock_agent, messages=messages) async def final_handler(ctx: AgentContext) -> AgentResponse: streaming_flags.append(ctx.stream) - return AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) + return AgentResponse(messages=[Message(role="assistant", text="response")]) await pipeline.execute(context, final_handler) @@ -1311,7 +1311,7 @@ class TestStreamingScenarios: middleware = StreamProcessingMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages, stream=True) async def final_stream_handler(ctx: AgentContext) -> ResponseStream[AgentResponseUpdate, AgentResponse]: @@ -1351,20 +1351,20 @@ class TestStreamingScenarios: middleware = ChatStreamingFlagMiddleware() pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} # Test non-streaming - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) async def final_handler(ctx: ChatContext) -> ChatResponse: streaming_flags.append(ctx.stream) - return ChatResponse(messages=[ChatMessage(role="assistant", text="response")]) + return ChatResponse(messages=[Message(role="assistant", text="response")]) await pipeline.execute(context, final_handler) # Test streaming - context_stream = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, stream=True) + context_stream = ChatContext(client=mock_chat_client, messages=messages, options=chat_options, stream=True) def final_stream_handler(ctx: ChatContext) -> ResponseStream[ChatResponseUpdate, ChatResponse]: async def _stream() -> AsyncIterable[ChatResponseUpdate]: @@ -1393,9 +1393,9 @@ class TestStreamingScenarios: middleware = ChatStreamProcessingMiddleware() pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, stream=True) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options, stream=True) def final_stream_handler(ctx: ChatContext) -> ResponseStream[ChatResponseUpdate, ChatResponse]: async def _stream() -> AsyncIterable[ChatResponseUpdate]: @@ -1477,7 +1477,7 @@ class TestMiddlewareExecutionControl: middleware = NoNextMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) handler_called = False @@ -1485,7 +1485,7 @@ class TestMiddlewareExecutionControl: async def final_handler(ctx: AgentContext) -> AgentResponse: nonlocal handler_called handler_called = True - return AgentResponse(messages=[ChatMessage(role="assistant", text="should not execute")]) + return AgentResponse(messages=[Message(role="assistant", text="should not execute")]) result = await pipeline.execute(context, final_handler) @@ -1506,7 +1506,7 @@ class TestMiddlewareExecutionControl: middleware = NoNextStreamingMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages, stream=True) handler_called = False @@ -1580,7 +1580,7 @@ class TestMiddlewareExecutionControl: await call_next(context) pipeline = AgentMiddlewarePipeline(FirstMiddleware(), SecondMiddleware()) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) handler_called = False @@ -1588,7 +1588,7 @@ class TestMiddlewareExecutionControl: async def final_handler(ctx: AgentContext) -> AgentResponse: nonlocal handler_called handler_called = True - return AgentResponse(messages=[ChatMessage(role="assistant", text="should not execute")]) + return AgentResponse(messages=[Message(role="assistant", text="should not execute")]) result = await pipeline.execute(context, final_handler) @@ -1607,16 +1607,16 @@ class TestMiddlewareExecutionControl: middleware = NoNextChatMiddleware() pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) handler_called = False async def final_handler(ctx: ChatContext) -> ChatResponse: nonlocal handler_called handler_called = True - return ChatResponse(messages=[ChatMessage(role="assistant", text="should not execute")]) + return ChatResponse(messages=[Message(role="assistant", text="should not execute")]) result = await pipeline.execute(context, final_handler) @@ -1635,9 +1635,9 @@ class TestMiddlewareExecutionControl: middleware = NoNextStreamingChatMiddleware() pipeline = ChatMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options, stream=True) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options, stream=True) handler_called = False @@ -1680,16 +1680,16 @@ class TestMiddlewareExecutionControl: await call_next(context) pipeline = ChatMiddlewarePipeline(FirstChatMiddleware(), SecondChatMiddleware()) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] chat_options: dict[str, Any] = {} - context = ChatContext(chat_client=mock_chat_client, messages=messages, options=chat_options) + context = ChatContext(client=mock_chat_client, messages=messages, options=chat_options) handler_called = False async def final_handler(ctx: ChatContext) -> ChatResponse: nonlocal handler_called handler_called = True - return ChatResponse(messages=[ChatMessage(role="assistant", text="should not execute")]) + return ChatResponse(messages=[Message(role="assistant", text="should not execute")]) result = await pipeline.execute(context, final_handler) @@ -1718,8 +1718,8 @@ def mock_function() -> FunctionTool[Any, Any]: @pytest.fixture def mock_chat_client() -> Any: """Mock chat client for testing.""" - from agent_framework import ChatClientProtocol + from agent_framework import SupportsChatGetResponse - client = MagicMock(spec=ChatClientProtocol) + client = MagicMock(spec=SupportsChatGetResponse) client.service_url = MagicMock(return_value="mock://test") return client diff --git a/python/packages/core/tests/core/test_middleware_context_result.py b/python/packages/core/tests/core/test_middleware_context_result.py index abdea790df..d17e99a85e 100644 --- a/python/packages/core/tests/core/test_middleware_context_result.py +++ b/python/packages/core/tests/core/test_middleware_context_result.py @@ -8,11 +8,11 @@ import pytest from pydantic import BaseModel, Field from agent_framework import ( + Agent, AgentResponse, AgentResponseUpdate, - ChatAgent, - ChatMessage, Content, + Message, ResponseStream, SupportsAgentRun, ) @@ -40,7 +40,7 @@ class TestResultOverrideMiddleware: async def test_agent_middleware_response_override_non_streaming(self, mock_agent: SupportsAgentRun) -> None: """Test that agent middleware can override response for non-streaming execution.""" - override_response = AgentResponse(messages=[ChatMessage(role="assistant", text="overridden response")]) + override_response = AgentResponse(messages=[Message(role="assistant", text="overridden response")]) class ResponseOverrideMiddleware(AgentMiddleware): async def process( @@ -52,7 +52,7 @@ class TestResultOverrideMiddleware: middleware = ResponseOverrideMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages) handler_called = False @@ -60,7 +60,7 @@ class TestResultOverrideMiddleware: async def final_handler(ctx: AgentContext) -> AgentResponse: nonlocal handler_called handler_called = True - return AgentResponse(messages=[ChatMessage(role="assistant", text="original response")]) + return AgentResponse(messages=[Message(role="assistant", text="original response")]) result = await pipeline.execute(context, final_handler) @@ -88,7 +88,7 @@ class TestResultOverrideMiddleware: middleware = StreamResponseOverrideMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages, stream=True) async def final_handler(ctx: AgentContext) -> ResponseStream[AgentResponseUpdate, AgentResponse]: @@ -141,7 +141,7 @@ class TestResultOverrideMiddleware: assert handler_called async def test_chat_agent_middleware_response_override(self) -> None: - """Test result override functionality with ChatAgent integration.""" + """Test result override functionality with Agent integration.""" mock_chat_client = MockChatClient() class ChatAgentResponseOverrideMiddleware(AgentMiddleware): @@ -153,29 +153,29 @@ class TestResultOverrideMiddleware: # Then conditionally override based on content if any("special" in msg.text for msg in context.messages if msg.text): context.result = AgentResponse( - messages=[ChatMessage(role="assistant", text="Special response from middleware!")] + messages=[Message(role="assistant", text="Special response from middleware!")] ) - # Create ChatAgent with override middleware + # Create Agent with override middleware middleware = ChatAgentResponseOverrideMiddleware() - agent = ChatAgent(chat_client=mock_chat_client, middleware=[middleware]) + agent = Agent(client=mock_chat_client, middleware=[middleware]) # Test override case - override_messages = [ChatMessage(role="user", text="Give me a special response")] + override_messages = [Message(role="user", text="Give me a special response")] override_response = await agent.run(override_messages) assert override_response.messages[0].text == "Special response from middleware!" # Verify chat client was called since middleware called next() assert mock_chat_client.call_count == 1 # Test normal case - normal_messages = [ChatMessage(role="user", text="Normal request")] + normal_messages = [Message(role="user", text="Normal request")] normal_response = await agent.run(normal_messages) assert normal_response.messages[0].text == "test response" # Verify chat client was called for normal case assert mock_chat_client.call_count == 2 async def test_chat_agent_middleware_streaming_override(self) -> None: - """Test streaming result override functionality with ChatAgent integration.""" + """Test streaming result override functionality with Agent integration.""" mock_chat_client = MockChatClient() async def custom_stream() -> AsyncIterable[AgentResponseUpdate]: @@ -194,12 +194,12 @@ class TestResultOverrideMiddleware: # Normal case - let the agent handle it await call_next(context) - # Create ChatAgent with override middleware + # Create Agent with override middleware middleware = ChatAgentStreamOverrideMiddleware() - agent = ChatAgent(chat_client=mock_chat_client, middleware=[middleware]) + agent = Agent(client=mock_chat_client, middleware=[middleware]) # Test streaming override case - override_messages = [ChatMessage(role="user", text="Give me a custom stream")] + override_messages = [Message(role="user", text="Give me a custom stream")] override_updates: list[AgentResponseUpdate] = [] async for update in agent.run(override_messages, stream=True): override_updates.append(update) @@ -210,7 +210,7 @@ class TestResultOverrideMiddleware: assert override_updates[2].text == " response!" # Test normal streaming case - normal_messages = [ChatMessage(role="user", text="Normal streaming request")] + normal_messages = [Message(role="user", text="Normal streaming request")] normal_updates: list[AgentResponseUpdate] = [] async for update in agent.run(normal_messages, stream=True): normal_updates.append(update) @@ -239,10 +239,10 @@ class TestResultOverrideMiddleware: async def final_handler(ctx: AgentContext) -> AgentResponse: nonlocal handler_called handler_called = True - return AgentResponse(messages=[ChatMessage(role="assistant", text="executed response")]) + return AgentResponse(messages=[Message(role="assistant", text="executed response")]) # Test case where next() is NOT called - no_execute_messages = [ChatMessage(role="user", text="Don't run this")] + no_execute_messages = [Message(role="user", text="Don't run this")] no_execute_context = AgentContext(agent=mock_agent, messages=no_execute_messages, stream=False) no_execute_result = await pipeline.execute(no_execute_context, final_handler) @@ -254,7 +254,7 @@ class TestResultOverrideMiddleware: handler_called = False # Test case where next() IS called - execute_messages = [ChatMessage(role="user", text="Please execute this")] + execute_messages = [Message(role="user", text="Please execute this")] execute_context = AgentContext(agent=mock_agent, messages=execute_messages, stream=False) execute_result = await pipeline.execute(execute_context, final_handler) @@ -334,11 +334,11 @@ class TestResultObservability: middleware = ObservabilityMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages, stream=False) async def final_handler(ctx: AgentContext) -> AgentResponse: - return AgentResponse(messages=[ChatMessage(role="assistant", text="executed response")]) + return AgentResponse(messages=[Message(role="assistant", text="executed response")]) result = await pipeline.execute(context, final_handler) @@ -399,16 +399,16 @@ class TestResultObservability: if "modify" in context.result.messages[0].text: # Override after observing context.result = AgentResponse( - messages=[ChatMessage(role="assistant", text="modified after execution")] + messages=[Message(role="assistant", text="modified after execution")] ) middleware = PostExecutionOverrideMiddleware() pipeline = AgentMiddlewarePipeline(middleware) - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] context = AgentContext(agent=mock_agent, messages=messages, stream=False) async def final_handler(ctx: AgentContext) -> AgentResponse: - return AgentResponse(messages=[ChatMessage(role="assistant", text="response to modify")]) + return AgentResponse(messages=[Message(role="assistant", text="response to modify")]) result = await pipeline.execute(context, final_handler) diff --git a/python/packages/core/tests/core/test_middleware_with_agent.py b/python/packages/core/tests/core/test_middleware_with_agent.py index 10cc8b3011..17f0faf4f0 100644 --- a/python/packages/core/tests/core/test_middleware_with_agent.py +++ b/python/packages/core/tests/core/test_middleware_with_agent.py @@ -6,13 +6,11 @@ from typing import Any import pytest from agent_framework import ( + Agent, AgentContext, AgentMiddleware, AgentResponseUpdate, - ChatAgent, - ChatClientProtocol, ChatContext, - ChatMessage, ChatMiddleware, ChatResponse, ChatResponseUpdate, @@ -20,9 +18,11 @@ from agent_framework import ( FunctionInvocationContext, FunctionMiddleware, FunctionTool, + Message, MiddlewareException, MiddlewareTermination, MiddlewareType, + SupportsChatGetResponse, agent_middleware, chat_middleware, function_middleware, @@ -30,14 +30,14 @@ from agent_framework import ( from .conftest import MockBaseChatClient, MockChatClient -# region ChatAgent Tests +# region Agent Tests class TestChatAgentClassBasedMiddleware: - """Test cases for class-based middleware integration with ChatAgent.""" + """Test cases for class-based middleware integration with Agent.""" - async def test_class_based_agent_middleware_with_chat_agent(self, chat_client: ChatClientProtocol) -> None: - """Test class-based agent middleware with ChatAgent.""" + async def test_class_based_agent_middleware_with_chat_agent(self, client: SupportsChatGetResponse) -> None: + """Test class-based agent middleware with Agent.""" execution_order: list[str] = [] class TrackingAgentMiddleware(AgentMiddleware): @@ -51,12 +51,12 @@ class TestChatAgentClassBasedMiddleware: await call_next(context) execution_order.append(f"{self.name}_after") - # Create ChatAgent with middleware + # Create Agent with middleware middleware = TrackingAgentMiddleware("agent_middleware") - agent = ChatAgent(chat_client=chat_client, middleware=[middleware]) + agent = Agent(client=client, middleware=[middleware]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify response @@ -69,8 +69,8 @@ class TestChatAgentClassBasedMiddleware: # Verify middleware execution order assert execution_order == ["agent_middleware_before", "agent_middleware_after"] - async def test_class_based_function_middleware_with_chat_agent(self, chat_client: "MockChatClient") -> None: - """Test class-based function middleware with ChatAgent.""" + async def test_class_based_function_middleware_with_chat_agent(self, client: "MockChatClient") -> None: + """Test class-based function middleware with Agent.""" class TrackingFunctionMiddleware(FunctionMiddleware): async def process( @@ -81,12 +81,12 @@ class TestChatAgentClassBasedMiddleware: await call_next(context) middleware = TrackingFunctionMiddleware() - ChatAgent(chat_client=chat_client, middleware=[middleware]) + Agent(client=client, middleware=[middleware]) async def test_class_based_function_middleware_with_chat_agent_supported_client( self, chat_client_base: "MockBaseChatClient" ) -> None: - """Test class-based function middleware with ChatAgent using a full chat client.""" + """Test class-based function middleware with Agent using a full chat client.""" execution_order: list[str] = [] class TrackingFunctionMiddleware(FunctionMiddleware): @@ -103,9 +103,9 @@ class TestChatAgentClassBasedMiddleware: execution_order.append(f"{self.name}_after") middleware = TrackingFunctionMiddleware("function_middleware") - agent = ChatAgent(chat_client=chat_client_base, middleware=[middleware]) + agent = Agent(client=chat_client_base, middleware=[middleware]) - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) assert response is not None @@ -115,9 +115,9 @@ class TestChatAgentClassBasedMiddleware: class TestChatAgentFunctionBasedMiddleware: - """Test cases for function-based middleware integration with ChatAgent.""" + """Test cases for function-based middleware integration with Agent.""" - async def test_agent_middleware_with_pre_termination(self, chat_client: "MockChatClient") -> None: + async def test_agent_middleware_with_pre_termination(self, client: "MockChatClient") -> None: """Test that agent middleware can terminate execution before calling next().""" execution_order: list[str] = [] @@ -131,14 +131,14 @@ class TestChatAgentFunctionBasedMiddleware: await call_next(context) execution_order.append("middleware_after") - # Create ChatAgent with terminating middleware + # Create Agent with terminating middleware middleware = PreTerminationMiddleware() - agent = ChatAgent(chat_client=chat_client, middleware=[middleware]) + agent = Agent(client=client, middleware=[middleware]) # Execute the agent with multiple messages messages = [ - ChatMessage(role="user", text="message1"), - ChatMessage(role="user", text="message2"), # This should not be processed due to termination + Message(role="user", text="message1"), + Message(role="user", text="message2"), # This should not be processed due to termination ] response = await agent.run(messages) @@ -146,9 +146,9 @@ class TestChatAgentFunctionBasedMiddleware: assert response is None # Only middleware_before runs - middleware_after is unreachable after raise assert execution_order == ["middleware_before"] - assert chat_client.call_count == 0 # No calls should be made due to termination + assert client.call_count == 0 # No calls should be made due to termination - async def test_agent_middleware_with_post_termination(self, chat_client: "MockChatClient") -> None: + async def test_agent_middleware_with_post_termination(self, client: "MockChatClient") -> None: """Test that agent middleware can terminate execution after calling next().""" execution_order: list[str] = [] @@ -161,14 +161,14 @@ class TestChatAgentFunctionBasedMiddleware: execution_order.append("middleware_after") context.terminate = True - # Create ChatAgent with terminating middleware + # Create Agent with terminating middleware middleware = PostTerminationMiddleware() - agent = ChatAgent(chat_client=chat_client, middleware=[middleware]) + agent = Agent(client=client, middleware=[middleware]) # Execute the agent with multiple messages messages = [ - ChatMessage(role="user", text="message1"), - ChatMessage(role="user", text="message2"), + Message(role="user", text="message1"), + Message(role="user", text="message2"), ] response = await agent.run(messages) @@ -183,9 +183,9 @@ class TestChatAgentFunctionBasedMiddleware: "middleware_before", "middleware_after", ] - assert chat_client.call_count == 1 + assert client.call_count == 1 - async def test_function_middleware_with_pre_termination(self, chat_client: "MockChatClient") -> None: + async def test_function_middleware_with_pre_termination(self, client: "MockChatClient") -> None: """Test that function middleware can terminate execution before calling next().""" execution_order: list[str] = [] @@ -201,9 +201,9 @@ class TestChatAgentFunctionBasedMiddleware: await call_next(context) execution_order.append("middleware_after") - ChatAgent(chat_client=chat_client, middleware=[PreTerminationFunctionMiddleware()], tools=[]) + Agent(client=client, middleware=[PreTerminationFunctionMiddleware()], tools=[]) - async def test_function_middleware_with_post_termination(self, chat_client: "MockChatClient") -> None: + async def test_function_middleware_with_post_termination(self, client: "MockChatClient") -> None: """Test that function middleware can terminate execution after calling next().""" execution_order: list[str] = [] @@ -218,10 +218,10 @@ class TestChatAgentFunctionBasedMiddleware: execution_order.append("middleware_after") context.terminate = True - ChatAgent(chat_client=chat_client, middleware=[PostTerminationFunctionMiddleware()], tools=[]) + Agent(client=client, middleware=[PostTerminationFunctionMiddleware()], tools=[]) - async def test_function_based_agent_middleware_with_chat_agent(self, chat_client: "MockChatClient") -> None: - """Test function-based agent middleware with ChatAgent.""" + async def test_function_based_agent_middleware_with_chat_agent(self, client: "MockChatClient") -> None: + """Test function-based agent middleware with Agent.""" execution_order: list[str] = [] async def tracking_agent_middleware( @@ -231,11 +231,11 @@ class TestChatAgentFunctionBasedMiddleware: await call_next(context) execution_order.append("agent_function_after") - # Create ChatAgent with function middleware - agent = ChatAgent(chat_client=chat_client, middleware=[tracking_agent_middleware]) + # Create Agent with function middleware + agent = Agent(client=client, middleware=[tracking_agent_middleware]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify response @@ -243,25 +243,25 @@ class TestChatAgentFunctionBasedMiddleware: assert len(response.messages) > 0 assert response.messages[0].role == "assistant" assert response.messages[0].text == "test response" - assert chat_client.call_count == 1 + assert client.call_count == 1 # Verify middleware execution order assert execution_order == ["agent_function_before", "agent_function_after"] - async def test_function_based_function_middleware_with_chat_agent(self, chat_client: "MockChatClient") -> None: - """Test function-based function middleware with ChatAgent.""" + async def test_function_based_function_middleware_with_chat_agent(self, client: "MockChatClient") -> None: + """Test function-based function middleware with Agent.""" async def tracking_function_middleware( context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]] ) -> None: await call_next(context) - ChatAgent(chat_client=chat_client, middleware=[tracking_function_middleware]) + Agent(client=client, middleware=[tracking_function_middleware]) async def test_function_based_function_middleware_with_supported_client( self, chat_client_base: "MockBaseChatClient" ) -> None: - """Test function-based function middleware with ChatAgent using a full chat client.""" + """Test function-based function middleware with Agent using a full chat client.""" execution_order: list[str] = [] async def tracking_function_middleware( @@ -271,8 +271,8 @@ class TestChatAgentFunctionBasedMiddleware: await call_next(context) execution_order.append("function_function_after") - agent = ChatAgent(chat_client=chat_client_base, middleware=[tracking_function_middleware]) - messages = [ChatMessage(role="user", text="test message")] + agent = Agent(client=chat_client_base, middleware=[tracking_function_middleware]) + messages = [Message(role="user", text="test message")] response = await agent.run(messages) assert response is not None @@ -282,10 +282,10 @@ class TestChatAgentFunctionBasedMiddleware: class TestChatAgentStreamingMiddleware: - """Test cases for streaming middleware integration with ChatAgent.""" + """Test cases for streaming middleware integration with Agent.""" - async def test_agent_middleware_with_streaming(self, chat_client: "MockChatClient") -> None: - """Test agent middleware with streaming ChatAgent responses.""" + async def test_agent_middleware_with_streaming(self, client: "MockChatClient") -> None: + """Test agent middleware with streaming Agent responses.""" execution_order: list[str] = [] streaming_flags: list[bool] = [] @@ -298,12 +298,12 @@ class TestChatAgentStreamingMiddleware: await call_next(context) execution_order.append("middleware_after") - # Create ChatAgent with middleware + # Create Agent with middleware middleware = StreamingTrackingMiddleware() - agent = ChatAgent(chat_client=chat_client, middleware=[middleware]) + agent = Agent(client=client, middleware=[middleware]) # Set up mock streaming responses - chat_client.streaming_responses = [ + client.streaming_responses = [ [ ChatResponseUpdate(contents=[Content.from_text(text="Streaming")], role="assistant"), ChatResponseUpdate(contents=[Content.from_text(text=" response")], role="assistant"), @@ -311,7 +311,7 @@ class TestChatAgentStreamingMiddleware: ] # Execute streaming - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] updates: list[AgentResponseUpdate] = [] async for update in agent.run(messages, stream=True): updates.append(update) @@ -320,7 +320,7 @@ class TestChatAgentStreamingMiddleware: assert len(updates) == 2 assert updates[0].text == "Streaming" assert updates[1].text == " response" - assert chat_client.call_count == 1 + assert client.call_count == 1 # Verify middleware was called and streaming flag was set correctly assert execution_order == [ @@ -329,7 +329,7 @@ class TestChatAgentStreamingMiddleware: ] assert streaming_flags == [True] # Context should indicate streaming - async def test_non_streaming_vs_streaming_flag_validation(self, chat_client: "MockChatClient") -> None: + async def test_non_streaming_vs_streaming_flag_validation(self, client: "MockChatClient") -> None: """Test that stream flag is correctly set for different execution modes.""" streaming_flags: list[bool] = [] @@ -340,10 +340,10 @@ class TestChatAgentStreamingMiddleware: streaming_flags.append(context.stream) await call_next(context) - # Create ChatAgent with middleware + # Create Agent with middleware middleware = FlagTrackingMiddleware() - agent = ChatAgent(chat_client=chat_client, middleware=[middleware]) - messages = [ChatMessage(role="user", text="test message")] + agent = Agent(client=client, middleware=[middleware]) + messages = [Message(role="user", text="test message")] # Test non-streaming execution response = await agent.run(messages) @@ -358,10 +358,10 @@ class TestChatAgentStreamingMiddleware: class TestChatAgentMultipleMiddlewareOrdering: - """Test cases for multiple middleware execution order with ChatAgent.""" + """Test cases for multiple middleware execution order with Agent.""" - async def test_multiple_agent_middleware_execution_order(self, chat_client: "MockChatClient") -> None: - """Test that multiple agent middleware execute in correct order with ChatAgent.""" + async def test_multiple_agent_middleware_execution_order(self, client: "MockChatClient") -> None: + """Test that multiple agent middleware execute in correct order with Agent.""" execution_order: list[str] = [] class OrderedMiddleware(AgentMiddleware): @@ -380,23 +380,23 @@ class TestChatAgentMultipleMiddlewareOrdering: middleware2 = OrderedMiddleware("second") middleware3 = OrderedMiddleware("third") - # Create ChatAgent with multiple middleware - agent = ChatAgent(chat_client=chat_client, middleware=[middleware1, middleware2, middleware3]) + # Create Agent with multiple middleware + agent = Agent(client=client, middleware=[middleware1, middleware2, middleware3]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify response assert response is not None - assert chat_client.call_count == 1 + assert client.call_count == 1 # Verify execution order (should be nested: first wraps second wraps third) expected_order = ["first_before", "second_before", "third_before", "third_after", "second_after", "first_after"] assert execution_order == expected_order async def test_mixed_middleware_types_with_chat_agent(self, chat_client_base: "MockBaseChatClient") -> None: - """Test mixed class and function-based middleware with ChatAgent.""" + """Test mixed class and function-based middleware with Agent.""" execution_order: list[str] = [] class ClassAgentMiddleware(AgentMiddleware): @@ -431,8 +431,8 @@ class TestChatAgentMultipleMiddlewareOrdering: await call_next(context) execution_order.append("function_function_after") - agent = ChatAgent( - chat_client=chat_client_base, + agent = Agent( + client=chat_client_base, middleware=[ ClassAgentMiddleware(), function_agent_middleware, @@ -440,7 +440,7 @@ class TestChatAgentMultipleMiddlewareOrdering: function_function_middleware, ], ) - await agent.run([ChatMessage(role="user", text="test")]) + await agent.run([Message(role="user", text="test")]) async def test_mixed_middleware_types_with_supported_client(self, chat_client_base: "MockBaseChatClient") -> None: """Test mixed class and function-based middleware with a full chat client.""" @@ -468,8 +468,8 @@ class TestChatAgentMultipleMiddlewareOrdering: await call_next(context) execution_order.append("function_function_after") - agent = ChatAgent( - chat_client=chat_client_base, + agent = Agent( + client=chat_client_base, middleware=[ ClassAgentMiddleware(), function_agent_middleware, @@ -477,7 +477,7 @@ class TestChatAgentMultipleMiddlewareOrdering: ], ) - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) assert response is not None @@ -502,16 +502,16 @@ sample_tool_function = FunctionTool( ) -# region ChatAgent Function MiddlewareTypes Tests with Tools +# region Agent Function MiddlewareTypes Tests with Tools class TestChatAgentFunctionMiddlewareWithTools: - """Test cases for function middleware integration with ChatAgent when tools are used.""" + """Test cases for function middleware integration with Agent when tools are used.""" async def test_class_based_function_middleware_with_tool_calls( self, chat_client_base: "MockBaseChatClient" ) -> None: - """Test class-based function middleware with ChatAgent when function calls are made.""" + """Test class-based function middleware with Agent when function calls are made.""" execution_order: list[str] = [] class TrackingFunctionMiddleware(FunctionMiddleware): @@ -530,7 +530,7 @@ class TestChatAgentFunctionMiddlewareWithTools: # Set up mock to return a function call first, then a regular response function_call_response = ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -542,20 +542,20 @@ class TestChatAgentFunctionMiddlewareWithTools: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="Final response")]) + final_response = ChatResponse(messages=[Message(role="assistant", text="Final response")]) chat_client_base.run_responses = [function_call_response, final_response] - # Create ChatAgent with function middleware and tools + # Create Agent with function middleware and tools middleware = TrackingFunctionMiddleware("function_middleware") - agent = ChatAgent( - chat_client=chat_client_base, + agent = Agent( + client=chat_client_base, middleware=[middleware], tools=[sample_tool_function], ) # Execute the agent - messages = [ChatMessage(role="user", text="Get weather for Seattle")] + messages = [Message(role="user", text="Get weather for Seattle")] response = await agent.run(messages) # Verify response @@ -579,7 +579,7 @@ class TestChatAgentFunctionMiddlewareWithTools: async def test_function_based_function_middleware_with_tool_calls( self, chat_client_base: "MockBaseChatClient" ) -> None: - """Test function-based function middleware with ChatAgent when function calls are made.""" + """Test function-based function middleware with Agent when function calls are made.""" execution_order: list[str] = [] async def tracking_function_middleware( @@ -592,7 +592,7 @@ class TestChatAgentFunctionMiddlewareWithTools: # Set up mock to return a function call first, then a regular response function_call_response = ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -604,19 +604,19 @@ class TestChatAgentFunctionMiddlewareWithTools: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="Final response")]) + final_response = ChatResponse(messages=[Message(role="assistant", text="Final response")]) chat_client_base.run_responses = [function_call_response, final_response] - # Create ChatAgent with function middleware and tools - agent = ChatAgent( - chat_client=chat_client_base, + # Create Agent with function middleware and tools + agent = Agent( + client=chat_client_base, middleware=[tracking_function_middleware], tools=[sample_tool_function], ) # Execute the agent - messages = [ChatMessage(role="user", text="Get weather for San Francisco")] + messages = [Message(role="user", text="Get weather for San Francisco")] response = await agent.run(messages) # Verify response @@ -640,7 +640,7 @@ class TestChatAgentFunctionMiddlewareWithTools: async def test_mixed_agent_and_function_middleware_with_tool_calls( self, chat_client_base: "MockBaseChatClient" ) -> None: - """Test both agent and function middleware with ChatAgent when function calls are made.""" + """Test both agent and function middleware with Agent when function calls are made.""" execution_order: list[str] = [] class TrackingAgentMiddleware(AgentMiddleware): @@ -666,7 +666,7 @@ class TestChatAgentFunctionMiddlewareWithTools: # Set up mock to return a function call first, then a regular response function_call_response = ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -678,19 +678,19 @@ class TestChatAgentFunctionMiddlewareWithTools: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="Final response")]) + final_response = ChatResponse(messages=[Message(role="assistant", text="Final response")]) chat_client_base.run_responses = [function_call_response, final_response] - # Create ChatAgent with both agent and function middleware and tools - agent = ChatAgent( - chat_client=chat_client_base, + # Create Agent with both agent and function middleware and tools + agent = Agent( + client=chat_client_base, middleware=[TrackingAgentMiddleware(), TrackingFunctionMiddleware()], tools=[sample_tool_function], ) # Execute the agent - messages = [ChatMessage(role="user", text="Get weather for New York")] + messages = [Message(role="user", text="Get weather for New York")] response = await agent.run(messages) # Verify response @@ -753,7 +753,7 @@ class TestChatAgentFunctionMiddlewareWithTools: chat_client_base.run_responses = [ ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -763,14 +763,14 @@ class TestChatAgentFunctionMiddlewareWithTools: ) ] ), - ChatResponse(messages=[ChatMessage(role="assistant", contents=[Content.from_text("Function completed")])]), + ChatResponse(messages=[Message(role="assistant", contents=[Content.from_text("Function completed")])]), ] - # Create ChatAgent with function middleware - agent = ChatAgent(chat_client=chat_client_base, middleware=[kwargs_middleware], tools=[sample_tool_function]) + # Create Agent with function middleware + agent = Agent(client=chat_client_base, middleware=[kwargs_middleware], tools=[sample_tool_function]) # Execute the agent with custom parameters passed as kwargs - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages, options={"additional_function_arguments": {"custom_param": "test_value"}}) # Verify response @@ -792,7 +792,7 @@ class TestChatAgentFunctionMiddlewareWithTools: class TestMiddlewareDynamicRebuild: - """Test cases for dynamic middleware pipeline rebuilding with ChatAgent.""" + """Test cases for dynamic middleware pipeline rebuilding with Agent.""" class TrackingAgentMiddleware(AgentMiddleware): """Test middleware that tracks execution.""" @@ -806,13 +806,13 @@ class TestMiddlewareDynamicRebuild: await call_next(context) self.execution_log.append(f"{self.name}_end") - async def test_middleware_dynamic_rebuild_non_streaming(self, chat_client: "MockChatClient") -> None: + async def test_middleware_dynamic_rebuild_non_streaming(self, client: "MockChatClient") -> None: """Test that middleware pipeline is rebuilt when agent.middleware collection is modified for non-streaming.""" execution_log: list[str] = [] # Create agent with initial middleware middleware1 = self.TrackingAgentMiddleware("middleware1", execution_log) - agent = ChatAgent(chat_client=chat_client, middleware=[middleware1]) + agent = Agent(client=client, middleware=[middleware1]) # First execution - should use middleware1 await agent.run("Test message 1") @@ -856,13 +856,13 @@ class TestMiddlewareDynamicRebuild: await agent.run("Test message 4") assert len(execution_log) == 0 - async def test_middleware_dynamic_rebuild_streaming(self, chat_client: "MockChatClient") -> None: + async def test_middleware_dynamic_rebuild_streaming(self, client: "MockChatClient") -> None: """Test that middleware pipeline is rebuilt for streaming when agent.middleware collection is modified.""" execution_log: list[str] = [] # Create agent with initial middleware middleware1 = self.TrackingAgentMiddleware("stream_middleware1", execution_log) - agent = ChatAgent(chat_client=chat_client, middleware=[middleware1]) + agent = Agent(client=client, middleware=[middleware1]) # First streaming execution updates: list[AgentResponseUpdate] = [] @@ -889,7 +889,7 @@ class TestMiddlewareDynamicRebuild: assert "stream_middleware2_start" in execution_log assert "stream_middleware2_end" in execution_log - async def test_middleware_order_change_detection(self, chat_client: "MockChatClient") -> None: + async def test_middleware_order_change_detection(self, client: "MockChatClient") -> None: """Test that changing the order of middleware is detected and applied.""" execution_log: list[str] = [] @@ -897,7 +897,7 @@ class TestMiddlewareDynamicRebuild: middleware2 = self.TrackingAgentMiddleware("second", execution_log) # Create agent with middleware in order [first, second] - agent = ChatAgent(chat_client=chat_client, middleware=[middleware1, middleware2]) + agent = Agent(client=client, middleware=[middleware1, middleware2]) # First execution await agent.run("Test message 1") @@ -929,12 +929,12 @@ class TestRunLevelMiddleware: await call_next(context) self.execution_log.append(f"{self.name}_end") - async def test_run_level_middleware_isolation(self, chat_client: "MockChatClient") -> None: + async def test_run_level_middleware_isolation(self, client: "MockChatClient") -> None: """Test that run-level middleware is isolated between multiple runs.""" execution_log: list[str] = [] # Create agent without any agent-level middleware - agent = ChatAgent(chat_client=chat_client) + agent = Agent(client=client) # Create run-level middleware run_middleware1 = self.TrackingAgentMiddleware("run1", execution_log) @@ -967,7 +967,7 @@ class TestRunLevelMiddleware: await agent.run("Test message 4", middleware=[run_middleware1, run_middleware2]) assert execution_log == ["run1_start", "run2_start", "run2_end", "run1_end"] - async def test_agent_plus_run_middleware_execution_order(self, chat_client: "MockChatClient") -> None: + async def test_agent_plus_run_middleware_execution_order(self, client: "MockChatClient") -> None: """Test that agent middleware executes first, followed by run middleware.""" execution_log: list[str] = [] metadata_log: list[str] = [] @@ -1003,7 +1003,7 @@ class TestRunLevelMiddleware: # Create agent with agent-level middleware agent_middleware = MetadataAgentMiddleware("agent") - agent = ChatAgent(chat_client=chat_client, middleware=[agent_middleware]) + agent = Agent(client=client, middleware=[agent_middleware]) # Create run-level middleware run_middleware = MetadataRunMiddleware("run") @@ -1018,12 +1018,12 @@ class TestRunLevelMiddleware: # Verify that run middleware can read agent middleware metadata assert "run_reads_agent_key:agent_value" in metadata_log - async def test_run_level_middleware_non_streaming(self, chat_client: "MockChatClient") -> None: + async def test_run_level_middleware_non_streaming(self, client: "MockChatClient") -> None: """Test run-level middleware with non-streaming execution.""" execution_log: list[str] = [] # Create agent without agent-level middleware - agent = ChatAgent(chat_client=chat_client) + agent = Agent(client=client) # Create run-level middleware run_middleware = self.TrackingAgentMiddleware("run_nonstream", execution_log) @@ -1040,7 +1040,7 @@ class TestRunLevelMiddleware: # Verify middleware was executed assert execution_log == ["run_nonstream_start", "run_nonstream_end"] - async def test_run_level_middleware_streaming(self, chat_client: "MockChatClient") -> None: + async def test_run_level_middleware_streaming(self, client: "MockChatClient") -> None: """Test run-level middleware with streaming execution.""" execution_log: list[str] = [] streaming_flags: list[bool] = [] @@ -1058,10 +1058,10 @@ class TestRunLevelMiddleware: execution_log.append(f"{self.name}_end") # Create agent without agent-level middleware - agent = ChatAgent(chat_client=chat_client) + agent = Agent(client=client) # Set up mock streaming responses - chat_client.streaming_responses = [ + client.streaming_responses = [ [ ChatResponseUpdate(contents=[Content.from_text(text="Stream")], role="assistant"), ChatResponseUpdate(contents=[Content.from_text(text=" response")], role="assistant"), @@ -1149,7 +1149,7 @@ class TestRunLevelMiddleware: # Set up mock to return a function call first, then a regular response function_call_response = ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -1161,12 +1161,12 @@ class TestRunLevelMiddleware: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="Final response")]) + final_response = ChatResponse(messages=[Message(role="assistant", text="Final response")]) chat_client_base.run_responses = [function_call_response, final_response] # Create agent with agent-level middleware - agent = ChatAgent( - chat_client=chat_client_base, + agent = Agent( + client=chat_client_base, middleware=[AgentLevelAgentMiddleware(), AgentLevelFunctionMiddleware()], tools=[custom_tool_wrapped], ) @@ -1242,7 +1242,7 @@ class TestMiddlewareDecoratorLogic: # Set up mock to return a function call first, then a regular response function_call_response = ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -1254,23 +1254,23 @@ class TestMiddlewareDecoratorLogic: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="Final response")]) + final_response = ChatResponse(messages=[Message(role="assistant", text="Final response")]) chat_client_base.responses = [function_call_response, final_response] # Should work without errors - agent = ChatAgent( - chat_client=chat_client_base, + agent = Agent( + client=chat_client_base, middleware=[matching_agent_middleware, matching_function_middleware], tools=[custom_tool_wrapped], ) - response = await agent.run([ChatMessage(role="user", text="test")]) + response = await agent.run([Message(role="user", text="test")]) assert response is not None assert "decorator_type_match_agent" in execution_order assert "decorator_type_match_function" not in execution_order - async def test_decorator_and_type_mismatch(self, chat_client: MockChatClient) -> None: + async def test_decorator_and_type_mismatch(self, client: MockChatClient) -> None: """Both decorator and parameter type specified but don't match.""" # This will cause a type error at decoration time, so we need to test differently @@ -1284,8 +1284,8 @@ class TestMiddlewareDecoratorLogic: ) -> None: await call_next(context) - agent = ChatAgent(chat_client=chat_client, middleware=[mismatched_middleware]) - await agent.run([ChatMessage(role="user", text="test")]) + agent = Agent(client=client, middleware=[mismatched_middleware]) + await agent.run([Message(role="user", text="test")]) async def test_only_decorator_specified(self, chat_client_base: "MockBaseChatClient") -> None: """Only decorator specified - rely on decorator.""" @@ -1313,7 +1313,7 @@ class TestMiddlewareDecoratorLogic: # Set up mock to return a function call first, then a regular response function_call_response = ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -1325,17 +1325,17 @@ class TestMiddlewareDecoratorLogic: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="Final response")]) + final_response = ChatResponse(messages=[Message(role="assistant", text="Final response")]) chat_client_base.responses = [function_call_response, final_response] # Should work - relies on decorator - agent = ChatAgent( - chat_client=chat_client_base, + agent = Agent( + client=chat_client_base, middleware=[decorator_only_agent, decorator_only_function], tools=[custom_tool_wrapped], ) - response = await agent.run([ChatMessage(role="user", text="test")]) + response = await agent.run([Message(role="user", text="test")]) assert response is not None assert "decorator_only_agent" in execution_order @@ -1369,7 +1369,7 @@ class TestMiddlewareDecoratorLogic: # Set up mock to return a function call first, then a regular response function_call_response = ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -1381,21 +1381,21 @@ class TestMiddlewareDecoratorLogic: ) ] ) - final_response = ChatResponse(messages=[ChatMessage(role="assistant", text="Final response")]) + final_response = ChatResponse(messages=[Message(role="assistant", text="Final response")]) chat_client_base.responses = [function_call_response, final_response] # Should work - relies on type annotations - agent = ChatAgent( - chat_client=chat_client_base, middleware=[type_only_agent, type_only_function], tools=[custom_tool_wrapped] + agent = Agent( + client=chat_client_base, middleware=[type_only_agent, type_only_function], tools=[custom_tool_wrapped] ) - response = await agent.run([ChatMessage(role="user", text="test")]) + response = await agent.run([Message(role="user", text="test")]) assert response is not None assert "type_only_agent" in execution_order assert "type_only_function" not in execution_order - async def test_neither_decorator_nor_type(self, chat_client: Any) -> None: + async def test_neither_decorator_nor_type(self, client: Any) -> None: """Neither decorator nor parameter type specified - should throw exception.""" async def no_info_middleware(context: Any, call_next: Any) -> None: # No decorator, no type @@ -1403,12 +1403,12 @@ class TestMiddlewareDecoratorLogic: # Should raise MiddlewareException with pytest.raises(MiddlewareException, match="Cannot determine middleware type"): - agent = ChatAgent(chat_client=chat_client, middleware=[no_info_middleware]) - await agent.run([ChatMessage(role="user", text="test")]) + agent = Agent(client=client, middleware=[no_info_middleware]) + await agent.run([Message(role="user", text="test")]) - async def test_insufficient_parameters_error(self, chat_client: Any) -> None: + async def test_insufficient_parameters_error(self, client: Any) -> None: """Test that middleware with insufficient parameters raises an error.""" - from agent_framework import ChatAgent, agent_middleware + from agent_framework import Agent, agent_middleware # Should raise MiddlewareException about insufficient parameters with pytest.raises(MiddlewareException, match="must have at least 2 parameters"): @@ -1417,8 +1417,8 @@ class TestMiddlewareDecoratorLogic: async def insufficient_params_middleware(context: Any) -> None: # Missing 'next' parameter pass - agent = ChatAgent(chat_client=chat_client, middleware=[insufficient_params_middleware]) - await agent.run([ChatMessage(role="user", text="test")]) + agent = Agent(client=client, middleware=[insufficient_params_middleware]) + await agent.run([Message(role="user", text="test")]) async def test_decorator_markers_preserved(self) -> None: """Test that decorator markers are properly set on functions.""" @@ -1442,7 +1442,7 @@ class TestMiddlewareDecoratorLogic: class TestChatAgentThreadBehavior: """Test cases for thread behavior in AgentContext across multiple runs.""" - async def test_agent_context_thread_behavior_across_multiple_runs(self, chat_client: "MockChatClient") -> None: + async def test_agent_context_thread_behavior_across_multiple_runs(self, client: "MockChatClient") -> None: """Test that AgentContext.thread property behaves correctly across multiple agent runs.""" thread_states: list[dict[str, Any]] = [] @@ -1483,15 +1483,15 @@ class TestChatAgentThreadBehavior: # Import the ChatMessageStore to configure the agent with a message store factory from agent_framework import ChatMessageStore - # Create ChatAgent with thread tracking middleware and a message store factory + # Create Agent with thread tracking middleware and a message store factory middleware = ThreadTrackingMiddleware() - agent = ChatAgent(chat_client=chat_client, middleware=[middleware], chat_message_store_factory=ChatMessageStore) + agent = Agent(client=client, middleware=[middleware], chat_message_store_factory=ChatMessageStore) # Create a thread that will persist messages between runs thread = agent.get_new_thread() # First run - first_messages = [ChatMessage(role="user", text="first message")] + first_messages = [Message(role="user", text="first message")] first_response = await agent.run(first_messages, thread=thread) # Verify first response @@ -1499,7 +1499,7 @@ class TestChatAgentThreadBehavior: assert len(first_response.messages) > 0 # Second run - use the same thread - second_messages = [ChatMessage(role="user", text="second message")] + second_messages = [Message(role="user", text="second message")] second_response = await agent.run(second_messages, thread=thread) # Verify second response @@ -1553,10 +1553,10 @@ class TestChatAgentThreadBehavior: class TestChatAgentChatMiddleware: - """Test cases for chat middleware integration with ChatAgent.""" + """Test cases for chat middleware integration with Agent.""" async def test_class_based_chat_middleware_with_chat_agent(self) -> None: - """Test class-based chat middleware with ChatAgent.""" + """Test class-based chat middleware with Agent.""" execution_order: list[str] = [] class TrackingChatMiddleware(ChatMiddleware): @@ -1565,13 +1565,13 @@ class TestChatAgentChatMiddleware: await call_next(context) execution_order.append("chat_middleware_after") - # Create ChatAgent with chat middleware - chat_client = MockBaseChatClient() + # Create Agent with chat middleware + client = MockBaseChatClient() middleware = TrackingChatMiddleware() - agent = ChatAgent(chat_client=chat_client, middleware=[middleware]) + agent = Agent(client=client, middleware=[middleware]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify response @@ -1585,7 +1585,7 @@ class TestChatAgentChatMiddleware: ] async def test_function_based_chat_middleware_with_chat_agent(self) -> None: - """Test function-based chat middleware with ChatAgent.""" + """Test function-based chat middleware with Agent.""" execution_order: list[str] = [] async def tracking_chat_middleware( @@ -1595,12 +1595,12 @@ class TestChatAgentChatMiddleware: await call_next(context) execution_order.append("chat_middleware_after") - # Create ChatAgent with function-based chat middleware - chat_client = MockBaseChatClient() - agent = ChatAgent(chat_client=chat_client, middleware=[tracking_chat_middleware]) + # Create Agent with function-based chat middleware + client = MockBaseChatClient() + agent = Agent(client=client, middleware=[tracking_chat_middleware]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify response @@ -1626,16 +1626,16 @@ class TestChatAgentChatMiddleware: if msg.role == "system": continue original_text = msg.text or "" - context.messages[idx] = ChatMessage(role=msg.role, text=f"MODIFIED: {original_text}") + context.messages[idx] = Message(role=msg.role, text=f"MODIFIED: {original_text}") break await call_next(context) - # Create ChatAgent with message-modifying middleware - chat_client = MockBaseChatClient() - agent = ChatAgent(chat_client=chat_client, middleware=[message_modifier_middleware]) + # Create Agent with message-modifying middleware + client = MockBaseChatClient() + agent = Agent(client=client, middleware=[message_modifier_middleware]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify that the message was modified (MockBaseChatClient echoes back the input) @@ -1651,17 +1651,17 @@ class TestChatAgentChatMiddleware: ) -> None: # Override the response without calling next() context.result = ChatResponse( - messages=[ChatMessage(role="assistant", text="MiddlewareTypes overridden response")], + messages=[Message(role="assistant", text="MiddlewareTypes overridden response")], response_id="middleware-response-123", ) context.terminate = True - # Create ChatAgent with response-overriding middleware - chat_client = MockBaseChatClient() - agent = ChatAgent(chat_client=chat_client, middleware=[response_override_middleware]) + # Create Agent with response-overriding middleware + client = MockBaseChatClient() + agent = Agent(client=client, middleware=[response_override_middleware]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify that the response was overridden @@ -1686,12 +1686,12 @@ class TestChatAgentChatMiddleware: await call_next(context) execution_order.append("second_after") - # Create ChatAgent with multiple chat middleware - chat_client = MockBaseChatClient() - agent = ChatAgent(chat_client=chat_client, middleware=[first_middleware, second_middleware]) + # Create Agent with multiple chat middleware + client = MockBaseChatClient() + agent = Agent(client=client, middleware=[first_middleware, second_middleware]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify response @@ -1715,13 +1715,13 @@ class TestChatAgentChatMiddleware: await call_next(context) execution_order.append("streaming_chat_after") - # Create ChatAgent with chat middleware - chat_client = MockBaseChatClient() - agent = ChatAgent(chat_client=chat_client, middleware=[StreamingTrackingChatMiddleware()]) + # Create Agent with chat middleware + client = MockBaseChatClient() + agent = Agent(client=client, middleware=[StreamingTrackingChatMiddleware()]) # Set up mock streaming responses # TODO: refactor to return a ResponseStream object - chat_client.streaming_responses = [ + client.streaming_responses = [ [ ChatResponseUpdate(contents=[Content.from_text(text="Stream")], role="assistant"), ChatResponseUpdate(contents=[Content.from_text(text=" response")], role="assistant"), @@ -1729,7 +1729,7 @@ class TestChatAgentChatMiddleware: ] # Execute streaming - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] updates: list[AgentResponseUpdate] = [] async for update in agent.run(messages, stream=True): updates.append(update) @@ -1752,18 +1752,18 @@ class TestChatAgentChatMiddleware: async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None: execution_order.append("middleware_before") # Set a custom response since we're terminating - context.result = ChatResponse(messages=[ChatMessage(role="assistant", text="Terminated by middleware")]) + context.result = ChatResponse(messages=[Message(role="assistant", text="Terminated by middleware")]) raise MiddlewareTermination # We call next() but since terminate=True, execution should stop await call_next(context) execution_order.append("middleware_after") - # Create ChatAgent with terminating middleware - chat_client = MockBaseChatClient() - agent = ChatAgent(chat_client=chat_client, middleware=[PreTerminationChatMiddleware()]) + # Create Agent with terminating middleware + client = MockBaseChatClient() + agent = Agent(client=client, middleware=[PreTerminationChatMiddleware()]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify response was from middleware @@ -1783,12 +1783,12 @@ class TestChatAgentChatMiddleware: execution_order.append("middleware_after") context.terminate = True - # Create ChatAgent with terminating middleware - chat_client = MockBaseChatClient() - agent = ChatAgent(chat_client=chat_client, middleware=[PostTerminationChatMiddleware()]) + # Create Agent with terminating middleware + client = MockBaseChatClient() + agent = Agent(client=client, middleware=[PostTerminationChatMiddleware()]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify response is from actual execution @@ -1801,7 +1801,7 @@ class TestChatAgentChatMiddleware: ] async def test_combined_middleware(self) -> None: - """Test ChatAgent with combined middleware types.""" + """Test Agent with combined middleware types.""" execution_order: list[str] = [] async def agent_middleware(context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None: @@ -1821,13 +1821,13 @@ class TestChatAgentChatMiddleware: await call_next(context) execution_order.append("function_middleware_after") - # Create ChatAgent with function middleware and tools - agent = ChatAgent( - chat_client=MockBaseChatClient(), + # Create Agent with function middleware and tools + agent = Agent( + client=MockBaseChatClient(), middleware=[chat_middleware, function_middleware, agent_middleware], tools=[sample_tool_function], ) - await agent.run([ChatMessage(role="user", text="test")]) + await agent.run([Message(role="user", text="test")]) assert execution_order == [ "agent_middleware_before", @@ -1858,12 +1858,12 @@ class TestChatAgentChatMiddleware: await call_next(context) - # Create ChatAgent with agent middleware - chat_client = MockBaseChatClient() - agent = ChatAgent(chat_client=chat_client, middleware=[kwargs_middleware]) + # Create Agent with agent middleware + client = MockBaseChatClient() + agent = Agent(client=client, middleware=[kwargs_middleware]) # Execute the agent with custom parameters - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages, temperature=0.7, max_tokens=100, custom_param="test_value") # Verify response @@ -1920,7 +1920,7 @@ class TestChatAgentChatMiddleware: # yield AgentResponseUpdate() # return _stream() -# return AgentResponse(messages=[ChatMessage(role="assistant", text="response")]) +# return AgentResponse(messages=[Message(role="assistant", text="response")]) # def get_new_thread(self, **kwargs): # return None diff --git a/python/packages/core/tests/core/test_middleware_with_chat.py b/python/packages/core/tests/core/test_middleware_with_chat.py index 15621f759f..3c9d0246c7 100644 --- a/python/packages/core/tests/core/test_middleware_with_chat.py +++ b/python/packages/core/tests/core/test_middleware_with_chat.py @@ -4,16 +4,16 @@ from collections.abc import Awaitable, Callable from typing import Any from agent_framework import ( - ChatAgent, - ChatClientProtocol, + Agent, ChatContext, - ChatMessage, ChatMiddleware, ChatResponse, ChatResponseUpdate, Content, FunctionInvocationContext, FunctionTool, + Message, + SupportsChatGetResponse, chat_middleware, function_middleware, ) @@ -24,7 +24,7 @@ from .conftest import MockBaseChatClient class TestChatMiddleware: """Test cases for chat middleware functionality.""" - async def test_class_based_chat_middleware(self, chat_client_base: ChatClientProtocol) -> None: + async def test_class_based_chat_middleware(self, chat_client_base: SupportsChatGetResponse) -> None: """Test class-based chat middleware with ChatClient.""" execution_order: list[str] = [] @@ -42,7 +42,7 @@ class TestChatMiddleware: chat_client_base.chat_middleware = [LoggingChatMiddleware()] # Execute chat client directly - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await chat_client_base.get_response(messages) # Verify response @@ -69,7 +69,7 @@ class TestChatMiddleware: chat_client_base.chat_middleware = [logging_chat_middleware] # Execute chat client directly - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await chat_client_base.get_response(messages) # Verify response @@ -90,14 +90,14 @@ class TestChatMiddleware: # Modify the first message by adding a prefix if context.messages and len(context.messages) > 0: original_text = context.messages[0].text or "" - context.messages[0] = ChatMessage(role=context.messages[0].role, text=f"MODIFIED: {original_text}") + context.messages[0] = Message(role=context.messages[0].role, text=f"MODIFIED: {original_text}") await call_next(context) # Add middleware to chat client chat_client_base.chat_middleware = [message_modifier_middleware] # Execute chat client - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await chat_client_base.get_response(messages) # Verify that the message was modified (MockChatClient echoes back the input) @@ -115,7 +115,7 @@ class TestChatMiddleware: ) -> None: # Override the response without calling next() context.result = ChatResponse( - messages=[ChatMessage(role="assistant", text="MiddlewareTypes overridden response")], + messages=[Message(role="assistant", text="MiddlewareTypes overridden response")], response_id="middleware-response-123", ) context.terminate = True @@ -124,7 +124,7 @@ class TestChatMiddleware: chat_client_base.chat_middleware = [response_override_middleware] # Execute chat client - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await chat_client_base.get_response(messages) # Verify that the response was overridden @@ -153,7 +153,7 @@ class TestChatMiddleware: chat_client_base.chat_middleware = [first_middleware, second_middleware] # Execute chat client - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await chat_client_base.get_response(messages) # Verify response @@ -169,7 +169,7 @@ class TestChatMiddleware: assert execution_order == expected_order async def test_chat_agent_with_chat_middleware(self) -> None: - """Test ChatAgent with chat middleware specified at agent level.""" + """Test Agent with chat middleware specified at agent level.""" execution_order: list[str] = [] @chat_middleware @@ -180,13 +180,13 @@ class TestChatMiddleware: await call_next(context) execution_order.append("agent_chat_middleware_after") - chat_client = MockBaseChatClient() + client = MockBaseChatClient() - # Create ChatAgent with chat middleware - agent = ChatAgent(chat_client=chat_client, middleware=[agent_level_chat_middleware]) + # Create Agent with chat middleware + agent = Agent(client=client, middleware=[agent_level_chat_middleware]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify response @@ -201,7 +201,7 @@ class TestChatMiddleware: ] async def test_chat_agent_with_multiple_chat_middleware(self, chat_client_base: "MockBaseChatClient") -> None: - """Test that ChatAgent can have multiple chat middleware.""" + """Test that Agent can have multiple chat middleware.""" execution_order: list[str] = [] @chat_middleware @@ -216,11 +216,11 @@ class TestChatMiddleware: await call_next(context) execution_order.append("second_after") - # Create ChatAgent with multiple chat middleware - agent = ChatAgent(chat_client=chat_client_base, middleware=[first_middleware, second_middleware]) + # Create Agent with multiple chat middleware + agent = Agent(client=chat_client_base, middleware=[first_middleware, second_middleware]) # Execute the agent - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await agent.run(messages) # Verify response @@ -261,7 +261,7 @@ class TestChatMiddleware: chat_client_base.chat_middleware = [streaming_middleware] # Execute streaming response - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] updates: list[object] = [] async for update in chat_client_base.get_response(messages, stream=True): updates.append(update) @@ -285,19 +285,19 @@ class TestChatMiddleware: await call_next(context) # First call with run-level middleware - messages = [ChatMessage(role="user", text="first message")] + messages = [Message(role="user", text="first message")] response1 = await chat_client_base.get_response(messages, middleware=[counting_middleware]) assert response1 is not None assert execution_count["count"] == 1 # Second call WITHOUT run-level middleware - should not execute the middleware - messages = [ChatMessage(role="user", text="second message")] + messages = [Message(role="user", text="second message")] response2 = await chat_client_base.get_response(messages) assert response2 is not None assert execution_count["count"] == 1 # Should still be 1, not 2 # Third call with run-level middleware again - should execute - messages = [ChatMessage(role="user", text="third message")] + messages = [Message(role="user", text="third message")] response3 = await chat_client_base.get_response(messages, middleware=[counting_middleware]) assert response3 is not None assert execution_count["count"] == 2 # Should be 2 now @@ -328,7 +328,7 @@ class TestChatMiddleware: chat_client_base.chat_middleware = [kwargs_middleware] # Execute chat client with custom parameters - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] response = await chat_client_base.get_response( messages, temperature=0.7, max_tokens=100, custom_param="test_value" ) @@ -375,15 +375,15 @@ class TestChatMiddleware: ) # Create function-invocation enabled chat client (MockBaseChatClient already includes FunctionInvocationLayer) - chat_client = MockBaseChatClient() + client = MockBaseChatClient() # Set function middleware directly on the chat client - chat_client.function_middleware = [test_function_middleware] + client.function_middleware = [test_function_middleware] # Prepare responses that will trigger function invocation function_call_response = ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -396,18 +396,18 @@ class TestChatMiddleware: ] ) final_response = ChatResponse( - messages=[ChatMessage(role="assistant", text="Based on the weather data, it's sunny!")] + messages=[Message(role="assistant", text="Based on the weather data, it's sunny!")] ) - chat_client.run_responses = [function_call_response, final_response] + client.run_responses = [function_call_response, final_response] # Execute the chat client directly with tools - this should trigger function invocation and middleware - messages = [ChatMessage(role="user", text="What's the weather in San Francisco?")] - response = await chat_client.get_response(messages, options={"tools": [sample_tool_wrapped]}) + messages = [Message(role="user", text="What's the weather in San Francisco?")] + response = await client.get_response(messages, options={"tools": [sample_tool_wrapped]}) # Verify response assert response is not None assert len(response.messages) > 0 - assert chat_client.call_count == 2 # Two calls: function call + final response + assert client.call_count == 2 # Two calls: function call + final response # Verify function middleware was executed assert execution_order == [ @@ -440,12 +440,12 @@ class TestChatMiddleware: ) # Create function-invocation enabled chat client (MockBaseChatClient already includes FunctionInvocationLayer) - chat_client = MockBaseChatClient() + client = MockBaseChatClient() # Prepare responses that will trigger function invocation function_call_response = ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -457,18 +457,18 @@ class TestChatMiddleware: ) ] ) - chat_client.run_responses = [function_call_response] + client.run_responses = [function_call_response] # Execute the chat client directly with run-level middleware and tools - messages = [ChatMessage(role="user", text="What's the weather in New York?")] - response = await chat_client.get_response( + messages = [Message(role="user", text="What's the weather in New York?")] + response = await client.get_response( messages, options={"tools": [sample_tool_wrapped]}, middleware=[run_level_function_middleware] ) # Verify response assert response is not None assert len(response.messages) > 0 - assert chat_client.call_count == 2 # Two calls: function call + final response + assert client.call_count == 2 # Two calls: function call + final response # Verify run-level function middleware was executed once (during function invocation) assert execution_order == [ diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index a85f851957..77b88a873e 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -14,10 +14,10 @@ from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, AgentResponse, BaseChatClient, - ChatMessage, ChatResponse, ChatResponseUpdate, Content, + Message, ResponseStream, SupportsAgentRun, UsageDetails, @@ -27,8 +27,8 @@ from agent_framework import ( from agent_framework.observability import ( ROLE_EVENT_MAP, AgentTelemetryLayer, - ChatMessageListTimestampFilter, ChatTelemetryLayer, + MessageListTimestampFilter, OtelAttr, get_function_span, ) @@ -54,12 +54,12 @@ def test_enum_values(): assert OtelAttr.AGENT_INVOKE_OPERATION == "invoke_agent" -# region Test ChatMessageListTimestampFilter +# region Test MessageListTimestampFilter def test_filter_without_index_key(): """Test filter method when record doesn't have INDEX_KEY.""" - log_filter = ChatMessageListTimestampFilter() + log_filter = MessageListTimestampFilter() record = logging.LogRecord( name="test", level=logging.INFO, pathname="", lineno=0, msg="test message", args=(), exc_info=None ) @@ -73,14 +73,14 @@ def test_filter_without_index_key(): def test_filter_with_index_key(): """Test filter method when record has INDEX_KEY.""" - log_filter = ChatMessageListTimestampFilter() + log_filter = MessageListTimestampFilter() record = logging.LogRecord( name="test", level=logging.INFO, pathname="", lineno=0, msg="test message", args=(), exc_info=None ) original_created = record.created # Add the index key - setattr(record, ChatMessageListTimestampFilter.INDEX_KEY, 5) + setattr(record, MessageListTimestampFilter.INDEX_KEY, 5) result = log_filter.filter(record) @@ -91,7 +91,7 @@ def test_filter_with_index_key(): def test_index_key_constant(): """Test that INDEX_KEY constant is correctly defined.""" - assert ChatMessageListTimestampFilter.INDEX_KEY == "chat_message_index" + assert MessageListTimestampFilter.INDEX_KEY == "chat_message_index" # region Test get_function_span @@ -162,7 +162,7 @@ def mock_chat_client(): return "https://test.example.com" def _inner_get_response( - self, *, messages: MutableSequence[ChatMessage], stream: bool, options: dict[str, Any], **kwargs: Any + self, *, messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any ) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]: if stream: return self._get_streaming_response(messages=messages, options=options, **kwargs) @@ -173,16 +173,16 @@ def mock_chat_client(): return _get() async def _get_non_streaming_response( - self, *, messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + self, *, messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> ChatResponse: return ChatResponse( - messages=[ChatMessage("assistant", ["Test response"])], + messages=[Message("assistant", ["Test response"])], usage_details=UsageDetails(input_token_count=10, output_token_count=20), finish_reason=None, ) def _get_streaming_response( - self, *, messages: MutableSequence[ChatMessage], options: dict[str, Any], **kwargs: Any + self, *, messages: MutableSequence[Message], options: dict[str, Any], **kwargs: Any ) -> ResponseStream[ChatResponseUpdate, ChatResponse]: async def _stream() -> AsyncIterable[ChatResponseUpdate]: yield ChatResponseUpdate(contents=[Content.from_text("Hello")], role="assistant") @@ -203,7 +203,7 @@ async def test_chat_client_observability(mock_chat_client, span_exporter: InMemo """Test that when diagnostics are enabled, telemetry is applied.""" client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] span_exporter.clear() response = await client.get_response(messages=messages, model_id="Test") assert response is not None @@ -226,7 +226,7 @@ async def test_chat_client_streaming_observability( ): """Test streaming telemetry through the chat telemetry mixin.""" client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] span_exporter.clear() # Collect all yielded updates updates = [] @@ -257,7 +257,7 @@ async def test_chat_client_observability_with_instructions( client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] options = {"model_id": "Test", "instructions": "You are a helpful assistant."} span_exporter.clear() response = await client.get_response(messages=messages, options=options) @@ -286,7 +286,7 @@ async def test_chat_client_streaming_observability_with_instructions( import json client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] options = {"model_id": "Test", "instructions": "You are a helpful assistant."} span_exporter.clear() @@ -315,7 +315,7 @@ async def test_chat_client_observability_without_instructions( """Test that system_instructions attribute is not set when instructions are not provided.""" client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] options = {"model_id": "Test"} # No instructions span_exporter.clear() response = await client.get_response(messages=messages, options=options) @@ -336,7 +336,7 @@ async def test_chat_client_observability_with_empty_instructions( """Test that system_instructions attribute is not set when instructions is an empty string.""" client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] options = {"model_id": "Test", "instructions": ""} # Empty string span_exporter.clear() response = await client.get_response(messages=messages, options=options) @@ -359,7 +359,7 @@ async def test_chat_client_observability_with_list_instructions( client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] options = {"model_id": "Test", "instructions": ["Instruction 1", "Instruction 2"]} span_exporter.clear() response = await client.get_response(messages=messages, options=options) @@ -380,7 +380,7 @@ async def test_chat_client_observability_with_list_instructions( async def test_chat_client_without_model_id_observability(mock_chat_client, span_exporter: InMemorySpanExporter): """Test telemetry shouldn't fail when the model_id is not provided for unknown reason.""" client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] span_exporter.clear() response = await client.get_response(messages=messages) @@ -399,7 +399,7 @@ async def test_chat_client_streaming_without_model_id_observability( ): """Test streaming telemetry shouldn't fail when the model_id is not provided for unknown reason.""" client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] span_exporter.clear() # Collect all yielded updates updates = [] @@ -448,7 +448,7 @@ def mock_chat_agent(): async def _run_impl(self, messages=None, *, thread=None, **kwargs): return AgentResponse( - messages=[ChatMessage("assistant", ["Agent response"])], + messages=[Message("assistant", ["Agent response"])], usage_details=UsageDetails(input_token_count=15, output_token_count=25), response_id="test_response_id", ) @@ -1261,7 +1261,7 @@ async def test_chat_client_observability_exception(mock_chat_client, span_export raise ValueError("Test error") client = FailingChatClient() - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] span_exporter.clear() with pytest.raises(ValueError, match="Test error"): @@ -1291,7 +1291,7 @@ async def test_chat_client_streaming_observability_exception(mock_chat_client, s return ResponseStream(_stream(), finalizer=ChatResponse.from_updates) client = FailingStreamingChatClient() - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] span_exporter.clear() with pytest.raises(ValueError, match="Streaming error"): @@ -1580,7 +1580,7 @@ async def test_agent_observability(span_exporter: InMemorySpanExporter, enable_s self._run_stream(messages=messages, thread=thread), finalizer=lambda x: AgentResponse.from_updates(x), ) - return AgentResponse(messages=[ChatMessage("assistant", ["Test response"])]) + return AgentResponse(messages=[Message("assistant", ["Test response"])]) async def _run_stream( self, @@ -1691,7 +1691,7 @@ async def test_agent_streaming_observability(span_exporter: InMemorySpanExporter return self._run_impl(messages=messages, **kwargs) async def _run_impl(self, messages=None, *, thread=None, **kwargs): - return AgentResponse(messages=[ChatMessage("assistant", ["Test"])]) + return AgentResponse(messages=[Message("assistant", ["Test"])]) def _run_stream_impl(self, messages=None, *, thread=None, **kwargs): async def _stream(): @@ -1767,13 +1767,13 @@ async def test_capture_messages_with_finish_reason(mock_chat_client, span_export class ClientWithFinishReason(mock_chat_client): async def _inner_get_response(self, *, messages, options, **kwargs): return ChatResponse( - messages=[ChatMessage(role="assistant", text="Done")], + messages=[Message(role="assistant", text="Done")], usage_details=UsageDetails(input_token_count=5, output_token_count=10), finish_reason="stop", ) client = ClientWithFinishReason() - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] span_exporter.clear() response = await client.get_response(messages=messages, model_id="Test") @@ -1863,7 +1863,7 @@ async def test_agent_streaming_exception(span_exporter: InMemorySpanExporter, en async def test_chat_client_when_disabled(mock_chat_client, span_exporter: InMemorySpanExporter): """Test that no spans are created when instrumentation is disabled.""" client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] span_exporter.clear() response = await client.get_response(messages=messages, model_id="Test") @@ -1878,7 +1878,7 @@ async def test_chat_client_when_disabled(mock_chat_client, span_exporter: InMemo async def test_chat_client_streaming_when_disabled(mock_chat_client, span_exporter: InMemorySpanExporter): """Test streaming creates no spans when instrumentation is disabled.""" client = mock_chat_client() - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] span_exporter.clear() updates = [] @@ -2208,14 +2208,14 @@ async def test_layer_ordering_span_sequence_with_function_calling(span_exporter: return "https://test.example.com" def _inner_get_response( - self, *, messages: MutableSequence[ChatMessage], stream: bool, options: dict[str, Any], **kwargs: Any + self, *, messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any ) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]: async def _get() -> ChatResponse: self.call_count += 1 if self.call_count == 1: return ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[ Content.from_function_call( @@ -2228,7 +2228,7 @@ async def test_layer_ordering_span_sequence_with_function_calling(span_exporter: ], ) return ChatResponse( - messages=[ChatMessage(role="assistant", text="The weather in Seattle is sunny!")], + messages=[Message(role="assistant", text="The weather in Seattle is sunny!")], ) return _get() @@ -2237,7 +2237,7 @@ async def test_layer_ordering_span_sequence_with_function_calling(span_exporter: span_exporter.clear() response = await client.get_response( - messages=[ChatMessage(role="user", text="What's the weather in Seattle?")], + messages=[Message(role="user", text="What's the weather in Seattle?")], options={"tools": [get_weather], "tool_choice": "auto"}, ) diff --git a/python/packages/core/tests/core/test_sessions.py b/python/packages/core/tests/core/test_sessions.py new file mode 100644 index 0000000000..bd3a22e70d --- /dev/null +++ b/python/packages/core/tests/core/test_sessions.py @@ -0,0 +1,421 @@ +# Copyright (c) Microsoft. All rights reserved. + +import json +from collections.abc import Sequence + +from agent_framework import Message +from agent_framework._sessions import ( + AgentSession, + BaseContextProvider, + BaseHistoryProvider, + InMemoryHistoryProvider, + SessionContext, +) + +# --------------------------------------------------------------------------- +# SessionContext tests +# --------------------------------------------------------------------------- + + +class TestSessionContext: + def test_init_defaults(self) -> None: + ctx = SessionContext(input_messages=[]) + assert ctx.session_id is None + assert ctx.service_session_id is None + assert ctx.input_messages == [] + assert ctx.context_messages == {} + assert ctx.instructions == [] + assert ctx.tools == [] + assert ctx.response is None + assert ctx.options == {} + assert ctx.metadata == {} + + def test_extend_messages_creates_key(self) -> None: + ctx = SessionContext(input_messages=[]) + msg = Message(role="user", contents=["hello"]) + ctx.extend_messages("rag", [msg]) + assert "rag" in ctx.context_messages + assert len(ctx.context_messages["rag"]) == 1 + assert ctx.context_messages["rag"][0].text == "hello" + + def test_extend_messages_appends_to_existing(self) -> None: + ctx = SessionContext(input_messages=[]) + msg1 = Message(role="user", contents=["first"]) + msg2 = Message(role="user", contents=["second"]) + ctx.extend_messages("src", [msg1]) + ctx.extend_messages("src", [msg2]) + assert len(ctx.context_messages["src"]) == 2 + + def test_extend_messages_preserves_source_order(self) -> None: + ctx = SessionContext(input_messages=[]) + ctx.extend_messages("a", [Message(role="user", contents=["a"])]) + ctx.extend_messages("b", [Message(role="user", contents=["b"])]) + ctx.extend_messages("c", [Message(role="user", contents=["c"])]) + assert list(ctx.context_messages.keys()) == ["a", "b", "c"] + + def test_extend_messages_sets_attribution(self) -> None: + ctx = SessionContext(input_messages=[]) + msg = Message(role="system", contents=["context"]) + ctx.extend_messages("rag", [msg]) + stored = ctx.context_messages["rag"][0] + assert stored.additional_properties["_attribution"] == {"source_id": "rag"} + # Original message is not mutated + assert "_attribution" not in msg.additional_properties + + def test_extend_messages_does_not_overwrite_existing_attribution(self) -> None: + ctx = SessionContext(input_messages=[]) + msg = Message( + role="system", contents=["context"], additional_properties={"_attribution": {"source_id": "custom"}} + ) + ctx.extend_messages("rag", [msg]) + stored = ctx.context_messages["rag"][0] + assert stored.additional_properties["_attribution"] == {"source_id": "custom"} + + def test_extend_messages_copies_messages(self) -> None: + ctx = SessionContext(input_messages=[]) + msg = Message(role="user", contents=["hello"]) + ctx.extend_messages("src", [msg]) + stored = ctx.context_messages["src"][0] + assert stored is not msg + assert stored.text == "hello" + # Mutating stored copy does not affect original + stored.additional_properties["extra"] = True + assert "extra" not in msg.additional_properties + + def test_extend_messages_sender_sets_source_type(self) -> None: + class MyProvider: + source_id = "rag" + + ctx = SessionContext(input_messages=[]) + msg = Message(role="system", contents=["ctx"]) + ctx.extend_messages(MyProvider(), [msg]) + stored = ctx.context_messages["rag"][0] + assert stored.additional_properties["_attribution"] == {"source_id": "rag", "source_type": "MyProvider"} + + def test_extend_instructions_string(self) -> None: + ctx = SessionContext(input_messages=[]) + ctx.extend_instructions("sys", "Be helpful") + assert ctx.instructions == ["Be helpful"] + + def test_extend_instructions_sequence(self) -> None: + ctx = SessionContext(input_messages=[]) + ctx.extend_instructions("sys", ["Be helpful", "Be concise"]) + assert ctx.instructions == ["Be helpful", "Be concise"] + + def test_get_messages_all(self) -> None: + ctx = SessionContext(input_messages=[]) + ctx.extend_messages("a", [Message(role="user", contents=["a"])]) + ctx.extend_messages("b", [Message(role="user", contents=["b"])]) + result = ctx.get_messages() + assert len(result) == 2 + assert result[0].text == "a" + assert result[1].text == "b" + + def test_get_messages_filter_sources(self) -> None: + ctx = SessionContext(input_messages=[]) + ctx.extend_messages("a", [Message(role="user", contents=["a"])]) + ctx.extend_messages("b", [Message(role="user", contents=["b"])]) + result = ctx.get_messages(sources=["a"]) + assert len(result) == 1 + assert result[0].text == "a" + + def test_get_messages_exclude_sources(self) -> None: + ctx = SessionContext(input_messages=[]) + ctx.extend_messages("a", [Message(role="user", contents=["a"])]) + ctx.extend_messages("b", [Message(role="user", contents=["b"])]) + result = ctx.get_messages(exclude_sources=["a"]) + assert len(result) == 1 + assert result[0].text == "b" + + def test_get_messages_include_input(self) -> None: + input_msg = Message(role="user", contents=["input"]) + ctx = SessionContext(input_messages=[input_msg]) + ctx.extend_messages("a", [Message(role="user", contents=["context"])]) + result = ctx.get_messages(include_input=True) + assert len(result) == 2 + assert result[1].text == "input" + + def test_get_messages_include_response(self) -> None: + from agent_framework import AgentResponse + + ctx = SessionContext(input_messages=[]) + ctx._response = AgentResponse(messages=[Message(role="assistant", contents=["reply"])]) + result = ctx.get_messages(include_response=True) + assert len(result) == 1 + assert result[0].text == "reply" + + def test_response_readonly(self) -> None: + ctx = SessionContext(input_messages=[]) + assert ctx.response is None + # Can set via _response internally + from agent_framework import AgentResponse + + resp = AgentResponse(messages=[]) + ctx._response = resp + assert ctx.response is resp + + +# --------------------------------------------------------------------------- +# BaseContextProvider tests +# --------------------------------------------------------------------------- + + +class TestContextProviderBase: + def test_source_id_required(self) -> None: + provider = BaseContextProvider(source_id="test") + assert provider.source_id == "test" + + async def test_before_run_is_noop(self) -> None: + provider = BaseContextProvider(source_id="test") + session = AgentSession() + ctx = SessionContext(input_messages=[]) + # Should not raise + await provider.before_run(agent=None, session=session, context=ctx, state={}) # type: ignore[arg-type] + + async def test_after_run_is_noop(self) -> None: + provider = BaseContextProvider(source_id="test") + session = AgentSession() + ctx = SessionContext(input_messages=[]) + await provider.after_run(agent=None, session=session, context=ctx, state={}) # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- +# BaseHistoryProvider tests +# --------------------------------------------------------------------------- + + +class ConcreteHistoryProvider(BaseHistoryProvider): + """Concrete test implementation.""" + + def __init__(self, source_id: str, stored_messages: list[Message] | None = None, **kwargs) -> None: + super().__init__(source_id, **kwargs) + self.stored: list[Message] = [] + self._stored_messages = stored_messages or [] + + async def get_messages(self, session_id: str | None, **kwargs) -> list[Message]: + return list(self._stored_messages) + + async def save_messages(self, session_id: str | None, messages: Sequence[Message], **kwargs) -> None: + self.stored.extend(messages) + + +class TestHistoryProviderBase: + def test_default_flags(self) -> None: + provider = ConcreteHistoryProvider("mem") + assert provider.load_messages is True + assert provider.store_outputs is True + assert provider.store_inputs is True + assert provider.store_context_messages is False + assert provider.store_context_from is None + + def test_custom_flags(self) -> None: + provider = ConcreteHistoryProvider( + "audit", + load_messages=False, + store_inputs=False, + store_context_messages=True, + store_context_from={"rag"}, + ) + assert provider.load_messages is False + assert provider.store_inputs is False + assert provider.store_context_messages is True + assert provider.store_context_from == {"rag"} + + async def test_before_run_loads_messages(self) -> None: + msgs = [Message(role="user", contents=["history"])] + provider = ConcreteHistoryProvider("mem", stored_messages=msgs) + session = AgentSession() + ctx = SessionContext(session_id="s1", input_messages=[]) + await provider.before_run(agent=None, session=session, context=ctx, state={}) # type: ignore[arg-type] + assert len(ctx.context_messages["mem"]) == 1 + assert ctx.context_messages["mem"][0].text == "history" + + async def test_after_run_stores_inputs_and_responses(self) -> None: + from agent_framework import AgentResponse + + provider = ConcreteHistoryProvider("mem") + session = AgentSession() + input_msg = Message(role="user", contents=["hello"]) + resp_msg = Message(role="assistant", contents=["hi"]) + ctx = SessionContext(session_id="s1", input_messages=[input_msg]) + ctx._response = AgentResponse(messages=[resp_msg]) + await provider.after_run(agent=None, session=session, context=ctx, state={}) # type: ignore[arg-type] + assert len(provider.stored) == 2 + assert provider.stored[0].text == "hello" + assert provider.stored[1].text == "hi" + + async def test_after_run_skips_inputs_when_disabled(self) -> None: + from agent_framework import AgentResponse + + provider = ConcreteHistoryProvider("mem", store_inputs=False) + ctx = SessionContext(session_id="s1", input_messages=[Message(role="user", contents=["hello"])]) + ctx._response = AgentResponse(messages=[Message(role="assistant", contents=["hi"])]) + await provider.after_run(agent=None, session=AgentSession(), context=ctx, state={}) # type: ignore[arg-type] + assert len(provider.stored) == 1 + assert provider.stored[0].text == "hi" + + async def test_after_run_skips_responses_when_disabled(self) -> None: + from agent_framework import AgentResponse + + provider = ConcreteHistoryProvider("mem", store_outputs=False) + ctx = SessionContext(session_id="s1", input_messages=[Message(role="user", contents=["hello"])]) + ctx._response = AgentResponse(messages=[Message(role="assistant", contents=["hi"])]) + await provider.after_run(agent=None, session=AgentSession(), context=ctx, state={}) # type: ignore[arg-type] + assert len(provider.stored) == 1 + assert provider.stored[0].text == "hello" + + async def test_after_run_stores_context_messages(self) -> None: + from agent_framework import AgentResponse + + provider = ConcreteHistoryProvider("audit", load_messages=False, store_context_messages=True) + ctx = SessionContext(session_id="s1", input_messages=[Message(role="user", contents=["hello"])]) + ctx.extend_messages("rag", [Message(role="system", contents=["context"])]) + ctx._response = AgentResponse(messages=[Message(role="assistant", contents=["hi"])]) + await provider.after_run(agent=None, session=AgentSession(), context=ctx, state={}) # type: ignore[arg-type] + # Should store: context from rag + input + response + texts = [m.text for m in provider.stored] + assert "context" in texts + assert "hello" in texts + assert "hi" in texts + + async def test_after_run_stores_context_from_specific_sources(self) -> None: + from agent_framework import AgentResponse + + provider = ConcreteHistoryProvider( + "audit", load_messages=False, store_context_messages=True, store_context_from={"rag"} + ) + ctx = SessionContext(session_id="s1", input_messages=[]) + ctx.extend_messages("rag", [Message(role="system", contents=["rag-context"])]) + ctx.extend_messages("other", [Message(role="system", contents=["other-context"])]) + ctx._response = AgentResponse(messages=[]) + await provider.after_run(agent=None, session=AgentSession(), context=ctx, state={}) # type: ignore[arg-type] + texts = [m.text for m in provider.stored] + assert "rag-context" in texts + assert "other-context" not in texts + + +# --------------------------------------------------------------------------- +# AgentSession tests +# --------------------------------------------------------------------------- + + +class TestAgentSession: + def test_auto_generates_session_id(self) -> None: + session = AgentSession() + assert session.session_id is not None + assert len(session.session_id) > 0 + + def test_custom_session_id(self) -> None: + session = AgentSession(session_id="custom-123") + assert session.session_id == "custom-123" + + def test_state_starts_empty(self) -> None: + session = AgentSession() + assert session.state == {} + + def test_service_session_id(self) -> None: + session = AgentSession(service_session_id="svc-456") + assert session.service_session_id == "svc-456" + + def test_to_dict(self) -> None: + session = AgentSession(session_id="s1", service_session_id="svc1") + session.state = {"key": "value"} + d = session.to_dict() + assert d["type"] == "session" + assert d["session_id"] == "s1" + assert d["service_session_id"] == "svc1" + assert d["state"] == {"key": "value"} + + def test_from_dict(self) -> None: + data = { + "type": "session", + "session_id": "s1", + "service_session_id": "svc1", + "state": {"key": "value"}, + } + session = AgentSession.from_dict(data) + assert session.session_id == "s1" + assert session.service_session_id == "svc1" + assert session.state == {"key": "value"} + + def test_roundtrip(self) -> None: + session = AgentSession(session_id="rt-1") + session.state = {"messages": ["a", "b"], "count": 42} + json_str = json.dumps(session.to_dict()) + restored = AgentSession.from_dict(json.loads(json_str)) + assert restored.session_id == "rt-1" + assert restored.state == {"messages": ["a", "b"], "count": 42} + + def test_from_dict_missing_state(self) -> None: + data = {"session_id": "s1"} + session = AgentSession.from_dict(data) + assert session.state == {} + + +# --------------------------------------------------------------------------- +# InMemoryHistoryProvider tests +# --------------------------------------------------------------------------- + + +class TestInMemoryHistoryProvider: + async def test_empty_state_returns_no_messages(self) -> None: + provider = InMemoryHistoryProvider("memory") + session = AgentSession() + ctx = SessionContext(session_id="s1", input_messages=[]) + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + assert ctx.context_messages.get("memory", []) == [] + + async def test_stores_and_loads_messages(self) -> None: + from agent_framework import AgentResponse + + provider = InMemoryHistoryProvider("memory") + session = AgentSession() + + # First run: send input, get response + input_msg = Message(role="user", contents=["hello"]) + resp_msg = Message(role="assistant", contents=["hi there"]) + ctx1 = SessionContext(session_id="s1", input_messages=[input_msg]) + await provider.before_run(agent=None, session=session, context=ctx1, state=session.state) # type: ignore[arg-type] + ctx1._response = AgentResponse(messages=[resp_msg]) + await provider.after_run(agent=None, session=session, context=ctx1, state=session.state) # type: ignore[arg-type] + + # Second run: should load previous messages + ctx2 = SessionContext(session_id="s1", input_messages=[Message(role="user", contents=["again"])]) + await provider.before_run(agent=None, session=session, context=ctx2, state=session.state) # type: ignore[arg-type] + loaded = ctx2.context_messages.get("memory", []) + assert len(loaded) == 2 + assert loaded[0].text == "hello" + assert loaded[1].text == "hi there" + + async def test_state_is_serializable(self) -> None: + from agent_framework import AgentResponse + + provider = InMemoryHistoryProvider("memory") + session = AgentSession() + + input_msg = Message(role="user", contents=["test"]) + ctx = SessionContext(session_id="s1", input_messages=[input_msg]) + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + ctx._response = AgentResponse(messages=[Message(role="assistant", contents=["reply"])]) + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + # State contains Message objects (not dicts) + assert isinstance(session.state["memory"]["messages"][0], Message) + + # to_dict() serializes them via SerializationProtocol + session_dict = session.to_dict() + json_str = json.dumps(session_dict) + assert json_str # no error + + # Round-trip through session serialization restores Message objects + restored = AgentSession.from_dict(json.loads(json_str)) + assert isinstance(restored.state["memory"]["messages"][0], Message) + assert restored.state["memory"]["messages"][0].text == "test" + assert restored.state["memory"]["messages"][1].text == "reply" + + async def test_source_id_attribution(self) -> None: + provider = InMemoryHistoryProvider("custom-source") + assert provider.source_id == "custom-source" + ctx = SessionContext(session_id="s1", input_messages=[]) + ctx.extend_messages("custom-source", [Message(role="user", contents=["test"])]) + assert "custom-source" in ctx.context_messages diff --git a/python/packages/core/tests/core/test_threads.py b/python/packages/core/tests/core/test_threads.py index a891f6b440..5b3fc5ffd1 100644 --- a/python/packages/core/tests/core/test_threads.py +++ b/python/packages/core/tests/core/test_threads.py @@ -5,7 +5,7 @@ from typing import Any import pytest -from agent_framework import AgentThread, ChatMessage, ChatMessageStore +from agent_framework import AgentThread, ChatMessageStore, Message from agent_framework._threads import AgentThreadState, ChatMessageStoreState from agent_framework.exceptions import AgentThreadException @@ -13,15 +13,15 @@ from agent_framework.exceptions import AgentThreadException class MockChatMessageStore: """Mock implementation of ChatMessageStoreProtocol for testing.""" - def __init__(self, messages: list[ChatMessage] | None = None) -> None: + def __init__(self, messages: list[Message] | None = None) -> None: self._messages = messages or [] self._serialize_calls = 0 self._deserialize_calls = 0 - async def list_messages(self) -> list[ChatMessage]: + async def list_messages(self) -> list[Message]: return self._messages - async def add_messages(self, messages: Sequence[ChatMessage]) -> None: + async def add_messages(self, messages: Sequence[Message]) -> None: self._messages.extend(messages) async def serialize(self, **kwargs: Any) -> Any: @@ -41,19 +41,19 @@ class MockChatMessageStore: @pytest.fixture -def sample_messages() -> list[ChatMessage]: +def sample_messages() -> list[Message]: """Fixture providing sample chat messages for testing.""" return [ - ChatMessage(role="user", text="Hello", message_id="msg1"), - ChatMessage(role="assistant", text="Hi there!", message_id="msg2"), - ChatMessage(role="user", text="How are you?", message_id="msg3"), + Message(role="user", text="Hello", message_id="msg1"), + Message(role="assistant", text="Hi there!", message_id="msg2"), + Message(role="user", text="How are you?", message_id="msg3"), ] @pytest.fixture -def sample_message() -> ChatMessage: +def sample_message() -> Message: """Fixture providing a single sample chat message for testing.""" - return ChatMessage(role="user", text="Test message", message_id="test1") + return Message(role="user", text="Test message", message_id="test1") class TestAgentThread: @@ -124,14 +124,14 @@ class TestAgentThread: thread.message_store = None # Should not raise error assert thread.message_store is None - async def test_get_messages_with_message_store(self, sample_messages: list[ChatMessage]) -> None: + async def test_get_messages_with_message_store(self, sample_messages: list[Message]) -> None: """Test get_messages when message_store is set.""" store = ChatMessageStore(sample_messages) thread = AgentThread(message_store=store) assert thread.message_store is not None - messages: list[ChatMessage] = await thread.message_store.list_messages() + messages: list[Message] = await thread.message_store.list_messages() assert messages is not None assert len(messages) == 3 @@ -145,7 +145,7 @@ class TestAgentThread: assert thread.message_store is None - async def test_on_new_messages_with_service_thread_id(self, sample_message: ChatMessage) -> None: + async def test_on_new_messages_with_service_thread_id(self, sample_message: Message) -> None: """Test _on_new_messages when service_thread_id is set (should do nothing).""" thread = AgentThread(service_thread_id="test-conv") @@ -154,7 +154,7 @@ class TestAgentThread: # Should not create a message store assert thread.message_store is None - async def test_on_new_messages_single_message_creates_store(self, sample_message: ChatMessage) -> None: + async def test_on_new_messages_single_message_creates_store(self, sample_message: Message) -> None: """Test _on_new_messages with single message creates ChatMessageStore.""" thread = AgentThread() @@ -166,7 +166,7 @@ class TestAgentThread: assert len(messages) == 1 assert messages[0].text == "Test message" - async def test_on_new_messages_multiple_messages(self, sample_messages: list[ChatMessage]) -> None: + async def test_on_new_messages_multiple_messages(self, sample_messages: list[Message]) -> None: """Test _on_new_messages with multiple messages.""" thread = AgentThread() @@ -176,9 +176,9 @@ class TestAgentThread: messages = await thread.message_store.list_messages() assert len(messages) == 3 - async def test_on_new_messages_with_existing_store(self, sample_message: ChatMessage) -> None: + async def test_on_new_messages_with_existing_store(self, sample_message: Message) -> None: """Test _on_new_messages adds to existing message store.""" - initial_messages = [ChatMessage(role="user", text="Initial", message_id="init1")] + initial_messages = [Message(role="user", text="Initial", message_id="init1")] store = ChatMessageStore(initial_messages) thread = AgentThread(message_store=store) @@ -199,7 +199,7 @@ class TestAgentThread: assert thread.service_thread_id == "test-conv-123" assert thread.message_store is None - async def test_deserialize_with_store_state(self, sample_messages: list[ChatMessage]) -> None: + async def test_deserialize_with_store_state(self, sample_messages: list[Message]) -> None: """Test _deserialize with chat_message_store_state.""" store_state = {"messages": sample_messages} serialized_data = {"service_thread_id": None, "chat_message_store_state": store_state} @@ -226,7 +226,7 @@ class TestAgentThread: thread = AgentThread(message_store=store) serialized_data: dict[str, Any] = { "service_thread_id": None, - "chat_message_store_state": {"messages": [ChatMessage(role="user", text="test")]}, + "chat_message_store_state": {"messages": [Message(role="user", text="test")]}, } await thread.update_from_thread_state(serialized_data) @@ -272,7 +272,7 @@ class TestAgentThread: assert store._serialize_calls == 1 # pyright: ignore[reportPrivateUsage] - async def test_serialize_round_trip_messages(self, sample_messages: list[ChatMessage]) -> None: + async def test_serialize_round_trip_messages(self, sample_messages: list[Message]) -> None: """Test a roundtrip of the serialization.""" store = ChatMessageStore(sample_messages) thread = AgentThread(message_store=store) @@ -298,12 +298,12 @@ class TestChatMessageList: store = ChatMessageStore() assert len(store.messages) == 0 - def test_init_with_messages(self, sample_messages: list[ChatMessage]) -> None: + def test_init_with_messages(self, sample_messages: list[Message]) -> None: """Test ChatMessageStore initialization with messages.""" store = ChatMessageStore(sample_messages) assert len(store.messages) == 3 - async def test_add_messages(self, sample_messages: list[ChatMessage]) -> None: + async def test_add_messages(self, sample_messages: list[Message]) -> None: """Test adding messages to the store.""" store = ChatMessageStore() @@ -313,7 +313,7 @@ class TestChatMessageList: messages = await store.list_messages() assert messages[0].text == "Hello" - async def test_get_messages(self, sample_messages: list[ChatMessage]) -> None: + async def test_get_messages(self, sample_messages: list[Message]) -> None: """Test getting messages from the store.""" store = ChatMessageStore(sample_messages) @@ -322,7 +322,7 @@ class TestChatMessageList: assert len(messages) == 3 assert messages[0].message_id == "msg1" - async def test_serialize_state(self, sample_messages: list[ChatMessage]) -> None: + async def test_serialize_state(self, sample_messages: list[Message]) -> None: """Test serializing store state.""" store = ChatMessageStore(sample_messages) @@ -340,7 +340,7 @@ class TestChatMessageList: assert "messages" in result assert len(result["messages"]) == 0 - async def test_deserialize_state(self, sample_messages: list[ChatMessage]) -> None: + async def test_deserialize_state(self, sample_messages: list[Message]) -> None: """Test deserializing store state.""" store = ChatMessageStore() state_data = {"messages": sample_messages} @@ -371,7 +371,7 @@ class TestChatMessageList: class TestStoreState: """Test cases for ChatMessageStoreState class.""" - def test_init(self, sample_messages: list[ChatMessage]) -> None: + def test_init(self, sample_messages: list[Message]) -> None: """Test ChatMessageStoreState initialization.""" state = ChatMessageStoreState(messages=sample_messages) @@ -449,7 +449,7 @@ class TestThreadState: def test_init_with_chat_message_store_state_object(self) -> None: """Test AgentThreadState initialization with ChatMessageStoreState object.""" - store_state = ChatMessageStoreState(messages=[ChatMessage(role="user", text="test")]) + store_state = ChatMessageStoreState(messages=[Message(role="user", text="test")]) state = AgentThreadState(chat_message_store_state=store_state) assert state.service_thread_id is None @@ -479,7 +479,7 @@ class TestChatMessageStoreStateEdgeCases: state = ChatMessageStoreState(messages=messages) assert len(state.messages) == 2 - assert isinstance(state.messages[0], ChatMessage) + assert isinstance(state.messages[0], Message) assert state.messages[0].text == "Hello" @@ -551,7 +551,7 @@ class TestAgentThreadEdgeCases: """Test deserialize raises AgentThreadException when message store fails.""" class FailingStore: - async def add_messages(self, messages: Sequence[ChatMessage], **kwargs: Any) -> None: + async def add_messages(self, messages: Sequence[Message], **kwargs: Any) -> None: raise RuntimeError("Store failed") serialized_data = { diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index 0a616a35fc..436ae7fdd1 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -10,10 +10,6 @@ from pydantic import BaseModel, ValidationError from agent_framework import ( Content, FunctionTool, - HostedCodeInterpreterTool, - HostedImageGenerationTool, - HostedMCPTool, - ToolProtocol, tool, ) from agent_framework._tools import ( @@ -21,7 +17,6 @@ from agent_framework._tools import ( _parse_annotation, _parse_inputs, ) -from agent_framework.exceptions import ToolException from agent_framework.observability import OtelAttr # region FunctionTool and tool decorator tests @@ -35,7 +30,6 @@ def test_tool_decorator(): """A simple function that adds two numbers.""" return x + y - assert isinstance(test_tool, ToolProtocol) assert isinstance(test_tool, FunctionTool) assert test_tool.name == "test_tool" assert test_tool.description == "A test tool" @@ -56,7 +50,6 @@ def test_tool_decorator_without_args(): """A simple function that adds two numbers.""" return x + y - assert isinstance(test_tool, ToolProtocol) assert isinstance(test_tool, FunctionTool) assert test_tool.name == "test_tool" assert test_tool.description == "A simple function that adds two numbers." @@ -174,7 +167,7 @@ def test_tool_without_args(): """A simple function that adds two numbers.""" return 1 + 2 - assert isinstance(test_tool, ToolProtocol) + assert isinstance(test_tool, FunctionTool) assert isinstance(test_tool, FunctionTool) assert test_tool.name == "test_tool" assert test_tool.description == "A simple function that adds two numbers." @@ -194,7 +187,6 @@ async def test_tool_decorator_with_async(): """An async function that adds two numbers.""" return x + y - assert isinstance(async_test_tool, ToolProtocol) assert isinstance(async_test_tool, FunctionTool) assert async_test_tool.name == "async_test_tool" assert async_test_tool.description == "An async test tool" @@ -218,7 +210,6 @@ def test_tool_decorator_in_class(): test_tool = my_tools().test_tool - assert isinstance(test_tool, ToolProtocol) assert isinstance(test_tool, FunctionTool) assert test_tool.name == "test_tool" assert test_tool.description == "A test tool" @@ -701,30 +692,7 @@ def test_tool_serialization(): assert restored_tool_2(10, 4) == 6 -# region HostedCodeInterpreterTool and _parse_inputs - - -def test_hosted_code_interpreter_tool_default(): - """Test HostedCodeInterpreterTool with default parameters.""" - tool = HostedCodeInterpreterTool() - - assert tool.name == "code_interpreter" - assert tool.inputs == [] - assert tool.description == "" - assert tool.additional_properties is None - assert str(tool) == "HostedCodeInterpreterTool(name=code_interpreter)" - - -def test_hosted_code_interpreter_tool_with_description(): - """Test HostedCodeInterpreterTool with description and additional properties.""" - tool = HostedCodeInterpreterTool( - description="A test code interpreter", - additional_properties={"version": "1.0", "language": "python"}, - ) - - assert tool.name == "code_interpreter" - assert tool.description == "A test code interpreter" - assert tool.additional_properties == {"version": "1.0", "language": "python"} +# region _parse_inputs tests def test_parse_inputs_none(): @@ -853,185 +821,7 @@ def test_parse_inputs_unsupported_type(): _parse_inputs(123) -def test_hosted_code_interpreter_tool_with_string_input(): - """Test HostedCodeInterpreterTool with string input.""" - - tool = HostedCodeInterpreterTool(inputs="http://example.com") - - assert len(tool.inputs) == 1 - assert tool.inputs[0].type == "uri" - assert tool.inputs[0].uri == "http://example.com" - - -def test_hosted_code_interpreter_tool_with_dict_inputs(): - """Test HostedCodeInterpreterTool with dictionary inputs.""" - - inputs = [{"uri": "http://example.com", "media_type": "text/html"}, {"file_id": "file-123"}] - - tool = HostedCodeInterpreterTool(inputs=inputs) - - assert len(tool.inputs) == 2 - assert tool.inputs[0].type == "uri" - assert tool.inputs[0].uri == "http://example.com" - assert tool.inputs[0].media_type == "text/html" - assert tool.inputs[1].type == "hosted_file" - assert tool.inputs[1].file_id == "file-123" - - -def test_hosted_code_interpreter_tool_with_ai_contents(): - """Test HostedCodeInterpreterTool with Content instances.""" - - inputs = [Content.from_text(text="Hello, world!"), Content.from_data(data=b"test", media_type="text/plain")] - - tool = HostedCodeInterpreterTool(inputs=inputs) - - assert len(tool.inputs) == 2 - assert tool.inputs[0].type == "text" - assert tool.inputs[0].text == "Hello, world!" - assert tool.inputs[1].type == "data" - assert tool.inputs[1].media_type == "text/plain" - - -def test_hosted_code_interpreter_tool_with_single_input(): - """Test HostedCodeInterpreterTool with single input (not in list).""" - - input_dict = {"file_id": "file-single"} - tool = HostedCodeInterpreterTool(inputs=input_dict) - - assert len(tool.inputs) == 1 - assert tool.inputs[0].type == "hosted_file" - assert tool.inputs[0].file_id == "file-single" - - -def test_hosted_code_interpreter_tool_with_unknown_input(): - """Test HostedCodeInterpreterTool with single unknown input.""" - with pytest.raises(ValueError, match="Unsupported input type"): - HostedCodeInterpreterTool(inputs={"hosted_file": "file-single"}) - - -def test_hosted_image_generation_tool_defaults(): - """HostedImageGenerationTool should default name and empty description.""" - tool = HostedImageGenerationTool() - - assert tool.name == "image_generation" - assert tool.description == "" - assert tool.options is None - assert str(tool) == "HostedImageGenerationTool(name=image_generation)" - - -def test_hosted_image_generation_tool_with_options(): - """HostedImageGenerationTool should store options.""" - tool = HostedImageGenerationTool( - description="Generate images", - options={"format": "png", "size": "1024x1024"}, - additional_properties={"quality": "high"}, - ) - - assert tool.name == "image_generation" - assert tool.description == "Generate images" - assert tool.options == {"format": "png", "size": "1024x1024"} - assert tool.additional_properties == {"quality": "high"} - - -# region HostedMCPTool tests - - -def test_hosted_mcp_tool_with_other_fields(): - """Test creating a HostedMCPTool with a specific approval dict, headers and additional properties.""" - tool = HostedMCPTool( - name="mcp-tool", - url="https://mcp.example", - description="A test MCP tool", - headers={"x": "y"}, - additional_properties={"p": 1}, - ) - - assert tool.name == "mcp-tool" - # pydantic AnyUrl preserves as string-like - assert str(tool.url).startswith("https://") - assert tool.headers == {"x": "y"} - assert tool.additional_properties == {"p": 1} - assert tool.description == "A test MCP tool" - - -@pytest.mark.parametrize( - "approval_mode", - [ - "always_require", - "never_require", - { - "always_require_approval": {"toolA"}, - "never_require_approval": {"toolB"}, - }, - { - "always_require_approval": ["toolA"], - "never_require_approval": ("toolB",), - }, - ], - ids=["always_require", "never_require", "specific", "specific_with_parsing"], -) -def test_hosted_mcp_tool_with_approval_mode(approval_mode: str | dict[str, Any]): - """Test creating a HostedMCPTool with a specific approval dict, headers and additional properties.""" - tool = HostedMCPTool(name="mcp-tool", url="https://mcp.example", approval_mode=approval_mode) - - assert tool.name == "mcp-tool" - # pydantic AnyUrl preserves as string-like - assert str(tool.url).startswith("https://") - if not isinstance(approval_mode, dict): - assert tool.approval_mode == approval_mode - else: - # approval_mode parsed to sets - assert isinstance(tool.approval_mode["always_require_approval"], set) - assert isinstance(tool.approval_mode["never_require_approval"], set) - assert "toolA" in tool.approval_mode["always_require_approval"] - assert "toolB" in tool.approval_mode["never_require_approval"] - - -def test_hosted_mcp_tool_invalid_approval_mode_raises(): - """Invalid approval_mode string should raise ServiceInitializationError.""" - with pytest.raises(ToolException): - HostedMCPTool(name="bad", url="https://x", approval_mode="invalid_mode") - - -@pytest.mark.parametrize( - "tools", - [ - {"toolA", "toolB"}, - ("toolA", "toolB"), - ["toolA", "toolB"], - ["toolA", "toolB", "toolA"], - ], - ids=[ - "set", - "tuple", - "list", - "list_with_duplicates", - ], -) -def test_hosted_mcp_tool_with_allowed_tools(tools: list[str] | tuple[str, ...] | set[str]): - """Test creating a HostedMCPTool with a list of allowed tools.""" - tool = HostedMCPTool( - name="mcp-tool", - url="https://mcp.example", - allowed_tools=tools, - ) - - assert tool.name == "mcp-tool" - # pydantic AnyUrl preserves as string-like - assert str(tool.url).startswith("https://") - # approval_mode parsed to set - assert isinstance(tool.allowed_tools, set) - assert tool.allowed_tools == {"toolA", "toolB"} - - -def test_hosted_mcp_tool_with_dict_of_allowed_tools(): - """Test creating a HostedMCPTool with a dict of allowed tools.""" - with pytest.raises(ToolException): - HostedMCPTool( - name="mcp-tool", - url="https://mcp.example", - allowed_tools={"toolA": "Tool A", "toolC": "Tool C"}, - ) +# endregion async def test_ai_function_with_kwargs_injection(): diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 3fe9a1cf88..0be7b123bd 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -14,15 +14,15 @@ from agent_framework import ( AgentResponse, AgentResponseUpdate, Annotation, - ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, Content, + FunctionTool, + Message, ResponseStream, TextSpanRegion, ToolMode, - ToolProtocol, UsageDetails, detect_media_type_from_base64, merge_chat_options, @@ -41,26 +41,20 @@ from agent_framework.exceptions import ContentError @fixture -def ai_tool() -> ToolProtocol: - """Returns a generic ToolProtocol.""" +def ai_tool() -> FunctionTool: + """Returns a generic FunctionTool.""" - class GenericTool(BaseModel): - name: str - description: str | None = None - additional_properties: dict[str, Any] | None = None + @tool + def generic_tool(name: str) -> str: + """A generic tool that echoes the name.""" + return f"Hello, {name}" - def parameters(self) -> dict[str, Any]: - """Return the parameters of the tool as a JSON schema.""" - return { - "name": {"type": "string"}, - } - - return GenericTool(name="generic_tool", description="A generic tool") + return generic_tool @fixture -def tool_tool() -> ToolProtocol: - """Returns a executable ToolProtocol.""" +def tool_tool() -> FunctionTool: + """Returns a executable FunctionTool.""" @tool def simple_function(x: int, y: int) -> int: @@ -566,13 +560,13 @@ def test_ai_content_serialization(args: dict): assert content == deserialized -# region ChatMessage +# region Message def test_chat_message_text(): - """Test the ChatMessage class to ensure it initializes correctly with text content.""" - # Create a ChatMessage with a role and text content - message = ChatMessage(role="user", text="Hello, how are you?") + """Test the Message class to ensure it initializes correctly with text content.""" + # Create a Message with a role and text content + message = Message(role="user", text="Hello, how are you?") # Check the type and content assert message.role == "user" @@ -586,11 +580,11 @@ def test_chat_message_text(): def test_chat_message_contents(): - """Test the ChatMessage class to ensure it initializes correctly with contents.""" - # Create a ChatMessage with a role and multiple contents + """Test the Message class to ensure it initializes correctly with contents.""" + # Create a Message with a role and multiple contents content1 = Content.from_text("Hello, how are you?") content2 = Content.from_text("I'm fine, thank you!") - message = ChatMessage(role="user", contents=[content1, content2]) + message = Message(role="user", contents=[content1, content2]) # Check the type and content assert message.role == "user" @@ -603,7 +597,7 @@ def test_chat_message_contents(): def test_chat_message_with_chatrole_instance(): - m = ChatMessage(role="user", text="hi") + m = Message(role="user", text="hi") assert m.role == "user" assert m.text == "hi" @@ -613,8 +607,8 @@ def test_chat_message_with_chatrole_instance(): def test_chat_response(): """Test the ChatResponse class to ensure it initializes correctly with a message.""" - # Create a ChatMessage - message = ChatMessage(role="assistant", text="I'm doing well, thank you!") + # Create a Message + message = Message(role="assistant", text="I'm doing well, thank you!") # Create a ChatResponse with the message response = ChatResponse(messages=message) @@ -622,7 +616,7 @@ def test_chat_response(): # Check the type and content assert response.messages[0].role == "assistant" assert response.messages[0].text == "I'm doing well, thank you!" - assert isinstance(response.messages[0], ChatMessage) + assert isinstance(response.messages[0], Message) # __str__ returns text assert str(response) == response.text @@ -633,8 +627,8 @@ class OutputModel(BaseModel): def test_chat_response_with_format(): """Test the ChatResponse class to ensure it initializes correctly with a message.""" - # Create a ChatMessage - message = ChatMessage(role="assistant", text='{"response": "Hello"}') + # Create a Message + message = Message(role="assistant", text='{"response": "Hello"}') # Create a ChatResponse with the message response = ChatResponse(messages=message, response_format=OutputModel) @@ -642,7 +636,7 @@ def test_chat_response_with_format(): # Check the type and content assert response.messages[0].role == "assistant" assert response.messages[0].text == '{"response": "Hello"}' - assert isinstance(response.messages[0], ChatMessage) + assert isinstance(response.messages[0], Message) assert response.text == '{"response": "Hello"}' assert response.value is not None assert response.value.response == "Hello" @@ -650,8 +644,8 @@ def test_chat_response_with_format(): def test_chat_response_with_format_init(): """Test the ChatResponse class to ensure it initializes correctly with a message.""" - # Create a ChatMessage - message = ChatMessage(role="assistant", text='{"response": "Hello"}') + # Create a Message + message = Message(role="assistant", text='{"response": "Hello"}') # Create a ChatResponse with the message response = ChatResponse(messages=message, response_format=OutputModel) @@ -659,7 +653,7 @@ def test_chat_response_with_format_init(): # Check the type and content assert response.messages[0].role == "assistant" assert response.messages[0].text == '{"response": "Hello"}' - assert isinstance(response.messages[0], ChatMessage) + assert isinstance(response.messages[0], Message) assert response.text == '{"response": "Hello"}' assert response.value is not None assert response.value.response == "Hello" @@ -673,7 +667,7 @@ def test_chat_response_value_raises_on_invalid_schema(): name: str = Field(min_length=10) score: int = Field(gt=0, le=100) - message = ChatMessage(role="assistant", text='{"id": 1, "name": "test", "score": -5}') + message = Message(role="assistant", text='{"id": 1, "name": "test", "score": -5}') response = ChatResponse(messages=message, response_format=StrictSchema) with raises(ValidationError) as exc_info: @@ -694,7 +688,7 @@ def test_agent_response_value_raises_on_invalid_schema(): name: str = Field(min_length=10) score: int = Field(gt=0, le=100) - message = ChatMessage(role="assistant", text='{"id": 1, "name": "test", "score": -5}') + message = Message(role="assistant", text='{"id": 1, "name": "test", "score": -5}') response = AgentResponse(messages=message, response_format=StrictSchema) with raises(ValidationError) as exc_info: @@ -712,7 +706,7 @@ def test_agent_response_value_raises_on_invalid_schema(): def test_chat_response_update(): """Test the ChatResponseUpdate class to ensure it initializes correctly with a message.""" - # Create a ChatMessage + # Create a Message message = Content.from_text(text="I'm doing well, thank you!") # Create a ChatResponseUpdate with the message @@ -726,7 +720,7 @@ def test_chat_response_update(): def test_chat_response_updates_to_chat_response_one(): """Test converting ChatResponseUpdate to ChatResponse.""" - # Create a ChatMessage + # Create a Message message1 = Content.from_text("I'm doing well, ") message2 = Content.from_text("thank you!") @@ -742,14 +736,14 @@ def test_chat_response_updates_to_chat_response_one(): # Check the type and content assert len(chat_response.messages) == 1 assert chat_response.text == "I'm doing well, thank you!" - assert isinstance(chat_response.messages[0], ChatMessage) + assert isinstance(chat_response.messages[0], Message) assert len(chat_response.messages[0].contents) == 1 assert chat_response.messages[0].message_id == "1" def test_chat_response_updates_to_chat_response_two(): """Test converting ChatResponseUpdate to ChatResponse.""" - # Create a ChatMessage + # Create a Message message1 = Content.from_text("I'm doing well, ") message2 = Content.from_text("thank you!") @@ -765,15 +759,15 @@ def test_chat_response_updates_to_chat_response_two(): # Check the type and content assert len(chat_response.messages) == 2 assert chat_response.text == "I'm doing well, \nthank you!" - assert isinstance(chat_response.messages[0], ChatMessage) + assert isinstance(chat_response.messages[0], Message) assert chat_response.messages[0].message_id == "1" - assert isinstance(chat_response.messages[1], ChatMessage) + assert isinstance(chat_response.messages[1], Message) assert chat_response.messages[1].message_id == "2" def test_chat_response_updates_to_chat_response_multiple(): """Test converting ChatResponseUpdate to ChatResponse.""" - # Create a ChatMessage + # Create a Message message1 = Content.from_text("I'm doing well, ") message2 = Content.from_text("thank you!") @@ -790,14 +784,14 @@ def test_chat_response_updates_to_chat_response_multiple(): # Check the type and content assert len(chat_response.messages) == 1 assert chat_response.text == "I'm doing well, thank you!" - assert isinstance(chat_response.messages[0], ChatMessage) + assert isinstance(chat_response.messages[0], Message) assert len(chat_response.messages[0].contents) == 3 assert chat_response.messages[0].message_id == "1" def test_chat_response_updates_to_chat_response_multiple_multiple(): """Test converting ChatResponseUpdate to ChatResponse.""" - # Create a ChatMessage + # Create a Message message1 = Content.from_text("I'm doing well, ", raw_representation="I'm doing well, ") message2 = Content.from_text("thank you!") @@ -815,7 +809,7 @@ def test_chat_response_updates_to_chat_response_multiple_multiple(): # Check the type and content assert len(chat_response.messages) == 1 - assert isinstance(chat_response.messages[0], ChatMessage) + assert isinstance(chat_response.messages[0], Message) assert chat_response.messages[0].message_id == "1" assert chat_response.messages[0].contents[0].raw_representation is not None @@ -1012,8 +1006,8 @@ def test_chat_options_and_tool_choice_required_specific_function() -> None: @fixture -def chat_message() -> ChatMessage: - return ChatMessage(role="user", text="Hello") +def chat_message() -> Message: + return Message(role="user", text="Hello") @fixture @@ -1022,7 +1016,7 @@ def text_content() -> Content: @fixture -def agent_response(chat_message: ChatMessage) -> AgentResponse: +def agent_response(chat_message: Message) -> AgentResponse: return AgentResponse(messages=chat_message) @@ -1034,12 +1028,12 @@ def agent_response_update(text_content: Content) -> AgentResponseUpdate: # region AgentResponse -def test_agent_run_response_init_single_message(chat_message: ChatMessage) -> None: +def test_agent_run_response_init_single_message(chat_message: Message) -> None: response = AgentResponse(messages=chat_message) assert response.messages == [chat_message] -def test_agent_run_response_init_list_messages(chat_message: ChatMessage) -> None: +def test_agent_run_response_init_list_messages(chat_message: Message) -> None: response = AgentResponse(messages=[chat_message, chat_message]) assert len(response.messages) == 2 assert response.messages[0] == chat_message @@ -1050,7 +1044,7 @@ def test_agent_run_response_init_none_messages() -> None: assert response.messages == [] -def test_agent_run_response_text_property(chat_message: ChatMessage) -> None: +def test_agent_run_response_text_property(chat_message: Message) -> None: response = AgentResponse(messages=[chat_message, chat_message]) assert response.text == "HelloHello" @@ -1067,7 +1061,7 @@ def test_agent_run_response_from_updates(agent_response_update: AgentResponseUpd assert response.text == "Test contentTest content" -def test_agent_run_response_str_method(chat_message: ChatMessage) -> None: +def test_agent_run_response_str_method(chat_message: Message) -> None: response = AgentResponse(messages=chat_message) assert str(response) == "Hello" @@ -1130,7 +1124,7 @@ def test_agent_run_response_created_at() -> None: # Test with a properly formatted UTC timestamp utc_timestamp = "2024-12-01T00:31:30.000000Z" response = AgentResponse( - messages=[ChatMessage(role="assistant", text="Hello")], + messages=[Message(role="assistant", text="Hello")], created_at=utc_timestamp, ) assert response.created_at == utc_timestamp @@ -1140,7 +1134,7 @@ def test_agent_run_response_created_at() -> None: now_utc = datetime.now(tz=timezone.utc) formatted_utc = now_utc.strftime("%Y-%m-%dT%H:%M:%S.%fZ") response_with_now = AgentResponse( - messages=[ChatMessage(role="assistant", text="Hello")], + messages=[Message(role="assistant", text="Hello")], created_at=formatted_utc, ) assert response_with_now.created_at == formatted_utc @@ -1294,7 +1288,7 @@ def test_chat_tool_mode_eq_with_string(): @fixture def agent_run_response_async() -> AgentResponse: - return AgentResponse(messages=[ChatMessage(role="user", text="Hello")]) + return AgentResponse(messages=[Message(role="user", text="Hello")]) async def test_agent_run_response_from_async_generator(): @@ -1444,7 +1438,7 @@ def test_usage_details_iadd_edge_cases(): def test_chat_message_from_dict_with_mixed_content(): - """Test ChatMessage from_dict with mixed content types for better coverage.""" + """Test Message from_dict with mixed content types for better coverage.""" message_data = { "role": "assistant", @@ -1455,7 +1449,7 @@ def test_chat_message_from_dict_with_mixed_content(): ], } - message = ChatMessage.from_dict(message_data) + message = Message.from_dict(message_data) assert len(message.contents) == 3 # Unknown type is ignored assert message.contents[0].type == "text" assert message.contents[1].type == "function_call" @@ -1513,7 +1507,7 @@ def test_comprehensive_serialization_methods(): def test_chat_message_complex_content_serialization(): - """Test ChatMessage serialization with various content types.""" + """Test Message serialization with various content types.""" # Create a message with multiple content types contents = [ @@ -1522,7 +1516,7 @@ def test_chat_message_complex_content_serialization(): Content.from_function_result(call_id="call1", result="success"), ] - message = ChatMessage(role="assistant", contents=contents) + message = Message(role="assistant", contents=contents) # Test to_dict message_dict = message.to_dict() @@ -1532,7 +1526,7 @@ def test_chat_message_complex_content_serialization(): assert message_dict["contents"][2]["type"] == "function_result" # Test from_dict round-trip - reconstructed = ChatMessage.from_dict(message_dict) + reconstructed = Message.from_dict(message_dict) assert len(reconstructed.contents) == 3 assert reconstructed.contents[0].type == "text" assert reconstructed.contents[1].type == "function_call" @@ -1610,7 +1604,7 @@ def test_chat_response_complex_serialization(): response = ChatResponse.from_dict(response_data) assert len(response.messages) == 2 - assert isinstance(response.messages[0], ChatMessage) + assert isinstance(response.messages[0], Message) assert isinstance(response.finish_reason, str) # FinishReason is now a NewType of str assert isinstance(response.usage_details, dict) assert response.model_id == "gpt-4" # Should be stored as model_id @@ -1687,7 +1681,7 @@ def test_agent_run_response_complex_serialization(): response = AgentResponse.from_dict(response_data) assert len(response.messages) == 2 - assert isinstance(response.messages[0], ChatMessage) + assert isinstance(response.messages[0], Message) assert isinstance(response.usage_details, dict) # Test to_dict @@ -1869,7 +1863,7 @@ def test_agent_run_response_update_all_content_types(): id="function_approval_response", ), pytest.param( - ChatMessage, + Message, { "role": "\1", "contents": [ @@ -1887,12 +1881,12 @@ def test_agent_run_response_update_all_content_types(): "type": "chat_response", "messages": [ { - "type": "chat_message", + "type": "message", "role": "\1", "contents": [{"type": "text", "text": "Hello"}], }, { - "type": "chat_message", + "type": "message", "role": "\1", "contents": [{"type": "text", "text": "Hi there"}], }, @@ -2761,7 +2755,7 @@ class TestResponseStreamResultHooks: """Result hook can transform the final result.""" def wrap_text(response: ChatResponse) -> ChatResponse: - return ChatResponse(messages=ChatMessage("assistant", [f"[{response.text}]"])) + return ChatResponse(messages=Message("assistant", [f"[{response.text}]"])) stream = ResponseStream( _generate_updates(2), @@ -2777,10 +2771,10 @@ class TestResponseStreamResultHooks: """Multiple result hooks are called in order.""" def add_prefix(response: ChatResponse) -> ChatResponse: - return ChatResponse(messages=ChatMessage("assistant", [f"prefix_{response.text}"])) + return ChatResponse(messages=Message("assistant", [f"prefix_{response.text}"])) def add_suffix(response: ChatResponse) -> ChatResponse: - return ChatResponse(messages=ChatMessage("assistant", [f"{response.text}_suffix"])) + return ChatResponse(messages=Message("assistant", [f"{response.text}_suffix"])) stream = ResponseStream( _generate_updates(1), @@ -2828,7 +2822,7 @@ class TestResponseStreamResultHooks: """Async result hooks are awaited.""" async def async_hook(response: ChatResponse) -> ChatResponse: - return ChatResponse(messages=ChatMessage("assistant", [f"async_{response.text}"])) + return ChatResponse(messages=Message("assistant", [f"async_{response.text}"])) stream = ResponseStream( _generate_updates(2), @@ -2850,7 +2844,7 @@ class TestResponseStreamFinalizer: def capturing_finalizer(updates: list[ChatResponseUpdate]) -> ChatResponse: received_updates.extend(updates) - return ChatResponse(messages=ChatMessage("assistant", ["done"])) + return ChatResponse(messages=Message("assistant", ["done"])) stream = ResponseStream(_generate_updates(3), finalizer=capturing_finalizer) @@ -2875,7 +2869,7 @@ class TestResponseStreamFinalizer: async def async_finalizer(updates: list[ChatResponseUpdate]) -> ChatResponse: text = "".join(u.text or "" for u in updates) - return ChatResponse(messages=ChatMessage("assistant", [f"async_{text}"])) + return ChatResponse(messages=Message("assistant", [f"async_{text}"])) stream = ResponseStream(_generate_updates(2), finalizer=async_finalizer) @@ -2889,7 +2883,7 @@ class TestResponseStreamFinalizer: def counting_finalizer(updates: list[ChatResponseUpdate]) -> ChatResponse: call_count["value"] += 1 - return ChatResponse(messages=ChatMessage("assistant", ["done"])) + return ChatResponse(messages=Message("assistant", ["done"])) stream = ResponseStream(_generate_updates(2), finalizer=counting_finalizer) @@ -2949,7 +2943,7 @@ class TestResponseStreamMapAndWithFinalizer: def inner_result_hook(response: ChatResponse) -> ChatResponse: inner_result_hook_called["value"] = True - return ChatResponse(messages=ChatMessage("assistant", [f"hooked_{response.text}"])) + return ChatResponse(messages=Message("assistant", [f"hooked_{response.text}"])) inner = ResponseStream( _generate_updates(2), @@ -2969,7 +2963,7 @@ class TestResponseStreamMapAndWithFinalizer: def inner_finalizer(updates: Sequence[ChatResponseUpdate]) -> ChatResponse: inner_finalizer_called["value"] = True - return ChatResponse(messages=ChatMessage("assistant", ["inner_result"])) + return ChatResponse(messages=Message("assistant", ["inner_result"])) inner = ResponseStream( _generate_updates(2), @@ -2989,7 +2983,7 @@ class TestResponseStreamMapAndWithFinalizer: inner = ResponseStream(_generate_updates(2), finalizer=_combine_updates) def outer_hook(response: ChatResponse) -> ChatResponse: - return ChatResponse(messages=ChatMessage("assistant", [f"outer_{response.text}"])) + return ChatResponse(messages=Message("assistant", [f"outer_{response.text}"])) outer = inner.with_finalizer(_combine_updates).with_result_hook(outer_hook) @@ -3114,7 +3108,7 @@ class TestResponseStreamExecutionOrder: def finalizer(updates: list[ChatResponseUpdate]) -> ChatResponse: order.append("finalizer") - return ChatResponse(messages=ChatMessage("assistant", ["done"])) + return ChatResponse(messages=Message("assistant", ["done"])) def result_hook(response: ChatResponse) -> ChatResponse: order.append("result") @@ -3149,7 +3143,7 @@ class TestResponseStreamExecutionOrder: def finalizer(updates: list[ChatResponseUpdate]) -> ChatResponse: order.append("finalizer") - return ChatResponse(messages=ChatMessage("assistant", ["done"])) + return ChatResponse(messages=Message("assistant", ["done"])) stream = ResponseStream( _generate_updates(2), @@ -3269,7 +3263,7 @@ class TestResponseStreamEdgeCases: def finalizer(updates: list[ChatResponseUpdate]) -> ChatResponse: events.append("finalizer") - return ChatResponse(messages=ChatMessage("assistant", ["done"])) + return ChatResponse(messages=Message("assistant", ["done"])) def result(r: ChatResponse) -> ChatResponse: events.append("result") diff --git a/python/packages/core/tests/openai/test_assistant_provider.py b/python/packages/core/tests/openai/test_assistant_provider.py index 90b077c941..8a2b561d77 100644 --- a/python/packages/core/tests/openai/test_assistant_provider.py +++ b/python/packages/core/tests/openai/test_assistant_provider.py @@ -8,9 +8,9 @@ import pytest from openai.types.beta.assistant import Assistant from pydantic import BaseModel, Field -from agent_framework import ChatAgent, HostedCodeInterpreterTool, HostedFileSearchTool, normalize_tools, tool +from agent_framework import Agent, normalize_tools, tool from agent_framework.exceptions import ServiceInitializationError -from agent_framework.openai import OpenAIAssistantProvider +from agent_framework.openai import OpenAIAssistantProvider, OpenAIAssistantsClient from agent_framework.openai._shared import from_assistant_tools, to_assistant_tools # region Test Helpers @@ -202,7 +202,7 @@ class TestOpenAIAssistantProviderCreateAgent: instructions="You are helpful.", ) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.name == "CreatedAssistant" mock_async_openai.beta.assistants.create.assert_called_once() @@ -235,7 +235,7 @@ class TestOpenAIAssistantProviderCreateAgent: tools=[get_weather], ) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) # Verify tools were passed to create call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs @@ -269,7 +269,7 @@ class TestOpenAIAssistantProviderCreateAgent: await provider.create_agent( name="CodeAgent", model="gpt-4", - tools=[HostedCodeInterpreterTool()], + tools=[OpenAIAssistantsClient.get_code_interpreter_tool()], ) call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs @@ -282,7 +282,7 @@ class TestOpenAIAssistantProviderCreateAgent: await provider.create_agent( name="SearchAgent", model="gpt-4", - tools=[HostedFileSearchTool()], + tools=[OpenAIAssistantsClient.get_file_search_tool()], ) call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs @@ -295,7 +295,7 @@ class TestOpenAIAssistantProviderCreateAgent: await provider.create_agent( name="SearchAgent", model="gpt-4", - tools=[HostedFileSearchTool(max_results=10)], + tools=[OpenAIAssistantsClient.get_file_search_tool(max_num_results=10)], ) call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs @@ -309,7 +309,11 @@ class TestOpenAIAssistantProviderCreateAgent: await provider.create_agent( name="MultiToolAgent", model="gpt-4", - tools=[get_weather, HostedCodeInterpreterTool(), HostedFileSearchTool()], + tools=[ + get_weather, + OpenAIAssistantsClient.get_code_interpreter_tool(), + OpenAIAssistantsClient.get_file_search_tool(), + ], ) call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs @@ -343,7 +347,7 @@ class TestOpenAIAssistantProviderCreateAgent: assert call_kwargs["response_format"]["json_schema"]["name"] == "WeatherResponse" async def test_create_agent_returns_chat_agent(self, mock_async_openai: MagicMock) -> None: - """Test that create_agent returns a ChatAgent instance.""" + """Test that create_agent returns a Agent instance.""" provider = OpenAIAssistantProvider(mock_async_openai) agent = await provider.create_agent( @@ -351,7 +355,7 @@ class TestOpenAIAssistantProviderCreateAgent: model="gpt-4", ) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) # endregion @@ -369,7 +373,7 @@ class TestOpenAIAssistantProviderGetAgent: agent = await provider.get_agent(assistant_id="asst_123") - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) mock_async_openai.beta.assistants.retrieve.assert_called_once_with("asst_123") async def test_get_agent_with_instructions_override(self, mock_async_openai: MagicMock) -> None: @@ -382,7 +386,7 @@ class TestOpenAIAssistantProviderGetAgent: ) # Agent should be created successfully with the custom instructions - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) assert agent.id == "asst_retrieved123" async def test_get_agent_with_function_tools(self, mock_async_openai: MagicMock) -> None: @@ -398,7 +402,7 @@ class TestOpenAIAssistantProviderGetAgent: tools=[get_weather], ) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) async def test_get_agent_validates_missing_function_tools(self, mock_async_openai: MagicMock) -> None: """Test that missing function tools raise ValueError.""" @@ -439,7 +443,7 @@ class TestOpenAIAssistantProviderGetAgent: agent = await provider.get_agent(assistant_id="asst_123") # Hosted tools should be merged automatically - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) # endregion @@ -458,7 +462,7 @@ class TestOpenAIAssistantProviderAsAgent: agent = provider.as_agent(assistant) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) # Verify no HTTP calls were made mock_async_openai.beta.assistants.create.assert_not_called() mock_async_openai.beta.assistants.retrieve.assert_not_called() @@ -477,7 +481,7 @@ class TestOpenAIAssistantProviderAsAgent: assert agent.id == "asst_wrap123" assert agent.name == "WrappedAssistant" # Instructions are passed to ChatOptions, not exposed as attribute - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) def test_as_agent_with_instructions_override(self, mock_async_openai: MagicMock) -> None: """Test as_agent with instruction override.""" @@ -487,7 +491,7 @@ class TestOpenAIAssistantProviderAsAgent: agent = provider.as_agent(assistant, instructions="Override") # Agent should be created successfully with override instructions - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) def test_as_agent_validates_function_tools(self, mock_async_openai: MagicMock) -> None: """Test that missing function tools raise ValueError.""" @@ -506,7 +510,7 @@ class TestOpenAIAssistantProviderAsAgent: agent = provider.as_agent(assistant, tools=[get_weather]) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) def test_as_agent_merges_hosted_tools(self, mock_async_openai: MagicMock) -> None: """Test that hosted tools are merged automatically.""" @@ -515,7 +519,7 @@ class TestOpenAIAssistantProviderAsAgent: agent = provider.as_agent(assistant) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) def test_as_agent_hosted_tools_not_required(self, mock_async_openai: MagicMock) -> None: """Test that hosted tools don't require user implementations.""" @@ -525,7 +529,7 @@ class TestOpenAIAssistantProviderAsAgent: # Should not raise - hosted tools don't need implementations agent = provider.as_agent(assistant) - assert isinstance(agent, ChatAgent) + assert isinstance(agent, Agent) # endregion @@ -564,22 +568,22 @@ class TestToolConversion: assert api_tools[0]["function"]["name"] == "get_weather" def test_to_assistant_tools_code_interpreter(self) -> None: - """Test HostedCodeInterpreterTool conversion.""" - api_tools = to_assistant_tools([HostedCodeInterpreterTool()]) + """Test code_interpreter tool dict conversion.""" + api_tools = to_assistant_tools([OpenAIAssistantsClient.get_code_interpreter_tool()]) assert len(api_tools) == 1 assert api_tools[0] == {"type": "code_interpreter"} def test_to_assistant_tools_file_search(self) -> None: - """Test HostedFileSearchTool conversion.""" - api_tools = to_assistant_tools([HostedFileSearchTool()]) + """Test file_search tool dict conversion.""" + api_tools = to_assistant_tools([OpenAIAssistantsClient.get_file_search_tool()]) assert len(api_tools) == 1 assert api_tools[0]["type"] == "file_search" def test_to_assistant_tools_file_search_with_max_results(self) -> None: - """Test HostedFileSearchTool with max_results conversion.""" - api_tools = to_assistant_tools([HostedFileSearchTool(max_results=5)]) + """Test file_search tool with max_results conversion.""" + api_tools = to_assistant_tools([OpenAIAssistantsClient.get_file_search_tool(max_num_results=5)]) assert api_tools[0]["file_search"]["max_num_results"] == 5 @@ -605,7 +609,7 @@ class TestToolConversion: tools = from_assistant_tools(assistant_tools) assert len(tools) == 1 - assert isinstance(tools[0], HostedCodeInterpreterTool) + assert tools[0] == {"type": "code_interpreter"} def test_from_assistant_tools_file_search(self) -> None: """Test converting file_search tool from OpenAI format.""" @@ -614,7 +618,7 @@ class TestToolConversion: tools = from_assistant_tools(assistant_tools) assert len(tools) == 1 - assert isinstance(tools[0], HostedFileSearchTool) + assert tools[0] == {"type": "file_search"} def test_from_assistant_tools_function_skipped(self) -> None: """Test that function tools are skipped (no implementations).""" @@ -707,7 +711,7 @@ class TestToolMerging: merged = provider._merge_tools(assistant_tools, None) # type: ignore[reportPrivateUsage] assert len(merged) == 1 - assert isinstance(merged[0], HostedCodeInterpreterTool) + assert merged[0] == {"type": "code_interpreter"} def test_merge_file_search(self, mock_async_openai: MagicMock) -> None: """Test merging file search tool.""" @@ -717,7 +721,7 @@ class TestToolMerging: merged = provider._merge_tools(assistant_tools, None) # type: ignore[reportPrivateUsage] assert len(merged) == 1 - assert isinstance(merged[0], HostedFileSearchTool) + assert merged[0] == {"type": "file_search"} def test_merge_with_user_tools(self, mock_async_openai: MagicMock) -> None: """Test merging hosted and user tools.""" @@ -727,7 +731,7 @@ class TestToolMerging: merged = provider._merge_tools(assistant_tools, [get_weather]) # type: ignore[reportPrivateUsage] assert len(merged) == 2 - assert isinstance(merged[0], HostedCodeInterpreterTool) + assert merged[0] == {"type": "code_interpreter"} def test_merge_multiple_hosted_tools(self, mock_async_openai: MagicMock) -> None: """Test merging multiple hosted tools.""" diff --git a/python/packages/core/tests/openai/test_openai_assistants_client.py b/python/packages/core/tests/openai/test_openai_assistants_client.py index 2cefc5ad54..2bf56a94aa 100644 --- a/python/packages/core/tests/openai/test_openai_assistants_client.py +++ b/python/packages/core/tests/openai/test_openai_assistants_client.py @@ -11,17 +11,15 @@ from openai.types.beta.threads.runs import RunStep from pydantic import Field from agent_framework import ( + Agent, AgentResponse, AgentResponseUpdate, AgentThread, - ChatAgent, - ChatClientProtocol, - ChatMessage, ChatResponse, ChatResponseUpdate, Content, - HostedCodeInterpreterTool, - HostedFileSearchTool, + Message, + SupportsChatGetResponse, tool, ) from agent_framework.exceptions import ServiceInitializationError @@ -113,16 +111,16 @@ def mock_async_openai() -> MagicMock: def test_init_with_client(mock_async_openai: MagicMock) -> None: """Test OpenAIAssistantsClient initialization with existing client.""" - chat_client = create_test_openai_assistants_client( + client = create_test_openai_assistants_client( mock_async_openai, model_id="gpt-4", assistant_id="existing-assistant-id", thread_id="test-thread-id" ) - assert chat_client.client is mock_async_openai - assert chat_client.model_id == "gpt-4" - assert chat_client.assistant_id == "existing-assistant-id" - assert chat_client.thread_id == "test-thread-id" - assert not chat_client._should_delete_assistant # type: ignore - assert isinstance(chat_client, ChatClientProtocol) + assert client.client is mock_async_openai + assert client.model_id == "gpt-4" + assert client.assistant_id == "existing-assistant-id" + assert client.thread_id == "test-thread-id" + assert not client._should_delete_assistant # type: ignore + assert isinstance(client, SupportsChatGetResponse) def test_init_auto_create_client( @@ -130,7 +128,7 @@ def test_init_auto_create_client( mock_async_openai: MagicMock, ) -> None: """Test OpenAIAssistantsClient initialization with auto-created client.""" - chat_client = OpenAIAssistantsClient( + client = OpenAIAssistantsClient( model_id=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"], assistant_name="TestAssistant", api_key=openai_unit_test_env["OPENAI_API_KEY"], @@ -138,11 +136,11 @@ def test_init_auto_create_client( async_client=mock_async_openai, ) - assert chat_client.client is mock_async_openai - assert chat_client.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] - assert chat_client.assistant_id is None - assert chat_client.assistant_name == "TestAssistant" - assert not chat_client._should_delete_assistant # type: ignore + assert client.client is mock_async_openai + assert client.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert client.assistant_id is None + assert client.assistant_name == "TestAssistant" + assert not client._should_delete_assistant # type: ignore def test_init_validation_fail() -> None: @@ -172,31 +170,31 @@ def test_init_with_default_headers(openai_unit_test_env: dict[str, str]) -> None """Test OpenAIAssistantsClient initialization with default headers.""" default_headers = {"X-Unit-Test": "test-guid"} - chat_client = OpenAIAssistantsClient( + client = OpenAIAssistantsClient( model_id="gpt-4", api_key=openai_unit_test_env["OPENAI_API_KEY"], default_headers=default_headers, ) - assert chat_client.model_id == "gpt-4" - assert isinstance(chat_client, ChatClientProtocol) + assert client.model_id == "gpt-4" + assert isinstance(client, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers for key, value in default_headers.items(): - assert key in chat_client.client.default_headers - assert chat_client.client.default_headers[key] == value + assert key in client.client.default_headers + assert client.client.default_headers[key] == value async def test_get_assistant_id_or_create_existing_assistant( mock_async_openai: MagicMock, ) -> None: """Test _get_assistant_id_or_create when assistant_id is already provided.""" - chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_id="existing-assistant-id") + client = create_test_openai_assistants_client(mock_async_openai, assistant_id="existing-assistant-id") - assistant_id = await chat_client._get_assistant_id_or_create() # type: ignore + assistant_id = await client._get_assistant_id_or_create() # type: ignore assert assistant_id == "existing-assistant-id" - assert not chat_client._should_delete_assistant # type: ignore + assert not client._should_delete_assistant # type: ignore mock_async_openai.beta.assistants.create.assert_not_called() @@ -204,14 +202,12 @@ async def test_get_assistant_id_or_create_create_new( mock_async_openai: MagicMock, ) -> None: """Test _get_assistant_id_or_create when creating a new assistant.""" - chat_client = create_test_openai_assistants_client( - mock_async_openai, model_id="gpt-4", assistant_name="TestAssistant" - ) + client = create_test_openai_assistants_client(mock_async_openai, model_id="gpt-4", assistant_name="TestAssistant") - assistant_id = await chat_client._get_assistant_id_or_create() # type: ignore + assistant_id = await client._get_assistant_id_or_create() # type: ignore assert assistant_id == "test-assistant-id" - assert chat_client._should_delete_assistant # type: ignore + assert client._should_delete_assistant # type: ignore mock_async_openai.beta.assistants.create.assert_called_once() @@ -219,38 +215,38 @@ async def test_aclose_should_not_delete( mock_async_openai: MagicMock, ) -> None: """Test close when assistant should not be deleted.""" - chat_client = create_test_openai_assistants_client( + client = create_test_openai_assistants_client( mock_async_openai, assistant_id="assistant-to-keep", should_delete_assistant=False ) - await chat_client.close() # type: ignore + await client.close() # type: ignore # Verify assistant deletion was not called mock_async_openai.beta.assistants.delete.assert_not_called() - assert not chat_client._should_delete_assistant # type: ignore + assert not client._should_delete_assistant # type: ignore async def test_aclose_should_delete(mock_async_openai: MagicMock) -> None: """Test close method calls cleanup.""" - chat_client = create_test_openai_assistants_client( + client = create_test_openai_assistants_client( mock_async_openai, assistant_id="assistant-to-delete", should_delete_assistant=True ) - await chat_client.close() + await client.close() # Verify assistant deletion was called mock_async_openai.beta.assistants.delete.assert_called_once_with("assistant-to-delete") - assert not chat_client._should_delete_assistant # type: ignore + assert not client._should_delete_assistant # type: ignore async def test_async_context_manager(mock_async_openai: MagicMock) -> None: """Test async context manager functionality.""" - chat_client = create_test_openai_assistants_client( + client = create_test_openai_assistants_client( mock_async_openai, assistant_id="assistant-to-delete", should_delete_assistant=True ) # Test context manager - async with chat_client: + async with client: pass # Just test that we can enter and exit # Verify cleanup was called on exit @@ -262,7 +258,7 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None: default_headers = {"X-Unit-Test": "test-guid"} # Test basic initialization and to_dict - chat_client = OpenAIAssistantsClient( + client = OpenAIAssistantsClient( model_id="gpt-4", assistant_id="test-assistant-id", assistant_name="TestAssistant", @@ -272,7 +268,7 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None: default_headers=default_headers, ) - dumped_settings = chat_client.to_dict() + dumped_settings = client.to_dict() assert dumped_settings["model_id"] == "gpt-4" assert dumped_settings["assistant_id"] == "test-assistant-id" @@ -290,9 +286,9 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None: async def test_get_active_thread_run_none_thread_id(mock_async_openai: MagicMock) -> None: """Test _get_active_thread_run with None thread_id returns None.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) - result = await chat_client._get_active_thread_run(None) # type: ignore + result = await client._get_active_thread_run(None) # type: ignore assert result is None # Should not call the API when thread_id is None @@ -302,7 +298,7 @@ async def test_get_active_thread_run_none_thread_id(mock_async_openai: MagicMock async def test_get_active_thread_run_with_active_run(mock_async_openai: MagicMock) -> None: """Test _get_active_thread_run finds an active run.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Mock an active run (status not in completed states) mock_run = MagicMock() @@ -314,7 +310,7 @@ async def test_get_active_thread_run_with_active_run(mock_async_openai: MagicMoc mock_async_openai.beta.threads.runs.list.return_value.__aiter__ = mock_runs_list - result = await chat_client._get_active_thread_run("thread-123") # type: ignore + result = await client._get_active_thread_run("thread-123") # type: ignore assert result == mock_run mock_async_openai.beta.threads.runs.list.assert_called_once_with(thread_id="thread-123", limit=1, order="desc") @@ -322,7 +318,7 @@ async def test_get_active_thread_run_with_active_run(mock_async_openai: MagicMoc async def test_prepare_thread_create_new(mock_async_openai: MagicMock) -> None: """Test _prepare_thread creates new thread when thread_id is None.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Mock thread creation mock_thread = MagicMock() @@ -336,7 +332,7 @@ async def test_prepare_thread_create_new(mock_async_openai: MagicMock) -> None: "metadata": {"test": "true"}, } - result = await chat_client._prepare_thread(None, None, run_options) # type: ignore + result = await client._prepare_thread(None, None, run_options) # type: ignore assert result == "new-thread-123" assert run_options["additional_messages"] == [] # Should be cleared @@ -349,7 +345,7 @@ async def test_prepare_thread_create_new(mock_async_openai: MagicMock) -> None: async def test_prepare_thread_cancel_existing_run(mock_async_openai: MagicMock) -> None: """Test _prepare_thread cancels existing run when provided.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Mock an existing thread run mock_thread_run = MagicMock() @@ -357,7 +353,7 @@ async def test_prepare_thread_cancel_existing_run(mock_async_openai: MagicMock) run_options: dict[str, Any] = {"additional_messages": []} - result = await chat_client._prepare_thread("thread-123", mock_thread_run, run_options) # type: ignore + result = await client._prepare_thread("thread-123", mock_thread_run, run_options) # type: ignore assert result == "thread-123" mock_async_openai.beta.threads.runs.cancel.assert_called_once_with(run_id="run-456", thread_id="thread-123") @@ -365,11 +361,11 @@ async def test_prepare_thread_cancel_existing_run(mock_async_openai: MagicMock) async def test_prepare_thread_existing_no_run(mock_async_openai: MagicMock) -> None: """Test _prepare_thread with existing thread_id but no active run.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) run_options: dict[str, list[dict[str, str]]] = {"additional_messages": []} - result = await chat_client._prepare_thread("thread-123", None, run_options) # type: ignore + result = await client._prepare_thread("thread-123", None, run_options) # type: ignore assert result == "thread-123" # Should not call cancel since no thread_run provided @@ -378,7 +374,7 @@ async def test_prepare_thread_existing_no_run(mock_async_openai: MagicMock) -> N async def test_process_stream_events_thread_run_created(mock_async_openai: MagicMock) -> None: """Test _process_stream_events with thread.run.created event.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create a mock stream response for thread.run.created mock_response = MagicMock() @@ -396,7 +392,7 @@ async def test_process_stream_events_thread_run_created(mock_async_openai: Magic thread_id = "thread-123" updates: list[ChatResponseUpdate] = [] - async for update in chat_client._process_stream_events(mock_stream, thread_id): # type: ignore + async for update in client._process_stream_events(mock_stream, thread_id): # type: ignore updates.append(update) # Should yield one ChatResponseUpdate for thread.run.created @@ -411,7 +407,7 @@ async def test_process_stream_events_thread_run_created(mock_async_openai: Magic async def test_process_stream_events_message_delta_text(mock_async_openai: MagicMock) -> None: """Test _process_stream_events with thread.message.delta event containing text.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create a mock TextDeltaBlock with proper spec mock_delta_block = MagicMock(spec=TextDeltaBlock) @@ -440,7 +436,7 @@ async def test_process_stream_events_message_delta_text(mock_async_openai: Magic thread_id = "thread-456" updates: list[ChatResponseUpdate] = [] - async for update in chat_client._process_stream_events(mock_stream, thread_id): # type: ignore + async for update in client._process_stream_events(mock_stream, thread_id): # type: ignore updates.append(update) # Should yield one text update @@ -455,11 +451,11 @@ async def test_process_stream_events_message_delta_text(mock_async_openai: Magic async def test_process_stream_events_requires_action(mock_async_openai: MagicMock) -> None: """Test _process_stream_events with thread.run.requires_action event.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Mock the _parse_function_calls_from_assistants method to return test content test_function_content = Content.from_function_call(call_id="call-123", name="test_func", arguments={"arg": "value"}) - chat_client._parse_function_calls_from_assistants = MagicMock(return_value=[test_function_content]) # type: ignore + client._parse_function_calls_from_assistants = MagicMock(return_value=[test_function_content]) # type: ignore # Create a mock Run object mock_run = MagicMock(spec=Run) @@ -479,7 +475,7 @@ async def test_process_stream_events_requires_action(mock_async_openai: MagicMoc thread_id = "thread-789" updates: list[ChatResponseUpdate] = [] - async for update in chat_client._process_stream_events(mock_stream, thread_id): # type: ignore + async for update in client._process_stream_events(mock_stream, thread_id): # type: ignore updates.append(update) # Should yield one function call update @@ -493,13 +489,13 @@ async def test_process_stream_events_requires_action(mock_async_openai: MagicMoc assert update.raw_representation == mock_run # Verify _parse_function_calls_from_assistants was called correctly - chat_client._parse_function_calls_from_assistants.assert_called_once_with(mock_run, None) # type: ignore + client._parse_function_calls_from_assistants.assert_called_once_with(mock_run, None) # type: ignore async def test_process_stream_events_run_step_created(mock_async_openai: MagicMock) -> None: """Test _process_stream_events with thread.run.step.created event.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create a mock RunStep object mock_run_step = MagicMock(spec=RunStep) @@ -520,7 +516,7 @@ async def test_process_stream_events_run_step_created(mock_async_openai: MagicMo thread_id = "thread-789" updates: list[ChatResponseUpdate] = [] - async for update in chat_client._process_stream_events(mock_stream, thread_id): # type: ignore + async for update in client._process_stream_events(mock_stream, thread_id): # type: ignore updates.append(update) # The run step creation itself doesn't yield an update, @@ -533,7 +529,7 @@ async def test_process_stream_events_run_completed_with_usage( ) -> None: """Test _process_stream_events with thread.run.completed event containing usage.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create a mock Run object with usage information mock_usage = MagicMock() @@ -559,7 +555,7 @@ async def test_process_stream_events_run_completed_with_usage( thread_id = "thread-999" updates: list[ChatResponseUpdate] = [] - async for update in chat_client._process_stream_events(mock_stream, thread_id): # type: ignore + async for update in client._process_stream_events(mock_stream, thread_id): # type: ignore updates.append(update) # Should yield one usage update @@ -582,7 +578,7 @@ async def test_process_stream_events_run_completed_with_usage( def test_parse_function_calls_from_assistants_basic(mock_async_openai: MagicMock) -> None: """Test _parse_function_calls_from_assistants with a simple function call.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create a mock Run event that requires action mock_run = MagicMock() @@ -599,7 +595,7 @@ def test_parse_function_calls_from_assistants_basic(mock_async_openai: MagicMock # Call the method response_id = "response_456" - contents = chat_client._parse_function_calls_from_assistants(mock_run, response_id) # type: ignore + contents = client._parse_function_calls_from_assistants(mock_run, response_id) # type: ignore # Test that one function call content was created assert len(contents) == 1 @@ -685,7 +681,7 @@ def test_parse_run_step_with_mcp_tool_call(mock_async_openai: MagicMock) -> None def test_prepare_options_basic(mock_async_openai: MagicMock) -> None: """Test _prepare_options with basic chat options.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create basic chat options as a dict options = { @@ -695,10 +691,10 @@ def test_prepare_options_basic(mock_async_openai: MagicMock) -> None: "top_p": 0.9, } - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] # Call the method - run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore + run_options, tool_results = client._prepare_options(messages, options) # type: ignore # Check basic options were set assert run_options["max_completion_tokens"] == 100 @@ -711,7 +707,7 @@ def test_prepare_options_basic(mock_async_openai: MagicMock) -> None: def test_prepare_options_with_tool_tool(mock_async_openai: MagicMock) -> None: """Test _prepare_options with a FunctionTool.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create a simple function for testing and decorate it @tool(approval_mode="never_require") @@ -724,10 +720,10 @@ def test_prepare_options_with_tool_tool(mock_async_openai: MagicMock) -> None: "tool_choice": "auto", } - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] # Call the method - run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore + run_options, tool_results = client._prepare_options(messages, options) # type: ignore # Check tools were set correctly assert "tools" in run_options @@ -738,21 +734,21 @@ def test_prepare_options_with_tool_tool(mock_async_openai: MagicMock) -> None: def test_prepare_options_with_code_interpreter(mock_async_openai: MagicMock) -> None: - """Test _prepare_options with HostedCodeInterpreterTool.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + """Test _prepare_options with code interpreter tool.""" + client = create_test_openai_assistants_client(mock_async_openai) - # Create a real HostedCodeInterpreterTool - code_tool = HostedCodeInterpreterTool() + # Create a code interpreter tool dict + code_tool = OpenAIAssistantsClient.get_code_interpreter_tool() options = { "tools": [code_tool], "tool_choice": "auto", } - messages = [ChatMessage(role="user", text="Calculate something")] + messages = [Message(role="user", text="Calculate something")] # Call the method - run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore + run_options, tool_results = client._prepare_options(messages, options) # type: ignore # Check code interpreter tool was set correctly assert "tools" in run_options @@ -763,16 +759,16 @@ def test_prepare_options_with_code_interpreter(mock_async_openai: MagicMock) -> def test_prepare_options_tool_choice_none(mock_async_openai: MagicMock) -> None: """Test _prepare_options with tool_choice set to 'none' and no tools.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) options = { "tool_choice": "none", } - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] # Call the method - run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore + run_options, tool_results = client._prepare_options(messages, options) # type: ignore # Should set tool_choice to none - no tools because none were provided assert run_options["tool_choice"] == "none" @@ -785,7 +781,7 @@ def test_prepare_options_tool_choice_none_with_tools(mock_async_openai: MagicMoc When tool_choice='none', the model won't call tools, but tools should still be sent to the API so they're available for future turns in the conversation. """ - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create a function tool @tool(approval_mode="never_require") @@ -797,10 +793,10 @@ def test_prepare_options_tool_choice_none_with_tools(mock_async_openai: MagicMoc "tools": [test_func], } - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] # Call the method - run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore + run_options, tool_results = client._prepare_options(messages, options) # type: ignore # Should set tool_choice to none BUT still include tools assert run_options["tool_choice"] == "none" @@ -810,7 +806,7 @@ def test_prepare_options_tool_choice_none_with_tools(mock_async_openai: MagicMoc def test_prepare_options_required_function(mock_async_openai: MagicMock) -> None: """Test _prepare_options with required function tool choice.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create a required function tool choice as dict tool_choice = {"mode": "required", "required_function_name": "specific_function"} @@ -819,10 +815,10 @@ def test_prepare_options_required_function(mock_async_openai: MagicMock) -> None "tool_choice": tool_choice, } - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] # Call the method - run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore + run_options, tool_results = client._prepare_options(messages, options) # type: ignore # Check required function tool choice was set correctly expected_tool_choice = { @@ -833,34 +829,34 @@ def test_prepare_options_required_function(mock_async_openai: MagicMock) -> None def test_prepare_options_with_file_search_tool(mock_async_openai: MagicMock) -> None: - """Test _prepare_options with HostedFileSearchTool.""" + """Test _prepare_options with file_search tool.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) - # Create a HostedFileSearchTool with max_results - file_search_tool = HostedFileSearchTool(max_results=10) + # Create a file_search tool with max_results + file_search_tool = OpenAIAssistantsClient.get_file_search_tool(max_num_results=10) options = { "tools": [file_search_tool], "tool_choice": "auto", } - messages = [ChatMessage(role="user", text="Search for information")] + messages = [Message(role="user", text="Search for information")] # Call the method - run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore + run_options, tool_results = client._prepare_options(messages, options) # type: ignore # Check file search tool was set correctly assert "tools" in run_options assert len(run_options["tools"]) == 1 - expected_tool = {"type": "file_search", "max_num_results": 10} + expected_tool = {"type": "file_search", "file_search": {"max_num_results": 10}} assert run_options["tools"][0] == expected_tool assert run_options["tool_choice"] == "auto" def test_prepare_options_with_mapping_tool(mock_async_openai: MagicMock) -> None: """Test _prepare_options with MutableMapping tool.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create a tool as a MutableMapping (dict) mapping_tool = {"type": "custom_tool", "parameters": {"setting": "value"}} @@ -870,10 +866,10 @@ def test_prepare_options_with_mapping_tool(mock_async_openai: MagicMock) -> None "tool_choice": "auto", } - messages = [ChatMessage(role="user", text="Use custom tool")] + messages = [Message(role="user", text="Use custom tool")] # Call the method - run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore + run_options, tool_results = client._prepare_options(messages, options) # type: ignore # Check mapping tool was set correctly assert "tools" in run_options @@ -891,11 +887,11 @@ def test_prepare_options_with_pydantic_response_format(mock_async_openai: MagicM value: int model_config = ConfigDict(extra="forbid") - chat_client = create_test_openai_assistants_client(mock_async_openai) - messages = [ChatMessage(role="user", text="Test")] + client = create_test_openai_assistants_client(mock_async_openai) + messages = [Message(role="user", text="Test")] options = {"response_format": TestResponse} - run_options, _ = chat_client._prepare_options(messages, options) # type: ignore + run_options, _ = client._prepare_options(messages, options) # type: ignore assert "response_format" in run_options assert run_options["response_format"]["type"] == "json_schema" @@ -905,15 +901,15 @@ def test_prepare_options_with_pydantic_response_format(mock_async_openai: MagicM def test_prepare_options_with_system_message(mock_async_openai: MagicMock) -> None: """Test _prepare_options with system message converted to instructions.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) messages = [ - ChatMessage(role="system", text="You are a helpful assistant."), - ChatMessage(role="user", text="Hello"), + Message(role="system", text="You are a helpful assistant."), + Message(role="user", text="Hello"), ] # Call the method - run_options, tool_results = chat_client._prepare_options(messages, {}) # type: ignore + run_options, tool_results = client._prepare_options(messages, {}) # type: ignore # Check that additional_messages only contains the user message # System message should be converted to instructions (though this is handled internally) @@ -925,14 +921,14 @@ def test_prepare_options_with_system_message(mock_async_openai: MagicMock) -> No def test_prepare_options_with_image_content(mock_async_openai: MagicMock) -> None: """Test _prepare_options with image content.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create message with image content image_content = Content.from_uri(uri="https://example.com/image.jpg", media_type="image/jpeg") - messages = [ChatMessage(role="user", contents=[image_content])] + messages = [Message(role="user", contents=[image_content])] # Call the method - run_options, tool_results = chat_client._prepare_options(messages, {}) # type: ignore + run_options, tool_results = client._prepare_options(messages, {}) # type: ignore # Check that image content was processed assert "additional_messages" in run_options @@ -946,9 +942,9 @@ def test_prepare_options_with_image_content(mock_async_openai: MagicMock) -> Non def test_prepare_tool_outputs_for_assistants_empty(mock_async_openai: MagicMock) -> None: """Test _prepare_tool_outputs_for_assistants with empty list.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) - run_id, tool_outputs = chat_client._prepare_tool_outputs_for_assistants([]) # type: ignore + run_id, tool_outputs = client._prepare_tool_outputs_for_assistants([]) # type: ignore assert run_id is None assert tool_outputs is None @@ -956,12 +952,12 @@ def test_prepare_tool_outputs_for_assistants_empty(mock_async_openai: MagicMock) def test_prepare_tool_outputs_for_assistants_valid(mock_async_openai: MagicMock) -> None: """Test _prepare_tool_outputs_for_assistants with valid function results.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) call_id = json.dumps(["run-123", "call-456"]) function_result = Content.from_function_result(call_id=call_id, result="Function executed successfully") - run_id, tool_outputs = chat_client._prepare_tool_outputs_for_assistants([function_result]) # type: ignore + run_id, tool_outputs = client._prepare_tool_outputs_for_assistants([function_result]) # type: ignore assert run_id == "run-123" assert tool_outputs is not None @@ -974,7 +970,7 @@ def test_prepare_tool_outputs_for_assistants_mismatched_run_ids( mock_async_openai: MagicMock, ) -> None: """Test _prepare_tool_outputs_for_assistants with mismatched run IDs.""" - chat_client = create_test_openai_assistants_client(mock_async_openai) + client = create_test_openai_assistants_client(mock_async_openai) # Create function results with different run IDs call_id1 = json.dumps(["run-123", "call-456"]) @@ -982,7 +978,7 @@ def test_prepare_tool_outputs_for_assistants_mismatched_run_ids( function_result1 = Content.from_function_result(call_id=call_id1, result="Result 1") function_result2 = Content.from_function_result(call_id=call_id2, result="Result 2") - run_id, tool_outputs = chat_client._prepare_tool_outputs_for_assistants([function_result1, function_result2]) # type: ignore + run_id, tool_outputs = client._prepare_tool_outputs_for_assistants([function_result1, function_result2]) # type: ignore # Should only process the first one since run IDs don't match assert run_id == "run-123" @@ -994,36 +990,36 @@ def test_prepare_tool_outputs_for_assistants_mismatched_run_ids( def test_update_agent_name_and_description(mock_async_openai: MagicMock) -> None: """Test _update_agent_name_and_description method updates assistant_name when not already set.""" # Test updating agent name when assistant_name is None - chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name=None) + client = create_test_openai_assistants_client(mock_async_openai, assistant_name=None) # Call the private method to update agent name - chat_client._update_agent_name_and_description("New Assistant Name") # type: ignore + client._update_agent_name_and_description("New Assistant Name") # type: ignore - assert chat_client.assistant_name == "New Assistant Name" + assert client.assistant_name == "New Assistant Name" def test_update_agent_name_and_description_existing(mock_async_openai: MagicMock) -> None: """Test _update_agent_name_and_description method doesn't override existing assistant_name.""" # Test that existing assistant_name is not overridden - chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name="Existing Assistant") + client = create_test_openai_assistants_client(mock_async_openai, assistant_name="Existing Assistant") # Call the private method to update agent name - chat_client._update_agent_name_and_description("New Assistant Name") # type: ignore + client._update_agent_name_and_description("New Assistant Name") # type: ignore # Should keep the existing name - assert chat_client.assistant_name == "Existing Assistant" + assert client.assistant_name == "Existing Assistant" def test_update_agent_name_and_description_none(mock_async_openai: MagicMock) -> None: """Test _update_agent_name_and_description method with None agent_name parameter.""" # Test that None agent_name doesn't change anything - chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name=None) + client = create_test_openai_assistants_client(mock_async_openai, assistant_name=None) # Call the private method with None - chat_client._update_agent_name_and_description(None) # type: ignore + client._update_agent_name_and_description(None) # type: ignore # Should remain None - assert chat_client.assistant_name is None + assert client.assistant_name is None @tool(approval_mode="never_require") @@ -1039,17 +1035,17 @@ def get_weather( async def test_get_response() -> None: """Test OpenAI Assistants Client response.""" async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: - assert isinstance(openai_assistants_client, ChatClientProtocol) + assert isinstance(openai_assistants_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] + messages: list[Message] = [] messages.append( - ChatMessage( + Message( role="user", text="The weather in Seattle is currently sunny with a high of 25°C. " "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(Message(role="user", text="What's the weather like today?")) # Test that the client can be used to get a response response = await openai_assistants_client.get_response(messages=messages) @@ -1064,10 +1060,10 @@ async def test_get_response() -> None: async def test_get_response_tools() -> None: """Test OpenAI Assistants Client response with tools.""" async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: - assert isinstance(openai_assistants_client, ChatClientProtocol) + assert isinstance(openai_assistants_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages: list[Message] = [] + messages.append(Message(role="user", text="What's the weather like in Seattle?")) # Test that the client can be used to get a response response = await openai_assistants_client.get_response( @@ -1085,17 +1081,17 @@ async def test_get_response_tools() -> None: async def test_streaming() -> None: """Test OpenAI Assistants Client streaming response.""" async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: - assert isinstance(openai_assistants_client, ChatClientProtocol) + assert isinstance(openai_assistants_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] + messages: list[Message] = [] messages.append( - ChatMessage( + Message( role="user", text="The weather in Seattle is currently sunny with a high of 25°C. " "It's a beautiful day for outdoor activities.", ) ) - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages.append(Message(role="user", text="What's the weather like today?")) # Test that the client can be used to get a response response = openai_assistants_client.get_response(stream=True, messages=messages) @@ -1116,10 +1112,10 @@ async def test_streaming() -> None: async def test_streaming_tools() -> None: """Test OpenAI Assistants Client streaming response with tools.""" async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: - assert isinstance(openai_assistants_client, ChatClientProtocol) + assert isinstance(openai_assistants_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?")) + messages: list[Message] = [] + messages.append(Message(role="user", text="What's the weather like in Seattle?")) # Test that the client can be used to get a response response = openai_assistants_client.get_response( @@ -1148,7 +1144,7 @@ async def test_with_existing_assistant() -> None: # First create an assistant to use in the test async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as temp_client: # Get the assistant ID by triggering assistant creation - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] await temp_client.get_response(messages=messages) assistant_id = temp_client.assistant_id @@ -1156,10 +1152,10 @@ async def test_with_existing_assistant() -> None: async with OpenAIAssistantsClient( model_id=INTEGRATION_TEST_MODEL, assistant_id=assistant_id ) as openai_assistants_client: - assert isinstance(openai_assistants_client, ChatClientProtocol) + assert isinstance(openai_assistants_client, SupportsChatGetResponse) assert openai_assistants_client.assistant_id == assistant_id - messages = [ChatMessage(role="user", text="What can you do?")] + messages = [Message(role="user", text="What can you do?")] # Test that the client can be used to get a response response = await openai_assistants_client.get_response(messages=messages) @@ -1175,16 +1171,16 @@ async def test_with_existing_assistant() -> None: async def test_file_search() -> None: """Test OpenAI Assistants Client response.""" async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: - assert isinstance(openai_assistants_client, ChatClientProtocol) + assert isinstance(openai_assistants_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages: list[Message] = [] + messages.append(Message(role="user", text="What's the weather like today?")) file_id, vector_store = await create_vector_store(openai_assistants_client) response = await openai_assistants_client.get_response( messages=messages, options={ - "tools": [HostedFileSearchTool()], + "tools": [OpenAIAssistantsClient.get_file_search_tool()], "tool_resources": {"file_search": {"vector_store_ids": [vector_store.vector_store_id]}}, }, ) @@ -1201,17 +1197,17 @@ async def test_file_search() -> None: async def test_file_search_streaming() -> None: """Test OpenAI Assistants Client response.""" async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client: - assert isinstance(openai_assistants_client, ChatClientProtocol) + assert isinstance(openai_assistants_client, SupportsChatGetResponse) - messages: list[ChatMessage] = [] - messages.append(ChatMessage(role="user", text="What's the weather like today?")) + messages: list[Message] = [] + messages.append(Message(role="user", text="What's the weather like today?")) file_id, vector_store = await create_vector_store(openai_assistants_client) response = openai_assistants_client.get_response( stream=True, messages=messages, options={ - "tools": [HostedFileSearchTool()], + "tools": [OpenAIAssistantsClient.get_file_search_tool()], "tool_resources": {"file_search": {"vector_store_ids": [vector_store.vector_store_id]}}, }, ) @@ -1232,9 +1228,9 @@ async def test_file_search_streaming() -> None: @pytest.mark.flaky @skip_if_openai_integration_tests_disabled async def test_openai_assistants_agent_basic_run(): - """Test ChatAgent basic run functionality with OpenAIAssistantsClient.""" - async with ChatAgent( - chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + """Test Agent basic run functionality with OpenAIAssistantsClient.""" + async with Agent( + client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), ) as agent: # Run a simple query response = await agent.run("Hello! Please respond with 'Hello World' exactly.") @@ -1249,9 +1245,9 @@ async def test_openai_assistants_agent_basic_run(): @pytest.mark.flaky @skip_if_openai_integration_tests_disabled async def test_openai_assistants_agent_basic_run_streaming(): - """Test ChatAgent basic streaming functionality with OpenAIAssistantsClient.""" - async with ChatAgent( - chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + """Test Agent basic streaming functionality with OpenAIAssistantsClient.""" + async with Agent( + client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), ) as agent: # Run streaming query full_message: str = "" @@ -1269,9 +1265,9 @@ async def test_openai_assistants_agent_basic_run_streaming(): @pytest.mark.flaky @skip_if_openai_integration_tests_disabled async def test_openai_assistants_agent_thread_persistence(): - """Test ChatAgent thread persistence across runs with OpenAIAssistantsClient.""" - async with ChatAgent( - chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + """Test Agent thread persistence across runs with OpenAIAssistantsClient.""" + async with Agent( + client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), instructions="You are a helpful assistant with good memory.", ) as agent: # Create a new thread that will be reused @@ -1298,12 +1294,12 @@ async def test_openai_assistants_agent_thread_persistence(): @pytest.mark.flaky @skip_if_openai_integration_tests_disabled async def test_openai_assistants_agent_existing_thread_id(): - """Test ChatAgent with existing thread ID to continue conversations across agent instances.""" + """Test Agent with existing thread ID to continue conversations across agent instances.""" # First, create a conversation and capture the thread ID existing_thread_id = None - async with ChatAgent( - chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + async with Agent( + client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), instructions="You are a helpful weather agent.", tools=[get_weather], ) as agent: @@ -1322,8 +1318,8 @@ async def test_openai_assistants_agent_existing_thread_id(): # Now continue with the same thread ID in a new agent instance - async with ChatAgent( - chat_client=OpenAIAssistantsClient(thread_id=existing_thread_id), + async with Agent( + client=OpenAIAssistantsClient(thread_id=existing_thread_id), instructions="You are a helpful weather agent.", tools=[get_weather], ) as agent: @@ -1343,12 +1339,12 @@ async def test_openai_assistants_agent_existing_thread_id(): @pytest.mark.flaky @skip_if_openai_integration_tests_disabled async def test_openai_assistants_agent_code_interpreter(): - """Test ChatAgent with code interpreter through OpenAIAssistantsClient.""" + """Test Agent with code interpreter through OpenAIAssistantsClient.""" - async with ChatAgent( - chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + async with Agent( + client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), instructions="You are a helpful assistant that can write and execute Python code.", - tools=[HostedCodeInterpreterTool()], + tools=[OpenAIAssistantsClient.get_code_interpreter_tool()], ) as agent: # Request code execution response = await agent.run("Write Python code to calculate the factorial of 5 and show the result.") @@ -1365,8 +1361,8 @@ async def test_openai_assistants_agent_code_interpreter(): async def test_agent_level_tool_persistence(): """Test that agent-level tools persist across multiple runs with OpenAI Assistants Client.""" - async with ChatAgent( - chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), + async with Agent( + client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL), instructions="You are a helpful assistant that uses available tools.", tools=[get_weather], # Agent-level tool ) as agent: diff --git a/python/packages/core/tests/openai/test_openai_chat_client.py b/python/packages/core/tests/openai/test_openai_chat_client.py index 7b5f0cde13..6458a38402 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client.py +++ b/python/packages/core/tests/openai/test_openai_chat_client.py @@ -13,12 +13,10 @@ from pydantic import BaseModel from pytest import param from agent_framework import ( - ChatClientProtocol, - ChatMessage, ChatResponse, Content, - HostedWebSearchTool, - ToolProtocol, + Message, + SupportsChatGetResponse, prepare_function_call_results, tool, ) @@ -40,7 +38,7 @@ def test_init(openai_unit_test_env: dict[str, str]) -> None: open_ai_chat_completion = OpenAIChatClient() assert open_ai_chat_completion.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] - assert isinstance(open_ai_chat_completion, ChatClientProtocol) + assert isinstance(open_ai_chat_completion, SupportsChatGetResponse) def test_init_validation_fail() -> None: @@ -55,7 +53,7 @@ def test_init_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None open_ai_chat_completion = OpenAIChatClient(model_id=model_id) assert open_ai_chat_completion.model_id == model_id - assert isinstance(open_ai_chat_completion, ChatClientProtocol) + assert isinstance(open_ai_chat_completion, SupportsChatGetResponse) def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: @@ -67,7 +65,7 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: ) assert open_ai_chat_completion.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] - assert isinstance(open_ai_chat_completion, ChatClientProtocol) + assert isinstance(open_ai_chat_completion, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers for key, value in default_headers.items(): @@ -154,7 +152,7 @@ def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None: async def test_content_filter_exception_handling(openai_unit_test_env: dict[str, str]) -> None: """Test that content filter errors are properly handled.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] # Create a mock BadRequestError with content_filter code mock_response = MagicMock() @@ -172,18 +170,22 @@ async def test_content_filter_exception_handling(openai_unit_test_env: dict[str, def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None: - """Test that unsupported tool types are handled correctly.""" + """Test that unsupported tool types are passed through unchanged.""" client = OpenAIChatClient() - # Create a mock ToolProtocol that's not a FunctionTool - unsupported_tool = MagicMock(spec=ToolProtocol) - unsupported_tool.__class__.__name__ = "UnsupportedAITool" + # Create a random object that's not a FunctionTool, dict, or callable + # This simulates an unsupported tool type that gets passed through + class UnsupportedTool: + pass - # This should ignore the unsupported ToolProtocol and return empty list + unsupported_tool = UnsupportedTool() + + # Unsupported tools are passed through for the API to handle/reject result = client._prepare_tools_for_openai([unsupported_tool]) # type: ignore - assert result == {} + assert "tools" in result + assert len(result["tools"]) == 1 - # Also test with a non-ToolProtocol that should be converted to dict + # Also test with a dict-based tool that should be passed through dict_tool = {"type": "function", "name": "test"} result = client._prepare_tools_for_openai([dict_tool]) # type: ignore assert result["tools"] == [dict_tool] @@ -209,7 +211,7 @@ def get_weather(location: str) -> str: async def test_exception_message_includes_original_error_details() -> None: """Test that exception messages include original error details in the new format.""" client = OpenAIChatClient(model_id="test-model", api_key="test-key") - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] mock_response = MagicMock() original_error_message = "Invalid API request format" @@ -283,7 +285,7 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s client = OpenAIChatClient() # Test with empty list (falsy but not None) - message_with_empty_list = ChatMessage( + message_with_empty_list = Message( role="tool", contents=[Content.from_function_result(call_id="call-123", result=[])] ) @@ -292,7 +294,7 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s assert openai_messages[0]["content"] == "[]" # Empty list should be JSON serialized # Test with empty string (falsy but not None) - message_with_empty_string = ChatMessage( + message_with_empty_string = Message( role="tool", contents=[Content.from_function_result(call_id="call-456", result="")] ) @@ -301,9 +303,7 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s assert openai_messages[0]["content"] == "" # Empty string should be preserved # Test with False (falsy but not None) - message_with_false = ChatMessage( - role="tool", contents=[Content.from_function_result(call_id="call-789", result=False)] - ) + message_with_false = Message(role="tool", contents=[Content.from_function_result(call_id="call-789", result=False)]) openai_messages = client._prepare_message_for_openai(message_with_false) assert len(openai_messages) == 1 @@ -319,7 +319,7 @@ def test_function_result_exception_handling(openai_unit_test_env: dict[str, str] # Test with exception (no result) test_exception = ValueError("Test error message") - message_with_exception = ChatMessage( + message_with_exception = Message( role="tool", contents=[ Content.from_function_result(call_id="call-123", result="Error: Function failed.", exception=test_exception) @@ -609,7 +609,7 @@ def test_prepare_message_with_text_reasoning_content(openai_unit_test_env: dict[ reasoning_content = Content.from_text_reasoning(text=None, protected_data=json.dumps(mock_reasoning_data)) # Message must have other content first for reasoning to attach to - message = ChatMessage( + message = Message( role="assistant", contents=[ Content.from_text(text="The answer is 42."), @@ -652,17 +652,17 @@ def test_function_approval_content_is_skipped_in_preparation(openai_unit_test_en ) # Test that approval request is skipped - message_with_request = ChatMessage(role="assistant", contents=[approval_request]) + message_with_request = Message(role="assistant", contents=[approval_request]) prepared_request = client._prepare_message_for_openai(message_with_request) assert len(prepared_request) == 0 # Should be empty - approval content is skipped # Test that approval response is skipped - message_with_response = ChatMessage(role="user", contents=[approval_response]) + message_with_response = Message(role="user", contents=[approval_response]) prepared_response = client._prepare_message_for_openai(message_with_response) assert len(prepared_response) == 0 # Should be empty - approval content is skipped # Test with mixed content - approval should be skipped, text should remain - mixed_message = ChatMessage( + mixed_message = Message( role="assistant", contents=[ Content.from_text(text="I need approval for this action."), @@ -752,7 +752,7 @@ def test_prepare_options_without_model_id(openai_unit_test_env: dict[str, str]) client = OpenAIChatClient() client.model_id = None # Remove model_id - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] with pytest.raises(ValueError, match="model_id must be a non-empty string"): client._prepare_options(messages, {}) @@ -772,8 +772,8 @@ def test_prepare_tools_with_web_search_no_location(openai_unit_test_env: dict[st """Test preparing web search tool without user location.""" client = OpenAIChatClient() - # Web search tool without additional_properties - web_search_tool = HostedWebSearchTool() + # Web search tool using static method + web_search_tool = OpenAIChatClient.get_web_search_tool() result = client._prepare_tools_for_openai([web_search_tool]) @@ -786,7 +786,7 @@ def test_prepare_options_with_instructions(openai_unit_test_env: dict[str, str]) """Test that instructions are prepended as system message.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] options = {"instructions": "You are a helpful assistant."} prepared_options = client._prepare_options(messages, options) @@ -802,7 +802,7 @@ def test_prepare_message_with_author_name(openai_unit_test_env: dict[str, str]) """Test that author_name is included in prepared message.""" client = OpenAIChatClient() - message = ChatMessage( + message = Message( role="user", author_name="TestUser", contents=[Content.from_text(text="Hello")], @@ -819,7 +819,7 @@ def test_prepare_message_with_tool_result_author_name(openai_unit_test_env: dict client = OpenAIChatClient() # Tool messages should not have 'name' field (it's for function name instead) - message = ChatMessage( + message = Message( role="tool", author_name="ShouldNotAppear", contents=[Content.from_function_result(call_id="call_123", result="result")], @@ -836,7 +836,7 @@ def test_tool_choice_required_with_function_name(openai_unit_test_env: dict[str, """Test that tool_choice with required mode and function name is correctly prepared.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] options = { "tools": [get_weather], "tool_choice": {"mode": "required", "required_function_name": "get_weather"}, @@ -854,7 +854,7 @@ def test_response_format_dict_passthrough(openai_unit_test_env: dict[str, str]) """Test that response_format as dict is passed through directly.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] custom_format = { "type": "json_schema", "json_schema": {"name": "Test", "schema": {"type": "object"}}, @@ -872,7 +872,7 @@ def test_multiple_function_calls_in_single_message(openai_unit_test_env: dict[st client = OpenAIChatClient() # Create message with multiple function calls - message = ChatMessage( + message = Message( role="assistant", contents=[ Content.from_function_call(call_id="call_1", name="func_1", arguments='{"a": 1}'), @@ -894,7 +894,7 @@ def test_prepare_options_removes_parallel_tool_calls_when_no_tools(openai_unit_t """Test that parallel_tool_calls is removed when no tools are present.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] options = {"allow_multiple_tool_calls": True} prepared_options = client._prepare_options(messages, options) @@ -906,7 +906,7 @@ def test_prepare_options_removes_parallel_tool_calls_when_no_tools(openai_unit_t async def test_streaming_exception_handling(openai_unit_test_env: dict[str, str]) -> None: """Test that streaming errors are properly handled.""" client = OpenAIChatClient() - messages = [ChatMessage(role="user", text="test")] + messages = [Message(role="user", text="test")] # Create a mock error during streaming mock_error = Exception("Streaming error") @@ -1004,14 +1004,14 @@ async def test_integration_options( # Prepare test message if option_name.startswith("tools") or option_name.startswith("tool_choice"): # Use weather-related prompt for tool tests - messages = [ChatMessage(role="user", text="What is the weather in Seattle?")] + messages = [Message(role="user", text="What is the weather in Seattle?")] elif option_name.startswith("response_format"): # Use prompt that works well with structured output - messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")] - messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) + messages = [Message(role="user", text="The weather in Seattle is sunny")] + messages.append(Message(role="user", text="What is the weather in Seattle?")) else: # Generic prompt for simple options - messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")] + messages = [Message(role="user", text="Say 'Hello World' briefly.")] # Build options dict options: dict[str, Any] = {option_name: option_value} @@ -1073,11 +1073,13 @@ async def test_integration_web_search() -> None: client = OpenAIChatClient(model_id="gpt-4o-search-preview") for streaming in [False, True]: + # Use static method for web search tool + web_search_tool = OpenAIChatClient.get_web_search_tool() content = { "messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.", "options": { "tool_choice": "auto", - "tools": [HostedWebSearchTool()], + "tools": [web_search_tool], }, } if streaming: @@ -1092,17 +1094,19 @@ async def test_integration_web_search() -> None: assert "Zoey" in response.text # Test that the client will use the web search tool with location - additional_properties = { - "user_location": { - "country": "US", - "city": "Seattle", + web_search_tool_with_location = OpenAIChatClient.get_web_search_tool( + web_search_options={ + "user_location": { + "type": "approximate", + "approximate": {"country": "US", "city": "Seattle"}, + }, } - } + ) content = { "messages": "What is the current weather? Do not ask for my current location.", "options": { "tool_choice": "auto", - "tools": [HostedWebSearchTool(additional_properties=additional_properties)], + "tools": [web_search_tool_with_location], }, } if streaming: diff --git a/python/packages/core/tests/openai/test_openai_chat_client_base.py b/python/packages/core/tests/openai/test_openai_chat_client_base.py index 51a7ae0bc3..4c31394fb6 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client_base.py +++ b/python/packages/core/tests/openai/test_openai_chat_client_base.py @@ -14,7 +14,7 @@ from openai.types.chat.chat_completion_chunk import ChoiceDelta as ChunkChoiceDe from openai.types.chat.chat_completion_message import ChatCompletionMessage from pydantic import BaseModel -from agent_framework import ChatMessage, ChatResponseUpdate +from agent_framework import ChatResponseUpdate, Message from agent_framework.exceptions import ( ServiceResponseException, ) @@ -27,7 +27,7 @@ async def mock_async_process_chat_stream_response(_): @pytest.fixture(scope="function") -def chat_history() -> list[ChatMessage]: +def chat_history() -> list[Message]: return [] @@ -64,12 +64,12 @@ def mock_streaming_chat_completion_response() -> AsyncStream[ChatCompletionChunk @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_cmc( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) openai_chat_completion = OpenAIChatClient() await openai_chat_completion.get_response(messages=chat_history) @@ -83,12 +83,12 @@ async def test_cmc( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_cmc_chat_options( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) openai_chat_completion = OpenAIChatClient() await openai_chat_completion.get_response( @@ -104,12 +104,12 @@ async def test_cmc_chat_options( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_cmc_no_fcc_in_response( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) orig_chat_history = deepcopy(chat_history) openai_chat_completion = OpenAIChatClient() @@ -126,12 +126,12 @@ async def test_cmc_no_fcc_in_response( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_cmc_structured_output_no_fcc( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) # Define a mock response format class Test(BaseModel): @@ -148,12 +148,12 @@ async def test_cmc_structured_output_no_fcc( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_scmc_chat_options( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], mock_streaming_chat_completion_response: AsyncStream[ChatCompletionChunk], openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_streaming_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) openai_chat_completion = OpenAIChatClient() async for msg in openai_chat_completion.get_response( @@ -174,12 +174,12 @@ async def test_scmc_chat_options( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock, side_effect=Exception) async def test_cmc_general_exception( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) openai_chat_completion = OpenAIChatClient() with pytest.raises(ServiceResponseException): @@ -191,12 +191,12 @@ async def test_cmc_general_exception( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_cmc_additional_properties( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: ChatCompletion, openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) openai_chat_completion = OpenAIChatClient() await openai_chat_completion.get_response(messages=chat_history, options={"reasoning_effort": "low"}) @@ -214,7 +214,7 @@ async def test_cmc_additional_properties( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_get_streaming( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], openai_unit_test_env: dict[str, str], ): content1 = ChatCompletionChunk( @@ -234,7 +234,7 @@ async def test_get_streaming( stream = MagicMock(spec=AsyncStream) stream.__aiter__.return_value = [content1, content2] mock_create.return_value = stream - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) orig_chat_history = deepcopy(chat_history) openai_chat_completion = OpenAIChatClient() @@ -254,7 +254,7 @@ async def test_get_streaming( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_get_streaming_singular( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], openai_unit_test_env: dict[str, str], ): content1 = ChatCompletionChunk( @@ -274,7 +274,7 @@ async def test_get_streaming_singular( stream = MagicMock(spec=AsyncStream) stream.__aiter__.return_value = [content1, content2] mock_create.return_value = stream - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) orig_chat_history = deepcopy(chat_history) openai_chat_completion = OpenAIChatClient() @@ -294,7 +294,7 @@ async def test_get_streaming_singular( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_get_streaming_structured_output_no_fcc( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], openai_unit_test_env: dict[str, str], ): content1 = ChatCompletionChunk( @@ -314,7 +314,7 @@ async def test_get_streaming_structured_output_no_fcc( stream = MagicMock(spec=AsyncStream) stream.__aiter__.return_value = [content1, content2] mock_create.return_value = stream - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) # Define a mock response format class Test(BaseModel): @@ -333,12 +333,12 @@ async def test_get_streaming_structured_output_no_fcc( @patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock) async def test_get_streaming_no_fcc_in_response( mock_create: AsyncMock, - chat_history: list[ChatMessage], + chat_history: list[Message], mock_streaming_chat_completion_response: ChatCompletion, openai_unit_test_env: dict[str, str], ): mock_create.return_value = mock_streaming_chat_completion_response - chat_history.append(ChatMessage(role="user", text="hello world")) + chat_history.append(Message(role="user", text="hello world")) orig_chat_history = deepcopy(chat_history) openai_chat_completion = OpenAIChatClient() diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py index d4259f22ad..e51ed4e989 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/core/tests/openai/test_openai_responses_client.py @@ -27,17 +27,12 @@ from pydantic import BaseModel from pytest import param from agent_framework import ( - ChatClientProtocol, - ChatMessage, ChatOptions, ChatResponse, ChatResponseUpdate, Content, - HostedCodeInterpreterTool, - HostedFileSearchTool, - HostedImageGenerationTool, - HostedMCPTool, - HostedWebSearchTool, + Message, + SupportsChatGetResponse, tool, ) from agent_framework.exceptions import ( @@ -106,7 +101,7 @@ def test_init(openai_unit_test_env: dict[str, str]) -> None: openai_responses_client = OpenAIResponsesClient() assert openai_responses_client.model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] - assert isinstance(openai_responses_client, ChatClientProtocol) + assert isinstance(openai_responses_client, SupportsChatGetResponse) def test_init_validation_fail() -> None: @@ -121,7 +116,7 @@ def test_init_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None openai_responses_client = OpenAIResponsesClient(model_id=model_id) assert openai_responses_client.model_id == model_id - assert isinstance(openai_responses_client, ChatClientProtocol) + assert isinstance(openai_responses_client, SupportsChatGetResponse) def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: @@ -133,7 +128,7 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: ) assert openai_responses_client.model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] - assert isinstance(openai_responses_client, ChatClientProtocol) + assert isinstance(openai_responses_client, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers for key, value in default_headers.items(): @@ -211,7 +206,7 @@ async def test_get_response_with_all_parameters() -> None: # Test with comprehensive parameter set - should fail due to invalid API key with pytest.raises(ServiceResponseException): await client.get_response( - messages=[ChatMessage(role="user", text="Test message")], + messages=[Message(role="user", text="Test message")], options={ "include": ["message.output_text.logprobs"], "instructions": "You are a helpful assistant", @@ -236,66 +231,48 @@ async def test_get_response_with_all_parameters() -> None: ) +@pytest.mark.asyncio async def test_web_search_tool_with_location() -> None: - """Test HostedWebSearchTool with location parameters.""" + """Test web search tool with location parameters.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - # Test web search tool with location - web_search_tool = HostedWebSearchTool( - additional_properties={ - "user_location": { - "country": "US", - "city": "Seattle", - "region": "WA", - "timezone": "America/Los_Angeles", - } + # Test web search tool with location using static method + web_search_tool = OpenAIResponsesClient.get_web_search_tool( + user_location={ + "city": "Seattle", + "country": "US", + "region": "WA", + "timezone": "America/Los_Angeles", } ) # Should raise an authentication error due to invalid API key with pytest.raises(ServiceResponseException): await client.get_response( - messages=[ChatMessage(role="user", text="What's the weather?")], + messages=[Message(role="user", text="What's the weather?")], options={"tools": [web_search_tool], "tool_choice": "auto"}, ) -async def test_file_search_tool_with_invalid_inputs() -> None: - """Test HostedFileSearchTool with invalid vector store inputs.""" - client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - - # Test with invalid inputs type (should trigger ValueError) - file_search_tool = HostedFileSearchTool(inputs=[Content.from_hosted_file(file_id="invalid")]) - - # Should raise an error due to invalid inputs - with pytest.raises(ValueError, match="HostedFileSearchTool requires inputs to be of type"): - await client.get_response( - messages=[ChatMessage(role="user", text="Search files")], - options={"tools": [file_search_tool]}, - ) - - async def test_code_interpreter_tool_variations() -> None: """Test HostedCodeInterpreterTool with and without file inputs.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - # Test code interpreter without files - code_tool_empty = HostedCodeInterpreterTool() + # Test code interpreter using static method + code_tool = OpenAIResponsesClient.get_code_interpreter_tool() with pytest.raises(ServiceResponseException): await client.get_response( - messages=[ChatMessage(role="user", text="Run some code")], - options={"tools": [code_tool_empty]}, + messages=[Message("user", ["Run some code"])], + options={"tools": [code_tool]}, ) - # Test code interpreter with files - code_tool_with_files = HostedCodeInterpreterTool( - inputs=[Content.from_hosted_file(file_id="file1"), Content.from_hosted_file(file_id="file2")] - ) + # Test code interpreter with files using static method + code_tool_with_files = OpenAIResponsesClient.get_code_interpreter_tool(file_ids=["file1", "file2"]) with pytest.raises(ServiceResponseException): await client.get_response( - messages=[ChatMessage(role="user", text="Process these files")], + messages=[Message(role="user", text="Process these files")], options={"tools": [code_tool_with_files]}, ) @@ -314,23 +291,25 @@ async def test_content_filter_exception() -> None: with patch.object(client.client.responses, "create", side_effect=mock_error): with pytest.raises(OpenAIContentFilterException) as exc_info: - await client.get_response(messages=[ChatMessage(role="user", text="Test message")]) + await client.get_response(messages=[Message(role="user", text="Test message")]) assert "content error" in str(exc_info.value) +@pytest.mark.asyncio async def test_hosted_file_search_tool_validation() -> None: """Test get_response HostedFileSearchTool validation.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - # Test HostedFileSearchTool without inputs (should raise ValueError) - empty_file_search_tool = HostedFileSearchTool() + # Test file search tool with vector store IDs + file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=["vs_123"]) - with pytest.raises((ValueError, ServiceInvalidRequestError)): + # Test using file search tool - may raise various exceptions depending on API response + with pytest.raises((ValueError, ServiceInvalidRequestError, ServiceResponseException)): await client.get_response( - messages=[ChatMessage(role="user", text="Test")], - options={"tools": [empty_file_search_tool]}, + messages=[Message("user", ["Test"])], + options={"tools": [file_search_tool]}, ) @@ -349,9 +328,9 @@ async def test_chat_message_parsing_with_function_calls() -> None: function_result = Content.from_function_result(call_id="test-call-id", result="Function executed successfully") messages = [ - ChatMessage(role="user", text="Call a function"), - ChatMessage(role="assistant", contents=[function_call]), - ChatMessage(role="tool", contents=[function_result]), + Message(role="user", text="Call a function"), + Message(role="assistant", contents=[function_call]), + Message(role="tool", contents=[function_result]), ] # This should exercise the message parsing logic - will fail due to invalid API key @@ -377,7 +356,7 @@ async def test_response_format_parse_path() -> None: with patch.object(client.client.responses, "parse", return_value=mock_parsed_response): response = await client.get_response( - messages=[ChatMessage(role="user", text="Test message")], + messages=[Message(role="user", text="Test message")], options={"response_format": OutputStruct, "store": True}, ) assert response.response_id == "parsed_response_123" @@ -404,7 +383,7 @@ async def test_response_format_parse_path_with_conversation_id() -> None: with patch.object(client.client.responses, "parse", return_value=mock_parsed_response): response = await client.get_response( - messages=[ChatMessage(role="user", text="Test message")], + messages=[Message(role="user", text="Test message")], options={"response_format": OutputStruct, "store": True}, ) assert response.response_id == "parsed_response_123" @@ -427,7 +406,7 @@ async def test_bad_request_error_non_content_filter() -> None: with patch.object(client.client.responses, "parse", side_effect=mock_error): with pytest.raises(ServiceResponseException) as exc_info: await client.get_response( - messages=[ChatMessage(role="user", text="Test message")], + messages=[Message(role="user", text="Test message")], options={"response_format": OutputStruct}, ) @@ -448,7 +427,7 @@ async def test_streaming_content_filter_exception_handling() -> None: mock_create.side_effect.code = "content_filter" with pytest.raises(OpenAIContentFilterException, match="service encountered a content error"): - response_stream = client.get_response(stream=True, messages=[ChatMessage(role="user", text="Test")]) + response_stream = client.get_response(stream=True, messages=[Message(role="user", text="Test")]) async for _ in response_stream: break @@ -792,7 +771,7 @@ def test_prepare_message_for_openai_with_function_approval_response() -> None: function_call=function_call, ) - message = ChatMessage(role="user", contents=[approval_response]) + message = Message(role="user", contents=[approval_response]) call_id_to_id: dict[str, str] = {} result = client._prepare_message_for_openai(message, call_id_to_id) @@ -814,7 +793,7 @@ def test_chat_message_with_error_content() -> None: error_code="TEST_ERR", ) - message = ChatMessage(role="assistant", contents=[error_content]) + message = Message(role="assistant", contents=[error_content]) call_id_to_id: dict[str, str] = {} result = client._prepare_message_for_openai(message, call_id_to_id) @@ -839,7 +818,7 @@ def test_chat_message_with_usage_content() -> None: } ) - message = ChatMessage(role="assistant", contents=[usage_content]) + message = Message(role="assistant", contents=[usage_content]) call_id_to_id: dict[str, str] = {} result = client._prepare_message_for_openai(message, call_id_to_id) @@ -1074,18 +1053,17 @@ def test_streaming_chunk_with_usage_only() -> None: assert update.contents[0].usage_details["total_token_count"] == 75 -def test_prepare_tools_for_openai_with_hosted_mcp() -> None: - """Test that HostedMCPTool is converted to the correct response tool dict.""" +def test_prepare_tools_for_openai_with_mcp() -> None: + """Test that MCP tool dict is converted to the correct response tool dict.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - tool = HostedMCPTool( - name="My MCP", + # Use static method to create MCP tool + tool = OpenAIResponsesClient.get_mcp_tool( + name="My_MCP", url="https://mcp.example", - description="An MCP server", - approval_mode={"always_require_approval": ["tool_a", "tool_b"]}, - allowed_tools={"tool_a", "tool_b"}, + allowed_tools=["tool_a", "tool_b"], headers={"X-Test": "yes"}, - additional_properties={"custom": "value"}, + approval_mode={"always_require_approval": ["tool_a", "tool_b"]}, ) resp_tools = client._prepare_tools_for_openai([tool]) @@ -1097,7 +1075,6 @@ def test_prepare_tools_for_openai_with_hosted_mcp() -> None: assert mcp["server_label"] == "My_MCP" # server_url may be normalized to include a trailing slash by the client assert str(mcp["server_url"]).rstrip("/") == "https://mcp.example" - assert mcp["server_description"] == "An MCP server" assert mcp["headers"]["X-Test"] == "yes" assert set(mcp["allowed_tools"]) == {"tool_a", "tool_b"} # approval mapping created from approval_mode dict @@ -1258,13 +1235,15 @@ def test_prepare_tools_for_openai_with_raw_image_generation_minimal() -> None: assert len(image_tool) == 1 -def test_prepare_tools_for_openai_with_hosted_image_generation() -> None: - """Test HostedImageGenerationTool conversion.""" +def test_prepare_tools_for_openai_with_image_generation_options() -> None: + """Test image generation tool conversion with options.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - tool = HostedImageGenerationTool( - description="Generate images", - options={"output_format": "png", "size": "512x512"}, - additional_properties={"quality": "high"}, + + # Use static method to create image generation tool + tool = OpenAIResponsesClient.get_image_generation_tool( + output_format="png", + size="512x512", + quality="high", ) resp_tools = client._prepare_tools_for_openai([tool]) @@ -1343,14 +1322,14 @@ async def test_end_to_end_mcp_approval_flow(span_exporter) -> None: # Patch the create call to return the two mocked responses in sequence with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: # First call: get the approval request - response = await client.get_response(messages=[ChatMessage(role="user", text="Trigger approval")]) + response = await client.get_response(messages=[Message(role="user", text="Trigger approval")]) assert response.messages[0].contents[0].type == "function_approval_request" req = response.messages[0].contents[0] assert req.id == "approval-1" # Build a user approval and send it (include required function_call) approval = Content.from_function_approval_response(approved=True, id=req.id, function_call=req.function_call) - approval_message = ChatMessage(role="user", contents=[approval]) + approval_message = Message(role="user", contents=[approval]) _ = await client.get_response(messages=[approval_message]) # After approval is processed, the model is called again to get the final response @@ -1595,7 +1574,7 @@ def test_streaming_annotation_added_with_unknown_type() -> None: async def test_service_response_exception_includes_original_error_details() -> None: """Test that ServiceResponseException messages include original error details in the new format.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - messages = [ChatMessage(role="user", text="test message")] + messages = [Message(role="user", text="test message")] mock_response = MagicMock() original_error_message = "Request rate limit exceeded" @@ -1620,7 +1599,7 @@ async def test_service_response_exception_includes_original_error_details() -> N async def test_get_response_streaming_with_response_format() -> None: """Test get_response streaming with response_format.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - messages = [ChatMessage(role="user", text="Test streaming with format")] + messages = [Message(role="user", text="Test streaming with format")] # It will fail due to invalid API key, but exercises the code path with pytest.raises(ServiceResponseException): @@ -2126,7 +2105,7 @@ def test_parse_response_from_openai_image_generation_fallback(): async def test_prepare_options_store_parameter_handling() -> None: client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] test_conversation_id = "test-conversation-123" chat_options = ChatOptions(store=True, conversation_id=test_conversation_id) @@ -2152,7 +2131,7 @@ async def test_prepare_options_store_parameter_handling() -> None: async def test_conversation_id_precedence_kwargs_over_options() -> None: """When both kwargs and options contain conversation_id, kwargs wins.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] # options has a stale response id, kwargs carries the freshest one opts = {"conversation_id": "resp_old_123"} @@ -2259,14 +2238,14 @@ async def test_integration_options( # Prepare test message if option_name.startswith("tools") or option_name.startswith("tool_choice"): # Use weather-related prompt for tool tests - messages = [ChatMessage(role="user", text="What is the weather in Seattle?")] + messages = [Message(role="user", text="What is the weather in Seattle?")] elif option_name.startswith("response_format"): # Use prompt that works well with structured output - messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")] - messages.append(ChatMessage(role="user", text="What is the weather in Seattle?")) + messages = [Message(role="user", text="The weather in Seattle is sunny")] + messages.append(Message(role="user", text="What is the weather in Seattle?")) else: # Generic prompt for simple options - messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")] + messages = [Message(role="user", text="Say 'Hello World' briefly.")] # Build options dict options: dict[str, Any] = {option_name: option_value} @@ -2324,11 +2303,13 @@ async def test_integration_web_search() -> None: client = OpenAIResponsesClient(model_id="gpt-5") for streaming in [False, True]: + # Use static method for web search tool + web_search_tool = OpenAIResponsesClient.get_web_search_tool() content = { "messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.", "options": { "tool_choice": "auto", - "tools": [HostedWebSearchTool()], + "tools": [web_search_tool], }, } if streaming: @@ -2343,17 +2324,14 @@ async def test_integration_web_search() -> None: assert "Zoey" in response.text # Test that the client will use the web search tool with location - additional_properties = { - "user_location": { - "country": "US", - "city": "Seattle", - } - } + web_search_tool_with_location = OpenAIResponsesClient.get_web_search_tool( + user_location={"country": "US", "city": "Seattle"}, + ) content = { "messages": "What is the current weather? Do not ask for my current location.", "options": { "tool_choice": "auto", - "tools": [HostedWebSearchTool(additional_properties=additional_properties)], + "tools": [web_search_tool_with_location], }, } if streaming: @@ -2372,20 +2350,22 @@ async def test_integration_web_search() -> None: async def test_integration_file_search() -> None: openai_responses_client = OpenAIResponsesClient() - assert isinstance(openai_responses_client, ChatClientProtocol) + assert isinstance(openai_responses_client, SupportsChatGetResponse) file_id, vector_store = await create_vector_store(openai_responses_client) - # Test that the client will use the web search tool + # Use static method for file search tool + file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) + # Test that the client will use the file search tool response = await openai_responses_client.get_response( messages=[ - ChatMessage( + Message( role="user", text="What is the weather today? Do a file search to find the answer.", ) ], options={ "tool_choice": "auto", - "tools": [HostedFileSearchTool(inputs=vector_store)], + "tools": [file_search_tool], }, ) @@ -2403,21 +2383,22 @@ async def test_integration_file_search() -> None: async def test_integration_streaming_file_search() -> None: openai_responses_client = OpenAIResponsesClient() - assert isinstance(openai_responses_client, ChatClientProtocol) + assert isinstance(openai_responses_client, SupportsChatGetResponse) file_id, vector_store = await create_vector_store(openai_responses_client) + # Use static method for file search tool + file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) # Test that the client will use the web search tool - response = openai_responses_client.get_response( - stream=True, + response = openai_responses_client.get_streaming_response( messages=[ - ChatMessage( + Message( role="user", text="What is the weather today? Do a file search to find the answer.", ) ], options={ "tool_choice": "auto", - "tools": [HostedFileSearchTool(inputs=vector_store)], + "tools": [file_search_tool], }, ) @@ -2434,3 +2415,263 @@ async def test_integration_streaming_file_search() -> None: assert "sunny" in full_message.lower() assert "75" in full_message + + +# region Background Response / ContinuationToken Tests + + +def test_continuation_token_json_serializable() -> None: + """Test that OpenAIContinuationToken is a plain dict and JSON-serializable.""" + from agent_framework.openai import OpenAIContinuationToken + + token = OpenAIContinuationToken(response_id="resp_abc123") + assert token["response_id"] == "resp_abc123" + + # JSON round-trip + serialized = json.dumps(token) + restored = json.loads(serialized) + assert restored["response_id"] == "resp_abc123" + + +def test_chat_response_with_continuation_token() -> None: + """Test that ChatResponse accepts and stores continuation_token.""" + from agent_framework.openai import OpenAIContinuationToken + + token = OpenAIContinuationToken(response_id="resp_123") + response = ChatResponse( + messages=Message(role="assistant", contents=[Content.from_text(text="Hello")]), + response_id="resp_123", + continuation_token=token, + ) + assert response.continuation_token is not None + assert response.continuation_token["response_id"] == "resp_123" + + +def test_chat_response_without_continuation_token() -> None: + """Test that ChatResponse defaults continuation_token to None.""" + response = ChatResponse( + messages=Message(role="assistant", contents=[Content.from_text(text="Hello")]), + ) + assert response.continuation_token is None + + +def test_chat_response_update_with_continuation_token() -> None: + """Test that ChatResponseUpdate accepts and stores continuation_token.""" + from agent_framework.openai import OpenAIContinuationToken + + token = OpenAIContinuationToken(response_id="resp_456") + update = ChatResponseUpdate( + contents=[Content.from_text(text="chunk")], + role="assistant", + continuation_token=token, + ) + assert update.continuation_token is not None + assert update.continuation_token["response_id"] == "resp_456" + + +def test_agent_response_with_continuation_token() -> None: + """Test that AgentResponse accepts and stores continuation_token.""" + from agent_framework import AgentResponse + from agent_framework.openai import OpenAIContinuationToken + + token = OpenAIContinuationToken(response_id="resp_789") + response = AgentResponse( + messages=Message(role="assistant", contents=[Content.from_text(text="done")]), + continuation_token=token, + ) + assert response.continuation_token is not None + assert response.continuation_token["response_id"] == "resp_789" + + +def test_agent_response_update_with_continuation_token() -> None: + """Test that AgentResponseUpdate accepts and stores continuation_token.""" + from agent_framework import AgentResponseUpdate + from agent_framework.openai import OpenAIContinuationToken + + token = OpenAIContinuationToken(response_id="resp_012") + update = AgentResponseUpdate( + contents=[Content.from_text(text="streaming")], + role="assistant", + continuation_token=token, + ) + assert update.continuation_token is not None + assert update.continuation_token["response_id"] == "resp_012" + + +def test_parse_response_from_openai_with_background_in_progress() -> None: + """Test that _parse_response_from_openai sets continuation_token when status is in_progress.""" + client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + + mock_response = MagicMock() + mock_response.output_parsed = None + mock_response.metadata = {} + mock_response.usage = None + mock_response.id = "resp_bg_123" + mock_response.model = "test-model" + mock_response.created_at = 1000000000 + mock_response.status = "in_progress" + + mock_message = MagicMock() + mock_message.type = "message" + mock_message.content = [] + mock_response.output = [mock_message] + + options: dict[str, Any] = {"store": False} + result = client._parse_response_from_openai(mock_response, options=options) + + assert result.continuation_token is not None + assert result.continuation_token["response_id"] == "resp_bg_123" + + +def test_parse_response_from_openai_with_background_queued() -> None: + """Test that _parse_response_from_openai sets continuation_token when status is queued.""" + client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + + mock_response = MagicMock() + mock_response.output_parsed = None + mock_response.metadata = {} + mock_response.usage = None + mock_response.id = "resp_bg_456" + mock_response.model = "test-model" + mock_response.created_at = 1000000000 + mock_response.status = "queued" + + mock_message = MagicMock() + mock_message.type = "message" + mock_message.content = [] + mock_response.output = [mock_message] + + options: dict[str, Any] = {"store": False} + result = client._parse_response_from_openai(mock_response, options=options) + + assert result.continuation_token is not None + assert result.continuation_token["response_id"] == "resp_bg_456" + + +def test_parse_response_from_openai_with_background_completed() -> None: + """Test that _parse_response_from_openai does NOT set continuation_token when status is completed.""" + client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + + mock_response = MagicMock() + mock_response.output_parsed = None + mock_response.metadata = {} + mock_response.usage = None + mock_response.id = "resp_bg_789" + mock_response.model = "test-model" + mock_response.created_at = 1000000000 + mock_response.status = "completed" + + mock_text_content = MagicMock() + mock_text_content.type = "output_text" + mock_text_content.text = "Final answer" + mock_text_content.annotations = [] + mock_text_content.logprobs = None + + mock_message = MagicMock() + mock_message.type = "message" + mock_message.content = [mock_text_content] + mock_response.output = [mock_message] + + options: dict[str, Any] = {"store": False} + result = client._parse_response_from_openai(mock_response, options=options) + + assert result.continuation_token is None + + +def test_streaming_response_in_progress_sets_continuation_token() -> None: + """Test that _parse_chunk_from_openai sets continuation_token for in_progress events.""" + client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + chat_options: dict[str, Any] = {} + function_call_ids: dict[int, tuple[str, str]] = {} + + mock_event = MagicMock() + mock_event.type = "response.in_progress" + mock_event.response = MagicMock() + mock_event.response.id = "resp_stream_123" + mock_event.response.conversation = MagicMock() + mock_event.response.conversation.id = "conv_456" + mock_event.response.status = "in_progress" + + update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + + assert update.continuation_token is not None + assert update.continuation_token["response_id"] == "resp_stream_123" + + +def test_streaming_response_created_with_in_progress_status_sets_continuation_token() -> None: + """Test that response.created with in_progress status sets continuation_token.""" + client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + chat_options: dict[str, Any] = {} + function_call_ids: dict[int, tuple[str, str]] = {} + + mock_event = MagicMock() + mock_event.type = "response.created" + mock_event.response = MagicMock() + mock_event.response.id = "resp_created_123" + mock_event.response.conversation = MagicMock() + mock_event.response.conversation.id = "conv_789" + mock_event.response.status = "in_progress" + + update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + + assert update.continuation_token is not None + assert update.continuation_token["response_id"] == "resp_created_123" + + +def test_streaming_response_completed_no_continuation_token() -> None: + """Test that response.completed does NOT set continuation_token.""" + client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + chat_options: dict[str, Any] = {} + function_call_ids: dict[int, tuple[str, str]] = {} + + mock_event = MagicMock() + mock_event.type = "response.completed" + mock_event.response = MagicMock() + mock_event.response.id = "resp_done_123" + mock_event.response.conversation = MagicMock() + mock_event.response.conversation.id = "conv_done" + mock_event.response.model = "test-model" + mock_event.response.usage = None + + update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + + assert update.continuation_token is None + + +def test_map_chat_to_agent_update_preserves_continuation_token() -> None: + """Test that map_chat_to_agent_update propagates continuation_token.""" + from agent_framework._types import map_chat_to_agent_update + + token = {"response_id": "resp_map_123"} + chat_update = ChatResponseUpdate( + contents=[Content.from_text(text="chunk")], + role="assistant", + response_id="resp_map_123", + continuation_token=token, + ) + + agent_update = map_chat_to_agent_update(chat_update, agent_name="test-agent") + + assert agent_update.continuation_token is not None + assert agent_update.continuation_token["response_id"] == "resp_map_123" + + +async def test_prepare_options_excludes_continuation_token() -> None: + """Test that _prepare_options does not pass continuation_token to OpenAI API.""" + client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + + messages = [Message(role="user", contents=[Content.from_text(text="Hello")])] + options: dict[str, Any] = { + "model_id": "test-model", + "continuation_token": {"response_id": "resp_123"}, + "background": True, + } + + run_options = await client._prepare_options(messages, options) + + assert "continuation_token" not in run_options + assert "background" in run_options + assert run_options["background"] is True + + +# endregion diff --git a/python/packages/core/tests/workflow/test_agent_executor.py b/python/packages/core/tests/workflow/test_agent_executor.py index 841ef84b85..d3cef6f1fa 100644 --- a/python/packages/core/tests/workflow/test_agent_executor.py +++ b/python/packages/core/tests/workflow/test_agent_executor.py @@ -9,9 +9,9 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, ChatMessageStore, Content, + Message, ResponseStream, WorkflowRunState, ) @@ -29,7 +29,7 @@ class _CountingAgent(BaseAgent): def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -46,7 +46,7 @@ class _CountingAgent(BaseAgent): return ResponseStream(_stream(), finalizer=AgentResponse.from_updates) async def _run() -> AgentResponse: - return AgentResponse(messages=[ChatMessage("assistant", [f"Response #{self.call_count}: {self.name}"])]) + return AgentResponse(messages=[Message("assistant", [f"Response #{self.call_count}: {self.name}"])]) return _run() @@ -61,8 +61,8 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None: # Add some initial messages to the thread to verify thread state persistence initial_messages = [ - ChatMessage(role="user", text="Initial message 1"), - ChatMessage(role="assistant", text="Initial response 1"), + Message(role="user", text="Initial message 1"), + Message(role="assistant", text="Initial response 1"), ] await initial_thread.on_new_messages(initial_messages) @@ -165,9 +165,9 @@ async def test_agent_executor_save_and_restore_state_directly() -> None: # Add messages to thread thread_messages = [ - ChatMessage(role="user", text="Message in thread 1"), - ChatMessage(role="assistant", text="Thread response 1"), - ChatMessage(role="user", text="Message in thread 2"), + Message(role="user", text="Message in thread 1"), + Message(role="assistant", text="Thread response 1"), + Message(role="user", text="Message in thread 2"), ] await thread.on_new_messages(thread_messages) @@ -175,8 +175,8 @@ async def test_agent_executor_save_and_restore_state_directly() -> None: # Add messages to executor cache cache_messages = [ - ChatMessage(role="user", text="Cached user message"), - ChatMessage(role="assistant", text="Cached assistant response"), + Message(role="user", text="Cached user message"), + Message(role="assistant", text="Cached assistant response"), ] executor._cache = list(cache_messages) # type: ignore[reportPrivateUsage] diff --git a/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py b/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py index 2d4e3ecf39..3bb51d2224 100644 --- a/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py +++ b/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py @@ -8,18 +8,18 @@ from typing import Any from typing_extensions import Never from agent_framework import ( + Agent, AgentExecutor, AgentExecutorResponse, AgentResponse, AgentResponseUpdate, AgentThread, BaseAgent, - ChatAgent, - ChatMessage, ChatResponse, ChatResponseUpdate, Content, FunctionTool, + Message, ResponseStream, WorkflowBuilder, WorkflowContext, @@ -39,7 +39,7 @@ class _ToolCallingAgent(BaseAgent): def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -49,7 +49,7 @@ class _ToolCallingAgent(BaseAgent): return ResponseStream(self._run_stream_impl(), finalizer=AgentResponse.from_updates) async def _run() -> AgentResponse: - return AgentResponse(messages=[ChatMessage("assistant", ["done"])]) + return AgentResponse(messages=[Message("assistant", ["done"])]) return _run() @@ -156,7 +156,7 @@ class MockChatClient(FunctionInvocationLayer[Any], BaseChatClient[Any]): def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], stream: bool, options: Mapping[str, Any], **kwargs: Any, @@ -175,7 +175,7 @@ class MockChatClient(FunctionInvocationLayer[Any], BaseChatClient[Any]): if self._iteration == 0: if self._parallel_request: response = ChatResponse( - messages=ChatMessage( + messages=Message( "assistant", [ Content.from_function_call( @@ -189,7 +189,7 @@ class MockChatClient(FunctionInvocationLayer[Any], BaseChatClient[Any]): ) else: response = ChatResponse( - messages=ChatMessage( + messages=Message( "assistant", [ Content.from_function_call( @@ -199,7 +199,7 @@ class MockChatClient(FunctionInvocationLayer[Any], BaseChatClient[Any]): ) ) else: - response = ChatResponse(messages=ChatMessage("assistant", ["Tool executed successfully."])) + response = ChatResponse(messages=Message("assistant", ["Tool executed successfully."])) self._iteration += 1 return response @@ -243,8 +243,8 @@ async def test_executor(agent_executor_response: AgentExecutorResponse, ctx: Wor async def test_agent_executor_tool_call_with_approval() -> None: """Test that AgentExecutor handles tool calls requiring approval.""" # Arrange - agent = ChatAgent( - chat_client=MockChatClient(), + agent = Agent( + client=MockChatClient(), name="ApprovalAgent", tools=[mock_tool_requiring_approval], ) @@ -277,8 +277,8 @@ async def test_agent_executor_tool_call_with_approval() -> None: async def test_agent_executor_tool_call_with_approval_streaming() -> None: """Test that AgentExecutor handles tool calls requiring approval in streaming mode.""" # Arrange - agent = ChatAgent( - chat_client=MockChatClient(), + agent = Agent( + client=MockChatClient(), name="ApprovalAgent", tools=[mock_tool_requiring_approval], ) @@ -314,8 +314,8 @@ async def test_agent_executor_tool_call_with_approval_streaming() -> None: async def test_agent_executor_parallel_tool_call_with_approval() -> None: """Test that AgentExecutor handles parallel tool calls requiring approval.""" # Arrange - agent = ChatAgent( - chat_client=MockChatClient(parallel_request=True), + agent = Agent( + client=MockChatClient(parallel_request=True), name="ApprovalAgent", tools=[mock_tool_requiring_approval], ) @@ -350,8 +350,8 @@ async def test_agent_executor_parallel_tool_call_with_approval() -> None: async def test_agent_executor_parallel_tool_call_with_approval_streaming() -> None: """Test that AgentExecutor handles parallel tool calls requiring approval in streaming mode.""" # Arrange - agent = ChatAgent( - chat_client=MockChatClient(parallel_request=True), + agent = Agent( + client=MockChatClient(parallel_request=True), name="ApprovalAgent", tools=[mock_tool_requiring_approval], ) @@ -409,7 +409,7 @@ class DeclarationOnlyMockChatClient(FunctionInvocationLayer[Any], BaseChatClient def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], stream: bool, options: Mapping[str, Any], **kwargs: Any, @@ -426,7 +426,7 @@ class DeclarationOnlyMockChatClient(FunctionInvocationLayer[Any], BaseChatClient if self._iteration == 0: if self._parallel_request: response = ChatResponse( - messages=ChatMessage( + messages=Message( "assistant", [ Content.from_function_call( @@ -440,7 +440,7 @@ class DeclarationOnlyMockChatClient(FunctionInvocationLayer[Any], BaseChatClient ) else: response = ChatResponse( - messages=ChatMessage( + messages=Message( "assistant", [ Content.from_function_call( @@ -450,7 +450,7 @@ class DeclarationOnlyMockChatClient(FunctionInvocationLayer[Any], BaseChatClient ) ) else: - response = ChatResponse(messages=ChatMessage("assistant", ["Tool executed successfully."])) + response = ChatResponse(messages=Message("assistant", ["Tool executed successfully."])) self._iteration += 1 return response @@ -483,8 +483,8 @@ class DeclarationOnlyMockChatClient(FunctionInvocationLayer[Any], BaseChatClient async def test_agent_executor_declaration_only_tool_emits_request_info() -> None: """Test that AgentExecutor emits request_info when agent calls a declaration-only tool.""" - agent = ChatAgent( - chat_client=DeclarationOnlyMockChatClient(), + agent = Agent( + client=DeclarationOnlyMockChatClient(), name="DeclarationOnlyAgent", tools=[declaration_only_tool], ) @@ -519,8 +519,8 @@ async def test_agent_executor_declaration_only_tool_emits_request_info() -> None async def test_agent_executor_declaration_only_tool_emits_request_info_streaming() -> None: """Test that AgentExecutor emits request_info for declaration-only tools in streaming mode.""" - agent = ChatAgent( - chat_client=DeclarationOnlyMockChatClient(), + agent = Agent( + client=DeclarationOnlyMockChatClient(), name="DeclarationOnlyAgent", tools=[declaration_only_tool], ) @@ -558,8 +558,8 @@ async def test_agent_executor_declaration_only_tool_emits_request_info_streaming async def test_agent_executor_parallel_declaration_only_tool_emits_request_info() -> None: """Test that AgentExecutor emits request_info for parallel declaration-only tool calls.""" - agent = ChatAgent( - chat_client=DeclarationOnlyMockChatClient(parallel_request=True), + agent = Agent( + client=DeclarationOnlyMockChatClient(parallel_request=True), name="DeclarationOnlyAgent", tools=[declaration_only_tool], ) diff --git a/python/packages/core/tests/workflow/test_agent_run_event_typing.py b/python/packages/core/tests/workflow/test_agent_run_event_typing.py index 410f57f962..ff8ef99893 100644 --- a/python/packages/core/tests/workflow/test_agent_run_event_typing.py +++ b/python/packages/core/tests/workflow/test_agent_run_event_typing.py @@ -2,13 +2,13 @@ """Tests for WorkflowEvent[T] generic type annotations.""" -from agent_framework import AgentResponse, AgentResponseUpdate, ChatMessage +from agent_framework import AgentResponse, AgentResponseUpdate, Message from agent_framework._workflows._events import WorkflowEvent def test_workflow_event_with_agent_response_data_type() -> None: """Verify WorkflowEvent[AgentResponse].data is typed as AgentResponse.""" - response = AgentResponse(messages=[ChatMessage(role="assistant", text="Hello")]) + response = AgentResponse(messages=[Message(role="assistant", text="Hello")]) event: WorkflowEvent[AgentResponse] = WorkflowEvent.emit(executor_id="test", data=response) # This assignment should pass type checking without a cast @@ -29,7 +29,7 @@ def test_workflow_event_with_agent_response_update_data_type() -> None: def test_workflow_event_repr() -> None: """Verify WorkflowEvent.__repr__ uses consistent format.""" - response = AgentResponse(messages=[ChatMessage(role="assistant", text="Hello")]) + response = AgentResponse(messages=[Message(role="assistant", text="Hello")]) event: WorkflowEvent[AgentResponse] = WorkflowEvent.emit(executor_id="test", data=response) repr_str = repr(event) diff --git a/python/packages/core/tests/workflow/test_agent_utils.py b/python/packages/core/tests/workflow/test_agent_utils.py index c26ecda04c..8a8beae5b1 100644 --- a/python/packages/core/tests/workflow/test_agent_utils.py +++ b/python/packages/core/tests/workflow/test_agent_utils.py @@ -3,7 +3,7 @@ from collections.abc import AsyncIterable from typing import Any -from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, ChatMessage +from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, Message from agent_framework._workflows._agent_utils import resolve_agent_id @@ -34,7 +34,7 @@ class MockAgent: def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, diff --git a/python/packages/core/tests/workflow/test_edge.py b/python/packages/core/tests/workflow/test_edge.py index 42ff6e5d36..f63cf9b45b 100644 --- a/python/packages/core/tests/workflow/test_edge.py +++ b/python/packages/core/tests/workflow/test_edge.py @@ -9,8 +9,8 @@ import pytest from agent_framework import ( Executor, InProcRunnerContext, - Message, WorkflowContext, + WorkflowMessage, handler, ) from agent_framework._workflows._edge import ( @@ -193,7 +193,7 @@ async def test_single_edge_group_send_message() -> None: ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) success = await edge_runner.send_message(message, state, ctx) assert success is True @@ -212,7 +212,7 @@ async def test_single_edge_group_send_message_with_target() -> None: ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id, target_id=target.id) + message = WorkflowMessage(data=data, source_id=source.id, target_id=target.id) success = await edge_runner.send_message(message, state, ctx) assert success is True @@ -231,7 +231,7 @@ async def test_single_edge_group_send_message_with_invalid_target() -> None: ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id, target_id="invalid_target") + message = WorkflowMessage(data=data, source_id=source.id, target_id="invalid_target") success = await edge_runner.send_message(message, state, ctx) assert success is False @@ -250,7 +250,7 @@ async def test_single_edge_group_send_message_with_invalid_data() -> None: ctx = InProcRunnerContext() data = "invalid_data" - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) success = await edge_runner.send_message(message, state, ctx) assert success is False @@ -270,7 +270,7 @@ async def test_single_edge_group_send_message_with_condition_pass() -> None: ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) success = await edge_runner.send_message(message, state, ctx) assert success is True @@ -292,7 +292,7 @@ async def test_single_edge_group_send_message_with_condition_fail() -> None: ctx = InProcRunnerContext() data = MockMessage(data="different") - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) success = await edge_runner.send_message(message, state, ctx) # Should return True because message was processed, but condition failed @@ -318,7 +318,9 @@ async def test_single_edge_group_tracing_success(span_exporter) -> None: source_span_ids = ["00f067aa0ba902b7"] data = MockMessage(data="test") - message = Message(data=data, source_id=source.id, trace_contexts=trace_contexts, source_span_ids=source_span_ids) + message = WorkflowMessage( + data=data, source_id=source.id, trace_contexts=trace_contexts, source_span_ids=source_span_ids + ) # Clear any build spans span_exporter.clear() @@ -363,7 +365,7 @@ async def test_single_edge_group_tracing_condition_failure(span_exporter) -> Non ctx = InProcRunnerContext() data = MockMessage(data="fail") - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) # Clear any build spans span_exporter.clear() @@ -398,7 +400,7 @@ async def test_single_edge_group_tracing_type_mismatch(span_exporter) -> None: # Send incompatible data type data = "invalid_data" - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) # Clear any build spans span_exporter.clear() @@ -432,7 +434,7 @@ async def test_single_edge_group_tracing_target_mismatch(span_exporter) -> None: ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id, target_id="wrong_target") + message = WorkflowMessage(data=data, source_id=source.id, target_id="wrong_target") # Clear any build spans span_exporter.clear() @@ -500,7 +502,7 @@ async def test_source_edge_group_send_message() -> None: ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) success = await edge_runner.send_message(message, state, ctx) @@ -523,7 +525,7 @@ async def test_source_edge_group_send_message_with_target() -> None: ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id, target_id=target1.id) + message = WorkflowMessage(data=data, source_id=source.id, target_id=target1.id) success = await edge_runner.send_message(message, state, ctx) @@ -546,7 +548,7 @@ async def test_source_edge_group_send_message_with_invalid_target() -> None: ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id, target_id="invalid_target") + message = WorkflowMessage(data=data, source_id=source.id, target_id="invalid_target") success = await edge_runner.send_message(message, state, ctx) assert success is False @@ -566,7 +568,7 @@ async def test_source_edge_group_send_message_with_invalid_data() -> None: ctx = InProcRunnerContext() data = "invalid_data" - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) success = await edge_runner.send_message(message, state, ctx) assert success is False @@ -586,7 +588,7 @@ async def test_source_edge_group_send_message_only_one_successful_send() -> None ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) success = await edge_runner.send_message(message, state, ctx) @@ -635,7 +637,7 @@ async def test_source_edge_group_with_selection_func_send_message() -> None: ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) with patch("agent_framework._workflows._edge_runner.EdgeRunner._execute_on_target") as mock_send: success = await edge_runner.send_message(message, state, ctx) @@ -663,7 +665,7 @@ async def test_source_edge_group_with_selection_func_send_message_with_invalid_s ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) with pytest.raises(RuntimeError): await edge_runner.send_message(message, state, ctx) @@ -688,7 +690,7 @@ async def test_source_edge_group_with_selection_func_send_message_with_target() ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id, target_id=target1.id) + message = WorkflowMessage(data=data, source_id=source.id, target_id=target1.id) with patch("agent_framework._workflows._edge_runner.EdgeRunner._execute_on_target") as mock_send: success = await edge_runner.send_message(message, state, ctx) @@ -717,7 +719,7 @@ async def test_source_edge_group_with_selection_func_send_message_with_target_no ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source.id, target_id=target2.id) + message = WorkflowMessage(data=data, source_id=source.id, target_id=target2.id) success = await edge_runner.send_message(message, state, ctx) assert success is False @@ -742,7 +744,7 @@ async def test_source_edge_group_with_selection_func_send_message_with_invalid_d ctx = InProcRunnerContext() data = "invalid_data" - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) success = await edge_runner.send_message(message, state, ctx) assert success is False @@ -767,7 +769,7 @@ async def test_source_edge_group_with_selection_func_send_message_with_target_in ctx = InProcRunnerContext() data = "invalid_data" - message = Message(data=data, source_id=source.id, target_id=target1.id) + message = WorkflowMessage(data=data, source_id=source.id, target_id=target1.id) success = await edge_runner.send_message(message, state, ctx) assert success is False @@ -791,7 +793,9 @@ async def test_fan_out_edge_group_tracing_success(span_exporter) -> None: source_span_ids = ["00f067aa0ba902b7"] data = MockMessage(data="test") - message = Message(data=data, source_id=source.id, trace_contexts=trace_contexts, source_span_ids=source_span_ids) + message = WorkflowMessage( + data=data, source_id=source.id, trace_contexts=trace_contexts, source_span_ids=source_span_ids + ) # Clear any build spans span_exporter.clear() @@ -841,7 +845,7 @@ async def test_fan_out_edge_group_tracing_with_target(span_exporter) -> None: source_span_ids = ["00f067aa0ba902b7"] data = MockMessage(data="test") - message = Message( + message = WorkflowMessage( data=data, source_id=source.id, target_id=target1.id, @@ -927,7 +931,7 @@ async def test_target_edge_group_send_message_buffer() -> None: with patch("agent_framework._workflows._edge_runner.EdgeRunner._execute_on_target") as mock_send: success = await edge_runner.send_message( - Message(data=data, source_id=source1.id), + WorkflowMessage(data=data, source_id=source1.id), state, ctx, ) @@ -937,7 +941,7 @@ async def test_target_edge_group_send_message_buffer() -> None: assert len(edge_runner._buffer[source1.id]) == 1 # type: ignore success = await edge_runner.send_message( - Message(data=data, source_id=source2.id), + WorkflowMessage(data=data, source_id=source2.id), state, ctx, ) @@ -963,7 +967,7 @@ async def test_target_edge_group_send_message_with_invalid_target() -> None: ctx = InProcRunnerContext() data = MockMessage(data="test") - message = Message(data=data, source_id=source1.id, target_id="invalid_target") + message = WorkflowMessage(data=data, source_id=source1.id, target_id="invalid_target") success = await edge_runner.send_message(message, state, ctx) assert success is False @@ -984,7 +988,7 @@ async def test_target_edge_group_send_message_with_invalid_data() -> None: ctx = InProcRunnerContext() data = "invalid_data" - message = Message(data=data, source_id=source1.id) + message = WorkflowMessage(data=data, source_id=source1.id) success = await edge_runner.send_message(message, state, ctx) assert success is False @@ -1017,7 +1021,9 @@ async def test_fan_in_edge_group_tracing_buffered(span_exporter) -> None: # Send first message (should be buffered) success = await edge_runner.send_message( - Message(data=data, source_id=source1.id, trace_contexts=trace_contexts1, source_span_ids=source_span_ids1), + WorkflowMessage( + data=data, source_id=source1.id, trace_contexts=trace_contexts1, source_span_ids=source_span_ids1 + ), state, ctx, ) @@ -1049,7 +1055,9 @@ async def test_fan_in_edge_group_tracing_buffered(span_exporter) -> None: span_exporter.clear() success = await edge_runner.send_message( - Message(data=data, source_id=source2.id, trace_contexts=trace_contexts2, source_span_ids=source_span_ids2), + WorkflowMessage( + data=data, source_id=source2.id, trace_contexts=trace_contexts2, source_span_ids=source_span_ids2 + ), state, ctx, ) @@ -1093,7 +1101,7 @@ async def test_fan_in_edge_group_tracing_type_mismatch(span_exporter) -> None: # Send incompatible data type data = "invalid_data" - message = Message(data=data, source_id=source1.id) + message = WorkflowMessage(data=data, source_id=source1.id) # Clear any build spans span_exporter.clear() @@ -1130,7 +1138,7 @@ async def test_fan_in_edge_group_with_multiple_message_types() -> None: data = MockMessage(data="test") success = await edge_runner.send_message( - Message(data=data, source_id=source1.id), + WorkflowMessage(data=data, source_id=source1.id), state, ctx, ) @@ -1138,7 +1146,7 @@ async def test_fan_in_edge_group_with_multiple_message_types() -> None: data2 = MockMessageSecondary(data="test") success = await edge_runner.send_message( - Message(data=data2, source_id=source2.id), + WorkflowMessage(data=data2, source_id=source2.id), state, ctx, ) @@ -1161,7 +1169,7 @@ async def test_fan_in_edge_group_with_multiple_message_types_failed() -> None: data = MockMessage(data="test") success = await edge_runner.send_message( - Message(data=data, source_id=source1.id), + WorkflowMessage(data=data, source_id=source1.id), state, ctx, ) @@ -1175,7 +1183,7 @@ async def test_fan_in_edge_group_with_multiple_message_types_failed() -> None: # source executors as a union. data2 = MockMessageSecondary(data="test") _ = await edge_runner.send_message( - Message(data=data2, source_id=source2.id), + WorkflowMessage(data=data2, source_id=source2.id), state, ctx, ) @@ -1275,7 +1283,7 @@ async def test_switch_case_edge_group_send_message() -> None: ctx = InProcRunnerContext() data = MockMessage(data=-1) - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) with patch("agent_framework._workflows._edge_runner.EdgeRunner._execute_on_target") as mock_send: success = await edge_runner.send_message(message, state, ctx) @@ -1285,7 +1293,7 @@ async def test_switch_case_edge_group_send_message() -> None: # Default condition should data = MockMessage(data=1) - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) with patch("agent_framework._workflows._edge_runner.EdgeRunner._execute_on_target") as mock_send: success = await edge_runner.send_message(message, state, ctx) @@ -1314,7 +1322,7 @@ async def test_switch_case_edge_group_send_message_with_invalid_target() -> None ctx = InProcRunnerContext() data = MockMessage(data=-1) - message = Message(data=data, source_id=source.id, target_id="invalid_target") + message = WorkflowMessage(data=data, source_id=source.id, target_id="invalid_target") success = await edge_runner.send_message(message, state, ctx) assert success is False @@ -1341,13 +1349,13 @@ async def test_switch_case_edge_group_send_message_with_valid_target() -> None: ctx = InProcRunnerContext() data = MockMessage(data=1) # Condition will fail - message = Message(data=data, source_id=source.id, target_id=target1.id) + message = WorkflowMessage(data=data, source_id=source.id, target_id=target1.id) success = await edge_runner.send_message(message, state, ctx) assert success is False data = MockMessage(data=-1) # Condition will pass - message = Message(data=data, source_id=source.id, target_id=target1.id) + message = WorkflowMessage(data=data, source_id=source.id, target_id=target1.id) success = await edge_runner.send_message(message, state, ctx) assert success is True @@ -1373,7 +1381,7 @@ async def test_switch_case_edge_group_send_message_with_invalid_data() -> None: ctx = InProcRunnerContext() data = "invalid_data" - message = Message(data=data, source_id=source.id) + message = WorkflowMessage(data=data, source_id=source.id) success = await edge_runner.send_message(message, state, ctx) assert success is False diff --git a/python/packages/core/tests/workflow/test_executor.py b/python/packages/core/tests/workflow/test_executor.py index 507b798e96..06d027f19d 100644 --- a/python/packages/core/tests/workflow/test_executor.py +++ b/python/packages/core/tests/workflow/test_executor.py @@ -6,12 +6,12 @@ import pytest from typing_extensions import Never from agent_framework import ( - ChatMessage, Executor, Message, WorkflowBuilder, WorkflowContext, WorkflowEvent, + WorkflowMessage, executor, handler, response_handler, @@ -98,9 +98,9 @@ def test_executor_with_valid_handlers(): executor = MockExecutorWithValidHandlers(id="test") assert executor.id is not None assert len(executor._handlers) == 2 # type: ignore - assert executor.can_handle(Message(data="text", source_id="mock")) is True - assert executor.can_handle(Message(data=42, source_id="mock")) is True - assert executor.can_handle(Message(data=3.14, source_id="mock")) is False + assert executor.can_handle(WorkflowMessage(data="text", source_id="mock")) is True + assert executor.can_handle(WorkflowMessage(data=42, source_id="mock")) is True + assert executor.can_handle(WorkflowMessage(data=3.14, source_id="mock")) is False def test_executor_handlers_with_output_types(): @@ -531,10 +531,10 @@ async def test_executor_invoked_event_data_not_mutated_by_handler(): """Test that executor_invoked event (type='executor_invoked').data captures original input, not mutated input.""" @executor(id="Mutator") - async def mutator(messages: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None: + async def mutator(messages: list[Message], ctx: WorkflowContext[list[Message]]) -> None: # The handler mutates the input list by appending new messages original_len = len(messages) - messages.append(ChatMessage(role="assistant", text="Added by executor")) + messages.append(Message(role="assistant", text="Added by executor")) await ctx.send_message(messages) # Verify mutation happened assert len(messages) == original_len + 1 @@ -542,7 +542,7 @@ async def test_executor_invoked_event_data_not_mutated_by_handler(): workflow = WorkflowBuilder(start_executor=mutator).build() # Run with a single user message - input_messages = [ChatMessage(role="user", text="hello")] + input_messages = [Message(role="user", text="hello")] events = await workflow.run(input_messages) # Find the invoked event for the Mutator executor @@ -581,9 +581,9 @@ class TestHandlerExplicitTypes: assert len(exec_instance._handlers) == 1 # Can handle str messages - assert exec_instance.can_handle(Message(data="hello", source_id="mock")) + assert exec_instance.can_handle(WorkflowMessage(data="hello", source_id="mock")) # Cannot handle int messages (since explicit type is str) - assert not exec_instance.can_handle(Message(data=42, source_id="mock")) + assert not exec_instance.can_handle(WorkflowMessage(data=42, source_id="mock")) def test_handler_with_explicit_output_type(self): """Test that explicit output works when input is also specified.""" @@ -623,8 +623,8 @@ class TestHandlerExplicitTypes: assert handler_func._handler_spec["output_types"] == [list] # Verify can_handle - assert exec_instance.can_handle(Message(data={"key": "value"}, source_id="mock")) - assert not exec_instance.can_handle(Message(data="string", source_id="mock")) + assert exec_instance.can_handle(WorkflowMessage(data={"key": "value"}, source_id="mock")) + assert not exec_instance.can_handle(WorkflowMessage(data="string", source_id="mock")) def test_handler_with_explicit_union_input_type(self): """Test that explicit union input_type is handled correctly.""" @@ -642,10 +642,10 @@ class TestHandlerExplicitTypes: assert len(exec_instance._handlers) == 1 # Can handle both str and int messages - assert exec_instance.can_handle(Message(data="hello", source_id="mock")) - assert exec_instance.can_handle(Message(data=42, source_id="mock")) + assert exec_instance.can_handle(WorkflowMessage(data="hello", source_id="mock")) + assert exec_instance.can_handle(WorkflowMessage(data=42, source_id="mock")) # Cannot handle float - assert not exec_instance.can_handle(Message(data=3.14, source_id="mock")) + assert not exec_instance.can_handle(WorkflowMessage(data=3.14, source_id="mock")) def test_handler_with_explicit_union_output_type(self): """Test that explicit union output is normalized to a list.""" @@ -736,7 +736,7 @@ class TestHandlerExplicitTypes: # Should work with explicit input_type assert str in exec_instance._handlers - assert exec_instance.can_handle(Message(data="hello", source_id="mock")) + assert exec_instance.can_handle(WorkflowMessage(data="hello", source_id="mock")) def test_handler_multiple_handlers_mixed_explicit_and_introspected(self): """Test executor with multiple handlers, some with explicit types and some introspected.""" @@ -773,7 +773,7 @@ class TestHandlerExplicitTypes: # Should resolve the string to the actual type assert ForwardRefMessage in exec_instance._handlers - assert exec_instance.can_handle(Message(data=ForwardRefMessage("hello"), source_id="mock")) + assert exec_instance.can_handle(WorkflowMessage(data=ForwardRefMessage("hello"), source_id="mock")) def test_handler_with_string_forward_reference_union(self): """Test that string forward references work with union types.""" @@ -786,8 +786,8 @@ class TestHandlerExplicitTypes: exec_instance = StringUnionExecutor(id="string_union") # Should handle both types - assert exec_instance.can_handle(Message(data=ForwardRefTypeA("hello"), source_id="mock")) - assert exec_instance.can_handle(Message(data=ForwardRefTypeB(42), source_id="mock")) + assert exec_instance.can_handle(WorkflowMessage(data=ForwardRefTypeA("hello"), source_id="mock")) + assert exec_instance.can_handle(WorkflowMessage(data=ForwardRefTypeB(42), source_id="mock")) def test_handler_with_string_forward_reference_output_type(self): """Test that string forward references work for output_type.""" @@ -851,7 +851,7 @@ class TestHandlerExplicitTypes: # Check input type assert str in exec_instance._handlers - assert exec_instance.can_handle(Message(data="hello", source_id="mock")) + assert exec_instance.can_handle(WorkflowMessage(data="hello", source_id="mock")) # Check output_type assert int in exec_instance.output_types diff --git a/python/packages/core/tests/workflow/test_full_conversation.py b/python/packages/core/tests/workflow/test_full_conversation.py index c29dd61fe5..3eb47803fc 100644 --- a/python/packages/core/tests/workflow/test_full_conversation.py +++ b/python/packages/core/tests/workflow/test_full_conversation.py @@ -13,9 +13,9 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, Executor, + Message, ResponseStream, WorkflowBuilder, WorkflowContext, @@ -34,7 +34,7 @@ class _SimpleAgent(BaseAgent): def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -48,7 +48,7 @@ class _SimpleAgent(BaseAgent): return ResponseStream(_stream(), finalizer=AgentResponse.from_updates) async def _run() -> AgentResponse: - return AgentResponse(messages=[ChatMessage("assistant", [self._reply_text])]) + return AgentResponse(messages=[Message("assistant", [self._reply_text])]) return _run() @@ -96,7 +96,7 @@ async def test_agent_executor_populates_full_conversation_non_streaming() -> Non class _CaptureAgent(BaseAgent): """Streaming-capable agent that records the messages it received.""" - _last_messages: list[ChatMessage] = PrivateAttr(default_factory=list) # type: ignore + _last_messages: list[Message] = PrivateAttr(default_factory=list) # type: ignore def __init__(self, *, reply_text: str, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -104,20 +104,20 @@ class _CaptureAgent(BaseAgent): def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, **kwargs: Any, ) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]: # Normalize and record messages for verification - norm: list[ChatMessage] = [] + norm: list[Message] = [] if messages: for m in messages: # type: ignore[iteration-over-optional] - if isinstance(m, ChatMessage): + if isinstance(m, Message): norm.append(m) elif isinstance(m, str): - norm.append(ChatMessage("user", [m])) + norm.append(Message("user", [m])) self._last_messages = norm if stream: @@ -128,7 +128,7 @@ class _CaptureAgent(BaseAgent): return ResponseStream(_stream(), finalizer=AgentResponse.from_updates) async def _run() -> AgentResponse: - return AgentResponse(messages=[ChatMessage("assistant", [self._reply_text])]) + return AgentResponse(messages=[Message("assistant", [self._reply_text])]) return _run() diff --git a/python/packages/core/tests/workflow/test_function_executor.py b/python/packages/core/tests/workflow/test_function_executor.py index 3d274f8cd7..c0b73156ff 100644 --- a/python/packages/core/tests/workflow/test_function_executor.py +++ b/python/packages/core/tests/workflow/test_function_executor.py @@ -8,9 +8,9 @@ from typing_extensions import Never from agent_framework import ( FunctionExecutor, - Message, WorkflowBuilder, WorkflowContext, + WorkflowMessage, executor, ) @@ -253,9 +253,9 @@ class TestFunctionExecutor: async def string_processor(text: str, ctx: WorkflowContext[str]) -> None: await ctx.send_message(text) - assert string_processor.can_handle(Message(data="hello", source_id="Mock")) - assert not string_processor.can_handle(Message(data=123, source_id="Mock")) - assert not string_processor.can_handle(Message(data=[], source_id="Mock")) + assert string_processor.can_handle(WorkflowMessage(data="hello", source_id="Mock")) + assert not string_processor.can_handle(WorkflowMessage(data=123, source_id="Mock")) + assert not string_processor.can_handle(WorkflowMessage(data=[], source_id="Mock")) def test_duplicate_handler_registration(self): """Test that registering duplicate handlers raises an error.""" @@ -332,9 +332,9 @@ class TestFunctionExecutor: async def int_processor(value: int): return value * 2 - assert int_processor.can_handle(Message(data=42, source_id="mock")) - assert not int_processor.can_handle(Message(data="hello", source_id="mock")) - assert not int_processor.can_handle(Message(data=[], source_id="mock")) + assert int_processor.can_handle(WorkflowMessage(data=42, source_id="mock")) + assert not int_processor.can_handle(WorkflowMessage(data="hello", source_id="mock")) + assert not int_processor.can_handle(WorkflowMessage(data=[], source_id="mock")) async def test_single_parameter_execution(self): """Test that single-parameter functions can be executed properly.""" @@ -348,7 +348,7 @@ class TestFunctionExecutor: WorkflowBuilder(start_executor=double_value).build() # For testing purposes, we can check that the handler is registered correctly - assert double_value.can_handle(Message(data=5, source_id="mock")) + assert double_value.can_handle(WorkflowMessage(data=5, source_id="mock")) assert int in double_value._handlers def test_sync_function_basic(self): @@ -392,9 +392,9 @@ class TestFunctionExecutor: def string_handler(text: str): return text.strip() - assert string_handler.can_handle(Message(data="hello", source_id="mock")) - assert not string_handler.can_handle(Message(data=123, source_id="mock")) - assert not string_handler.can_handle(Message(data=[], source_id="mock")) + assert string_handler.can_handle(WorkflowMessage(data="hello", source_id="mock")) + assert not string_handler.can_handle(WorkflowMessage(data=123, source_id="mock")) + assert not string_handler.can_handle(WorkflowMessage(data=[], source_id="mock")) def test_sync_function_validation(self): """Test validation for synchronous functions.""" @@ -436,8 +436,8 @@ class TestFunctionExecutor: assert isinstance(async_func, FunctionExecutor) # Both should handle strings - assert sync_func.can_handle(Message(data="test", source_id="mock")) - assert async_func.can_handle(Message(data="test", source_id="mock")) + assert sync_func.can_handle(WorkflowMessage(data="test", source_id="mock")) + assert async_func.can_handle(WorkflowMessage(data="test", source_id="mock")) # Both should be different instances assert sync_func is not async_func @@ -466,8 +466,8 @@ class TestFunctionExecutor: assert async_spec["workflow_output_types"] == [str] # Second parameter is str # Verify the executors can handle their input types - assert to_upper_sync.can_handle(Message(data="hello", source_id="mock")) - assert reverse_async.can_handle(Message(data="HELLO", source_id="mock")) + assert to_upper_sync.can_handle(WorkflowMessage(data="hello", source_id="mock")) + assert reverse_async.can_handle(WorkflowMessage(data="HELLO", source_id="mock")) # For integration testing, we mainly verify that the handlers are properly registered # and the functions are wrapped correctly @@ -574,9 +574,9 @@ class TestExecutorExplicitTypes: assert len(process._handlers) == 1 # Can handle str messages - assert process.can_handle(Message(data="hello", source_id="mock")) + assert process.can_handle(WorkflowMessage(data="hello", source_id="mock")) # Cannot handle int messages - assert not process.can_handle(Message(data=42, source_id="mock")) + assert not process.can_handle(WorkflowMessage(data=42, source_id="mock")) def test_executor_with_explicit_output_type(self): """Test that explicit output_type takes precedence over introspection.""" @@ -609,8 +609,8 @@ class TestExecutorExplicitTypes: assert spec["output_types"] == [list] # Verify can_handle - assert process.can_handle(Message(data={"key": "value"}, source_id="mock")) - assert not process.can_handle(Message(data="string", source_id="mock")) + assert process.can_handle(WorkflowMessage(data={"key": "value"}, source_id="mock")) + assert not process.can_handle(WorkflowMessage(data="string", source_id="mock")) def test_executor_with_explicit_union_input_type(self): """Test that explicit union input_type is handled correctly.""" @@ -623,10 +623,10 @@ class TestExecutorExplicitTypes: assert len(process._handlers) == 1 # Can handle both str and int messages - assert process.can_handle(Message(data="hello", source_id="mock")) - assert process.can_handle(Message(data=42, source_id="mock")) + assert process.can_handle(WorkflowMessage(data="hello", source_id="mock")) + assert process.can_handle(WorkflowMessage(data=42, source_id="mock")) # Cannot handle float - assert not process.can_handle(Message(data=3.14, source_id="mock")) + assert not process.can_handle(WorkflowMessage(data=3.14, source_id="mock")) def test_executor_with_explicit_union_output_type(self): """Test that explicit union output_type is normalized to a list.""" @@ -695,7 +695,7 @@ class TestExecutorExplicitTypes: # Should work with explicit input_type assert str in process._handlers - assert process.can_handle(Message(data="hello", source_id="mock")) + assert process.can_handle(WorkflowMessage(data="hello", source_id="mock")) def test_executor_explicit_types_with_id(self): """Test that explicit types work together with id parameter.""" @@ -717,8 +717,8 @@ class TestExecutorExplicitTypes: # Should work with explicit input_type assert str in process._handlers - assert process.can_handle(Message(data="hello", source_id="mock")) - assert not process.can_handle(Message(data=42, source_id="mock")) + assert process.can_handle(WorkflowMessage(data="hello", source_id="mock")) + assert not process.can_handle(WorkflowMessage(data=42, source_id="mock")) def test_executor_explicit_types_with_sync_function(self): """Test that explicit types work with synchronous functions.""" @@ -752,8 +752,8 @@ class TestExecutorExplicitTypes: pass # Can handle both str and int - assert process.can_handle(Message(data="hello", source_id="mock")) - assert process.can_handle(Message(data=42, source_id="mock")) + assert process.can_handle(WorkflowMessage(data="hello", source_id="mock")) + assert process.can_handle(WorkflowMessage(data=42, source_id="mock")) # Output types should include both assert set(process.output_types) == {bool, float} @@ -767,7 +767,7 @@ class TestExecutorExplicitTypes: # Should resolve the string to the actual type assert FuncExecForwardRefMessage in process._handlers - assert process.can_handle(Message(data=FuncExecForwardRefMessage("hello"), source_id="mock")) + assert process.can_handle(WorkflowMessage(data=FuncExecForwardRefMessage("hello"), source_id="mock")) def test_executor_with_string_forward_reference_union(self): """Test that string forward references work with union types.""" @@ -777,8 +777,8 @@ class TestExecutorExplicitTypes: pass # Should handle both types - assert process.can_handle(Message(data=FuncExecForwardRefTypeA("hello"), source_id="mock")) - assert process.can_handle(Message(data=FuncExecForwardRefTypeB(42), source_id="mock")) + assert process.can_handle(WorkflowMessage(data=FuncExecForwardRefTypeA("hello"), source_id="mock")) + assert process.can_handle(WorkflowMessage(data=FuncExecForwardRefTypeB(42), source_id="mock")) def test_executor_with_string_forward_reference_output_type(self): """Test that string forward references work for output_type.""" @@ -827,7 +827,7 @@ class TestExecutorExplicitTypes: # Check input type assert str in process._handlers - assert process.can_handle(Message(data="hello", source_id="mock")) + assert process.can_handle(WorkflowMessage(data="hello", source_id="mock")) # Check output_type assert int in process.output_types diff --git a/python/packages/core/tests/workflow/test_runner.py b/python/packages/core/tests/workflow/test_runner.py index 7af722e45a..e527ba13fa 100644 --- a/python/packages/core/tests/workflow/test_runner.py +++ b/python/packages/core/tests/workflow/test_runner.py @@ -20,8 +20,8 @@ from agent_framework._workflows._edge import SingleEdgeGroup from agent_framework._workflows._runner import Runner from agent_framework._workflows._runner_context import ( InProcRunnerContext, - Message, RunnerContext, + WorkflowMessage, ) from agent_framework._workflows._state import State @@ -179,7 +179,7 @@ async def test_runner_emits_runner_completion_for_agent_response_without_targets runner = Runner([], {}, State(), ctx) await ctx.send_message( - Message( + WorkflowMessage( data=AgentExecutorResponse("agent", AgentResponse()), source_id="agent", ) diff --git a/python/packages/core/tests/workflow/test_typing_utils.py b/python/packages/core/tests/workflow/test_typing_utils.py index ab483e05e9..4dc8d8c917 100644 --- a/python/packages/core/tests/workflow/test_typing_utils.py +++ b/python/packages/core/tests/workflow/test_typing_utils.py @@ -378,12 +378,12 @@ def test_type_compatibility_collections() -> None: # List compatibility - key use case @dataclass - class ChatMessage: + class Message: text: str - assert is_type_compatible(list[ChatMessage], list[Union[str, ChatMessage]]) - assert is_type_compatible(list[str], list[Union[str, ChatMessage]]) - assert not is_type_compatible(list[Union[str, ChatMessage]], list[ChatMessage]) + assert is_type_compatible(list[Message], list[Union[str, Message]]) + assert is_type_compatible(list[str], list[Union[str, Message]]) + assert not is_type_compatible(list[Union[str, Message]], list[Message]) # Dict compatibility assert is_type_compatible(dict[str, int], dict[str, Union[int, float]]) diff --git a/python/packages/core/tests/workflow/test_workflow.py b/python/packages/core/tests/workflow/test_workflow.py index 1e98ff08c5..6728bcfcb1 100644 --- a/python/packages/core/tests/workflow/test_workflow.py +++ b/python/packages/core/tests/workflow/test_workflow.py @@ -15,7 +15,6 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, Executor, FileCheckpointStorage, @@ -26,6 +25,7 @@ from agent_framework import ( WorkflowContext, WorkflowConvergenceException, WorkflowEvent, + WorkflowMessage, WorkflowRunState, handler, response_handler, @@ -275,7 +275,7 @@ async def test_workflow_with_checkpointing_enabled(simple_executor: Executor): ) # Verify workflow was created and can run - test_message = Message(data="test message", source_id="test", target_id=None) + test_message = WorkflowMessage(data="test message", source_id="test", target_id=None) result = await workflow.run(test_message) assert result is not None @@ -536,7 +536,7 @@ async def test_workflow_checkpoint_runtime_only_configuration( workflow = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build() # Run with runtime checkpoint storage - should create checkpoints - test_message = Message(data="runtime checkpoint test", source_id="test", target_id=None) + test_message = WorkflowMessage(data="runtime checkpoint test", source_id="test", target_id=None) result = await workflow.run(test_message, checkpoint_storage=storage) assert result is not None assert result.get_final_state() == WorkflowRunState.IDLE @@ -587,7 +587,7 @@ async def test_workflow_checkpoint_runtime_overrides_buildtime( ) # Run with runtime checkpoint storage override - test_message = Message(data="override test", source_id="test", target_id=None) + test_message = WorkflowMessage(data="override test", source_id="test", target_id=None) result = await workflow.run(test_message, checkpoint_storage=runtime_storage) assert result is not None @@ -833,7 +833,7 @@ class _StreamingTestAgent(BaseAgent): def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -849,7 +849,7 @@ class _StreamingTestAgent(BaseAgent): return ResponseStream(_stream(), finalizer=AgentResponse.from_updates) async def _run() -> AgentResponse: - return AgentResponse(messages=[ChatMessage("assistant", [self._reply_text])]) + return AgentResponse(messages=[Message("assistant", [self._reply_text])]) return _run() @@ -911,7 +911,7 @@ async def test_workflow_run_parameter_validation(simple_executor: Executor) -> N """Test that stream properly validate parameter combinations.""" workflow = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build() - test_message = Message(data="test", source_id="test", target_id=None) + test_message = WorkflowMessage(data="test", source_id="test", target_id=None) # Valid: message only (new run) result = await workflow.run(test_message) @@ -942,7 +942,7 @@ async def test_workflow_run_stream_parameter_validation( """Test stream=True specific parameter validation scenarios.""" workflow = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build() - test_message = Message(data="test", source_id="test", target_id=None) + test_message = WorkflowMessage(data="test", source_id="test", target_id=None) # Valid: message only (new run) events: list[WorkflowEvent] = [] diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index c121f369fa..1ccc400f92 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -12,10 +12,10 @@ from agent_framework import ( AgentResponse, AgentResponseUpdate, AgentThread, - ChatMessage, ChatMessageStore, Content, Executor, + Message, ResponseStream, SupportsAgentRun, UsageDetails, @@ -39,14 +39,14 @@ class SimpleExecutor(Executor): @handler async def handle_message( self, - message: list[ChatMessage], - ctx: WorkflowContext[list[ChatMessage], AgentResponseUpdate | AgentResponse], + message: list[Message], + ctx: WorkflowContext[list[Message], AgentResponseUpdate | AgentResponse], ) -> None: input_text = message[0].contents[0].text if message and message[0].contents[0].type == "text" else "no input" response_text = f"{self.response_text}: {input_text}" # Create response message for both streaming and non-streaming cases - response_message = ChatMessage(role="assistant", contents=[Content.from_text(text=response_text)]) + response_message = Message(role="assistant", contents=[Content.from_text(text=response_text)]) if self.streaming: # Emit update event. @@ -70,7 +70,7 @@ class RequestingExecutor(Executor): self.streaming = streaming @handler - async def handle_message(self, _: list[ChatMessage], ctx: WorkflowContext) -> None: + async def handle_message(self, _: list[Message], ctx: WorkflowContext) -> None: # Send a RequestInfoMessage to trigger the request info process await ctx.request_info("Mock request data", str) @@ -79,7 +79,7 @@ class RequestingExecutor(Executor): self, original_request: str, response: str, - ctx: WorkflowContext[ChatMessage, AgentResponseUpdate | AgentResponse], + ctx: WorkflowContext[Message, AgentResponseUpdate | AgentResponse], ) -> None: # Handle the response and emit completion response content = Content.from_text(text=f"Request completed with response: {response}") @@ -96,7 +96,7 @@ class RequestingExecutor(Executor): await ctx.yield_output( AgentResponse( messages=[ - ChatMessage( + Message( role="assistant", contents=[content], ) @@ -110,14 +110,14 @@ class ConversationHistoryCapturingExecutor(Executor): def __init__(self, id: str, streaming: bool = False): super().__init__(id=id) - self.received_messages: list[ChatMessage] = [] + self.received_messages: list[Message] = [] self.streaming = streaming @handler async def handle_message( self, - messages: list[ChatMessage], - ctx: WorkflowContext[list[ChatMessage], AgentResponseUpdate | AgentResponse], + messages: list[Message], + ctx: WorkflowContext[list[Message], AgentResponseUpdate | AgentResponse], ) -> None: # Capture all received messages self.received_messages = list(messages) @@ -126,7 +126,7 @@ class ConversationHistoryCapturingExecutor(Executor): message_count = len(messages) response_text = f"Received {message_count} messages" - response_message = ChatMessage(role="assistant", contents=[Content.from_text(text=response_text)]) + response_message = Message(role="assistant", contents=[Content.from_text(text=response_text)]) if self.streaming: # Emit streaming update @@ -162,8 +162,8 @@ class TestWorkflowAgent: assert len(result.messages) >= 2, f"Expected at least 2 messages, got {len(result.messages)}" # Find messages from each executor - step1_messages: list[ChatMessage] = [] - step2_messages: list[ChatMessage] = [] + step1_messages: list[Message] = [] + step2_messages: list[Message] = [] for message in result.messages: first_content = message.contents[0] @@ -281,7 +281,7 @@ class TestWorkflowAgent: ), ) - response_message = ChatMessage(role="user", contents=[approval_response]) + response_message = Message(role="user", contents=[approval_response]) # Continue the workflow with the response continuation_result = await agent.run(response_message) @@ -325,7 +325,7 @@ class TestWorkflowAgent: workflow = WorkflowBuilder(start_executor=executor).build() # Try to create an agent with unsupported input types - with pytest.raises(ValueError, match="Workflow's start executor cannot handle list\\[ChatMessage\\]"): + with pytest.raises(ValueError, match="Workflow's start executor cannot handle list\\[Message\\]"): workflow.as_agent() async def test_workflow_as_agent_yield_output_surfaces_as_agent_response(self) -> None: @@ -336,7 +336,7 @@ class TestWorkflowAgent: """ @executor - async def yielding_executor(messages: list[ChatMessage], ctx: WorkflowContext[Never, str]) -> None: + async def yielding_executor(messages: list[Message], ctx: WorkflowContext[Never, str]) -> None: # Extract text from input for demonstration input_text = messages[0].text if messages else "no input" await ctx.yield_output(f"processed: {input_text}") @@ -344,7 +344,7 @@ class TestWorkflowAgent: workflow = WorkflowBuilder(start_executor=yielding_executor).build() # Run directly - should return output event (type='output') in result - direct_result = await workflow.run([ChatMessage(role="user", text="hello")]) + direct_result = await workflow.run([Message(role="user", text="hello")]) direct_outputs = direct_result.get_outputs() assert len(direct_outputs) == 1 assert direct_outputs[0] == "processed: hello" @@ -361,7 +361,7 @@ class TestWorkflowAgent: """Test that ctx.yield_output() surfaces as AgentResponseUpdate when streaming.""" @executor - async def yielding_executor(messages: list[ChatMessage], ctx: WorkflowContext[Never, str]) -> None: + async def yielding_executor(messages: list[Message], ctx: WorkflowContext[Never, str]) -> None: await ctx.yield_output("first output") await ctx.yield_output("second output") @@ -381,7 +381,7 @@ class TestWorkflowAgent: """Test that yield_output preserves different content types (Content, Content, etc.).""" @executor - async def content_yielding_executor(messages: list[ChatMessage], ctx: WorkflowContext[Never, Content]) -> None: + async def content_yielding_executor(messages: list[Message], ctx: WorkflowContext[Never, Content]) -> None: # Yield different content types await ctx.yield_output(Content.from_text(text="text content")) await ctx.yield_output(Content.from_data(data=b"binary data", media_type="application/octet-stream")) @@ -406,11 +406,11 @@ class TestWorkflowAgent: assert result.messages[2].contents[0].uri == "https://example.com/image.png" async def test_workflow_as_agent_yield_output_with_chat_message(self) -> None: - """Test that yield_output with ChatMessage preserves the message structure.""" + """Test that yield_output with Message preserves the message structure.""" @executor - async def chat_message_executor(messages: list[ChatMessage], ctx: WorkflowContext[Never, ChatMessage]) -> None: - msg = ChatMessage( + async def chat_message_executor(messages: list[Message], ctx: WorkflowContext[Never, Message]) -> None: + msg = Message( role="assistant", contents=[Content.from_text(text="response text")], author_name="custom-author", @@ -440,7 +440,7 @@ class TestWorkflowAgent: @executor async def raw_yielding_executor( - messages: list[ChatMessage], ctx: WorkflowContext[Never, Content | CustomData | str] + messages: list[Message], ctx: WorkflowContext[Never, Content | CustomData | str] ) -> None: # Yield different types of data await ctx.yield_output("simple string") @@ -469,21 +469,19 @@ class TestWorkflowAgent: assert updates[2].raw_representation.value == 42 async def test_workflow_as_agent_yield_output_with_list_of_chat_messages(self) -> None: - """Test that yield_output with list[ChatMessage] extracts contents from all messages. + """Test that yield_output with list[Message] extracts contents from all messages. Note: Content items are coalesced by _finalize_response, so multiple text contents become a single merged Content in the final response. """ @executor - async def list_yielding_executor( - messages: list[ChatMessage], ctx: WorkflowContext[Never, list[ChatMessage]] - ) -> None: - # Yield a list of ChatMessages (as SequentialBuilder does) + async def list_yielding_executor(messages: list[Message], ctx: WorkflowContext[Never, list[Message]]) -> None: + # Yield a list of Messages (as SequentialBuilder does) msg_list = [ - ChatMessage(role="user", text="first message"), - ChatMessage(role="assistant", text="second message"), - ChatMessage( + Message(role="user", text="first message"), + Message(role="assistant", text="second message"), + Message( role="assistant", contents=[Content.from_text(text="third"), Content.from_text(text="fourth")], ), @@ -526,8 +524,8 @@ class TestWorkflowAgent: # Create a thread with existing conversation history history_messages = [ - ChatMessage(role="user", text="Previous user message"), - ChatMessage(role="assistant", text="Previous assistant response"), + Message(role="user", text="Previous user message"), + Message(role="assistant", text="Previous assistant response"), ] message_store = ChatMessageStore(messages=history_messages) thread = AgentThread(message_store=message_store) @@ -556,9 +554,9 @@ class TestWorkflowAgent: # Create a thread with existing conversation history history_messages = [ - ChatMessage(role="system", text="You are a helpful assistant"), - ChatMessage(role="user", text="Hello"), - ChatMessage("assistant", ["Hi there!"]), + Message(role="system", text="You are a helpful assistant"), + Message(role="user", text="Hello"), + Message("assistant", ["Hi there!"]), ] message_store = ChatMessageStore(messages=history_messages) thread = AgentThread(message_store=message_store) @@ -629,7 +627,7 @@ class TestWorkflowAgent: def run( self, - messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, + messages: str | Content | Message | Sequence[str | Content | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -641,7 +639,7 @@ class TestWorkflowAgent: async def _run( self, - messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, + messages: str | Content | Message | Sequence[str | Content | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -649,12 +647,12 @@ class TestWorkflowAgent: ) -> AgentResponse: return AgentResponse( - messages=[ChatMessage("assistant", [self._response_text])], + messages=[Message("assistant", [self._response_text])], ) def _run_stream( self, - messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, + messages: str | Content | Message | Sequence[str | Content | Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -670,16 +668,20 @@ class TestWorkflowAgent: return ResponseStream(_iter(), finalizer=AgentResponse.from_updates) @executor - async def start_executor(messages: list[ChatMessage], ctx: WorkflowContext[AgentExecutorRequest, str]) -> None: + async def start_exec(messages: list[Message], ctx: WorkflowContext[AgentExecutorRequest, str]) -> None: await ctx.yield_output("Start output") await ctx.send_message(AgentExecutorRequest(messages=messages, should_respond=True)) - # Build workflow: start -> agent1 (no output) -> agent2 (output_response=True) - builder = WorkflowBuilder(start_executor="start", output_executors=["start", "agent2"]) - builder.register_executor(lambda: start_executor, "start") - builder.register_agent(lambda: MockAgent("agent1", "Agent1 output - should NOT appear"), "agent1") - builder.register_agent(lambda: MockAgent("agent2", "Agent2 output - SHOULD appear"), "agent2") - workflow = builder.add_edge("start", "agent1").add_edge("agent1", "agent2").build() + agent1 = MockAgent("agent1", "Agent1 output - should NOT appear") + agent2 = MockAgent("agent2", "Agent2 output - SHOULD appear") + + # Build workflow: start -> agent1 (no output) -> agent2 (output visible) + workflow = ( + WorkflowBuilder(start_executor=start_exec, output_executors=[start_exec, agent2]) + .add_edge(start_exec, agent1) + .add_edge(agent1, agent2) + .build() + ) agent = WorkflowAgent(workflow=workflow, name="Test Agent") result = await agent.run("Test input") @@ -713,7 +715,7 @@ class TestWorkflowAgent: def run( self, - messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, + messages: str | Content | Message | Sequence[str | Content | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -725,7 +727,7 @@ class TestWorkflowAgent: async def _run( self, - messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, + messages: str | Content | Message | Sequence[str | Content | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -733,12 +735,12 @@ class TestWorkflowAgent: ) -> AgentResponse: return AgentResponse( - messages=[ChatMessage("assistant", [self._response_text])], + messages=[Message("assistant", [self._response_text])], ) def _run_stream( self, - messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None, + messages: str | Content | Message | Sequence[str | Content | Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -754,17 +756,13 @@ class TestWorkflowAgent: return ResponseStream(_iter(), finalizer=AgentResponse.from_updates) @executor - async def start_executor(messages: list[ChatMessage], ctx: WorkflowContext[AgentExecutorRequest]) -> None: + async def start_exec(messages: list[Message], ctx: WorkflowContext[AgentExecutorRequest]) -> None: await ctx.send_message(AgentExecutorRequest(messages=messages, should_respond=True)) + mock_agent = MockAgent("agent", "Unique response text") + # Build workflow with single agent - workflow = ( - WorkflowBuilder(start_executor="start") - .register_executor(lambda: start_executor, "start") - .register_agent(lambda: MockAgent("agent", "Unique response text"), "agent") - .add_edge("start", "agent") - .build() - ) + workflow = WorkflowBuilder(start_executor=start_exec).add_edge(start_exec, mock_agent).build() agent = WorkflowAgent(workflow=workflow, name="Test Agent") result = await agent.run("Test input") @@ -810,8 +808,8 @@ class TestWorkflowAgentAuthorName: @handler async def handle_message( self, - message: list[ChatMessage], - ctx: WorkflowContext[list[ChatMessage], AgentResponseUpdate], + message: list[Message], + ctx: WorkflowContext[list[Message], AgentResponseUpdate], ) -> None: # Emit update with explicit author_name update = AgentResponseUpdate( diff --git a/python/packages/core/tests/workflow/test_workflow_builder.py b/python/packages/core/tests/workflow/test_workflow_builder.py index 39c60717c2..fd0d74586a 100644 --- a/python/packages/core/tests/workflow/test_workflow_builder.py +++ b/python/packages/core/tests/workflow/test_workflow_builder.py @@ -11,8 +11,8 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Executor, + Message, WorkflowBuilder, WorkflowContext, WorkflowValidationError, @@ -27,13 +27,13 @@ class DummyAgent(BaseAgent): return self._run_impl(messages) async def _run_impl(self, messages=None) -> AgentResponse: - norm: list[ChatMessage] = [] + norm: list[Message] = [] if messages: for m in messages: # type: ignore[iteration-over-optional] - if isinstance(m, ChatMessage): + if isinstance(m, Message): norm.append(m) elif isinstance(m, str): - norm.append(ChatMessage(role="user", text=m)) + norm.append(Message(role="user", text=m)) return AgentResponse(messages=norm) async def _run_stream_impl(self): # type: ignore[override] @@ -134,322 +134,65 @@ def test_add_agent_duplicate_id_raises_error(): builder.add_edge(agent1, agent2).build() -# Tests for new executor registration patterns +def test_fan_out_edges_with_direct_instances(): + """Test fan-out edges with direct executor instances.""" + source = MockExecutor(id="Source") + target1 = MockExecutor(id="Target1") + target2 = MockExecutor(id="Target2") + workflow = WorkflowBuilder(start_executor=source).add_fan_out_edges(source, [target1, target2]).build() -def test_register_executor_basic(): - """Test basic executor registration with lazy initialization.""" - builder = WorkflowBuilder(start_executor="TestExecutor") - - # Register an executor factory - ID must match the registered name - result = builder.register_executor(lambda: MockExecutor(id="TestExecutor"), name="TestExecutor") - - # Verify that register returns the builder for chaining - assert result is builder - - # Build workflow and verify executor is instantiated - workflow = builder.build() - assert "TestExecutor" in workflow.executors - assert isinstance(workflow.executors["TestExecutor"], MockExecutor) - - -def test_register_multiple_executors(): - """Test registering multiple executors and connecting them with edges.""" - builder = WorkflowBuilder(start_executor="ExecutorA") - - # Register multiple executors - IDs must match registered names - builder.register_executor(lambda: MockExecutor(id="ExecutorA"), name="ExecutorA") - builder.register_executor(lambda: MockExecutor(id="ExecutorB"), name="ExecutorB") - builder.register_executor(lambda: MockExecutor(id="ExecutorC"), name="ExecutorC") - - # Build workflow with edges using registered names - workflow = builder.add_edge("ExecutorA", "ExecutorB").add_edge("ExecutorB", "ExecutorC").build() - - # Verify all executors are present - assert "ExecutorA" in workflow.executors - assert "ExecutorB" in workflow.executors - assert "ExecutorC" in workflow.executors - assert workflow.start_executor_id == "ExecutorA" - - -def test_register_with_multiple_names(): - """Test registering the same factory function under multiple names.""" - builder = WorkflowBuilder(start_executor="ExecutorA") - - # Register same executor factory under multiple names - # Note: Each call creates a new instance, so IDs won't conflict - counter = {"val": 0} - - def make_executor(): - counter["val"] += 1 - return MockExecutor(id="ExecutorA" if counter["val"] == 1 else "ExecutorB") - - builder.register_executor(make_executor, name=["ExecutorA", "ExecutorB"]) - - # Set up workflow - workflow = builder.add_edge("ExecutorA", "ExecutorB").build() - - # Verify both executors are present - assert "ExecutorA" in workflow.executors - assert "ExecutorB" in workflow.executors - assert workflow.start_executor_id == "ExecutorA" - - -def test_register_duplicate_name_raises_error(): - """Test that registering duplicate names raises an error.""" - builder = WorkflowBuilder(start_executor="MyExecutor") - - # Register first executor - builder.register_executor(lambda: MockExecutor(id="executor_1"), name="MyExecutor") - - # Registering second executor with same name should raise ValueError - with pytest.raises(ValueError, match="already registered"): - builder.register_executor(lambda: MockExecutor(id="executor_2"), name="MyExecutor") - - -def test_register_duplicate_id_raises_error(): - """Test that registering duplicate id raises an error.""" - builder = WorkflowBuilder(start_executor="MyExecutor1") - - # Register first executor - builder.register_executor(lambda: MockExecutor(id="executor"), name="MyExecutor1") - builder.register_executor(lambda: MockExecutor(id="executor"), name="MyExecutor2") - - # Registering second executor with same ID should raise ValueError - with pytest.raises(ValueError, match="Executor with ID 'executor' has already been registered."): - builder.build() - - -def test_register_agent_basic(): - """Test basic agent registration with lazy initialization.""" - builder = WorkflowBuilder(start_executor="TestAgent") - - # Register an agent factory - result = builder.register_agent(lambda: DummyAgent(id="agent_test", name="test_agent"), name="TestAgent") - - # Verify that register_agent returns the builder for chaining - assert result is builder - - # Build workflow and verify agent is wrapped in AgentExecutor - workflow = builder.build() - assert "test_agent" in workflow.executors - assert isinstance(workflow.executors["test_agent"], AgentExecutor) - - -def test_register_agent_with_thread(): - """Test registering an agent with a custom thread.""" - builder = WorkflowBuilder(start_executor="ThreadedAgent") - custom_thread = AgentThread() - - # Register agent with custom thread - builder.register_agent( - lambda: DummyAgent(id="agent_with_thread", name="threaded_agent"), - name="ThreadedAgent", - agent_thread=custom_thread, - ) - - # Build workflow and verify agent executor configuration - workflow = builder.build() - executor = workflow.executors["threaded_agent"] - - assert isinstance(executor, AgentExecutor) - assert executor.id == "threaded_agent" - assert executor._agent_thread is custom_thread # type: ignore - - -def test_register_agent_duplicate_name_raises_error(): - """Test that registering agents with duplicate names raises an error.""" - builder = WorkflowBuilder(start_executor="MyAgent") - - # Register first agent - builder.register_agent(lambda: DummyAgent(id="agent1", name="first"), name="MyAgent") - - # Registering second agent with same name should raise ValueError - with pytest.raises(ValueError, match="already registered"): - builder.register_agent(lambda: DummyAgent(id="agent2", name="second"), name="MyAgent") - - -def test_register_and_add_edge_with_strings(): - """Test that registered executors can be connected using string names.""" - builder = WorkflowBuilder(start_executor="Source") - - # Register executors - builder.register_executor(lambda: MockExecutor(id="source"), name="Source") - builder.register_executor(lambda: MockExecutor(id="target"), name="Target") - - # Add edge using string names - workflow = builder.add_edge("Source", "Target").build() - - # Verify edge is created correctly - assert workflow.start_executor_id == "source" - assert "source" in workflow.executors - assert "target" in workflow.executors - - -def test_register_agent_and_add_edge_with_strings(): - """Test that registered agents can be connected using string names.""" - builder = WorkflowBuilder(start_executor="Writer") - - # Register agents - builder.register_agent(lambda: DummyAgent(id="writer_id", name="writer"), name="Writer") - builder.register_agent(lambda: DummyAgent(id="reviewer_id", name="reviewer"), name="Reviewer") - - # Add edge using string names - workflow = builder.add_edge("Writer", "Reviewer").build() - - # Verify edge is created correctly - assert workflow.start_executor_id == "writer" - assert "writer" in workflow.executors - assert "reviewer" in workflow.executors - assert all(isinstance(e, AgentExecutor) for e in workflow.executors.values()) - - -def test_register_with_fan_out_edges(): - """Test using registered names with fan-out edge groups.""" - builder = WorkflowBuilder(start_executor="Source") - - # Register executors - IDs must match registered names - builder.register_executor(lambda: MockExecutor(id="Source"), name="Source") - builder.register_executor(lambda: MockExecutor(id="Target1"), name="Target1") - builder.register_executor(lambda: MockExecutor(id="Target2"), name="Target2") - - # Add fan-out edges using registered names - workflow = builder.add_fan_out_edges("Source", ["Target1", "Target2"]).build() - - # Verify all executors are present assert "Source" in workflow.executors assert "Target1" in workflow.executors assert "Target2" in workflow.executors -def test_register_with_fan_in_edges(): - """Test using registered names with fan-in edge groups.""" - builder = WorkflowBuilder(start_executor="Source1") +def test_fan_in_edges_with_direct_instances(): + """Test fan-in edges with direct executor instances.""" + source1 = MockExecutor(id="Source1") + source2 = MockExecutor(id="Source2") + aggregator = MockAggregator(id="Aggregator") - # Register executors - IDs must match registered names - builder.register_executor(lambda: MockExecutor(id="Source1"), name="Source1") - builder.register_executor(lambda: MockExecutor(id="Source2"), name="Source2") - builder.register_executor(lambda: MockAggregator(id="Aggregator"), name="Aggregator") + workflow = ( + WorkflowBuilder(start_executor=source1) + .add_edge(source1, source2) + .add_fan_in_edges([source1, source2], aggregator) + .build() + ) - # Add fan-in edges using registered names - # Both Source1 and Source2 need to be reachable, so connect Source1 to Source2 - workflow = builder.add_edge("Source1", "Source2").add_fan_in_edges(["Source1", "Source2"], "Aggregator").build() - - # Verify all executors are present assert "Source1" in workflow.executors assert "Source2" in workflow.executors assert "Aggregator" in workflow.executors -def test_register_with_chain(): - """Test using registered names with add_chain.""" - builder = WorkflowBuilder(start_executor="Step1") +def test_chain_with_direct_instances(): + """Test add_chain with direct executor instances.""" + step1 = MockExecutor(id="Step1") + step2 = MockExecutor(id="Step2") + step3 = MockExecutor(id="Step3") - # Register executors - IDs must match registered names - builder.register_executor(lambda: MockExecutor(id="Step1"), name="Step1") - builder.register_executor(lambda: MockExecutor(id="Step2"), name="Step2") - builder.register_executor(lambda: MockExecutor(id="Step3"), name="Step3") + workflow = WorkflowBuilder(start_executor=step1).add_chain([step1, step2, step3]).build() - # Add chain using registered names - workflow = builder.add_chain(["Step1", "Step2", "Step3"]).build() - - # Verify all executors are present assert "Step1" in workflow.executors assert "Step2" in workflow.executors assert "Step3" in workflow.executors assert workflow.start_executor_id == "Step1" -def test_register_factory_called_only_once(): - """Test that registered factory functions are called only during build.""" - call_count = 0 - - def factory(): - nonlocal call_count - call_count += 1 - return MockExecutor(id="Test") - - builder = WorkflowBuilder(start_executor="Test") - builder.register_executor(factory, name="Test") - - # Factory should not be called yet - assert call_count == 0 - - # Factory should still not be called - assert call_count == 0 - - # Build workflow - workflow = builder.build() - - # Factory should now be called exactly once - assert call_count == 1 - assert "Test" in workflow.executors - - -def test_mixing_eager_and_lazy_initialization_error(): - """Test that mixing eager executor instances with lazy string names raises appropriate error.""" - builder = WorkflowBuilder(start_executor="Lazy") - - # Create an eager executor instance - eager_executor = MockExecutor(id="eager") - - # Register a lazy executor - builder.register_executor(lambda: MockExecutor(id="Lazy"), name="Lazy") - - # Mixing eager and lazy should raise an error during add_edge - with pytest.raises( - ValueError, - match=( - r"Both source and target must be either registered factory names \(str\) " - r"or Executor/SupportsAgentRun instances\." - ), - ): - builder.add_edge(eager_executor, "Lazy") - - -def test_register_with_condition(): - """Test adding edges with conditions using registered names.""" - builder = WorkflowBuilder(start_executor="Source") +def test_add_edge_with_condition(): + """Test adding edges with conditions using direct executor instances.""" + source = MockExecutor(id="Source") + target = MockExecutor(id="Target") def condition_func(msg: MockMessage) -> bool: return msg.data > 0 - # Register executors - IDs must match registered names - builder.register_executor(lambda: MockExecutor(id="Source"), name="Source") - builder.register_executor(lambda: MockExecutor(id="Target"), name="Target") + workflow = WorkflowBuilder(start_executor=source).add_edge(source, target, condition=condition_func).build() - # Add edge with condition - workflow = builder.add_edge("Source", "Target", condition=condition_func).build() - - # Verify workflow is built correctly assert "Source" in workflow.executors assert "Target" in workflow.executors -def test_register_agent_creates_unique_instances(): - """Test that registered agent factories create new instances on each build.""" - instance_ids: list[int] = [] - - def agent_factory() -> DummyAgent: - agent = DummyAgent(id=f"agent_{len(instance_ids)}", name="test") - instance_ids.append(id(agent)) - return agent - - # Build first workflow - builder1 = WorkflowBuilder(start_executor="Agent") - builder1.register_agent(agent_factory, name="Agent") - _ = builder1.build() - - # Build second workflow - builder2 = WorkflowBuilder(start_executor="Agent") - builder2.register_agent(agent_factory, name="Agent") - _ = builder2.build() - - # Verify that two different agent instances were created - assert len(instance_ids) == 2 - assert instance_ids[0] != instance_ids[1] - - # region with_output_from tests @@ -488,14 +231,17 @@ def test_with_output_from_with_agent_instances(): assert workflow._output_executors == ["reviewer"] # type: ignore -def test_with_output_from_with_registered_names(): - """Test with_output_from with registered factory names (strings).""" - builder = WorkflowBuilder(start_executor="ExecutorAFactory", output_executors=["ExecutorBFactory"]) - builder.register_executor(lambda: MockExecutor(id="ExecutorA"), name="ExecutorAFactory") - builder.register_executor(lambda: MockExecutor(id="ExecutorB"), name="ExecutorBFactory") - workflow = builder.add_edge("ExecutorAFactory", "ExecutorBFactory").build() +def test_with_output_from_with_executor_instances_by_id(): + """Test with_output_from with direct executor instances resolves to executor IDs.""" + executor_a = MockExecutor(id="ExecutorA") + executor_b = MockExecutor(id="ExecutorB") + + workflow = ( + WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b]) + .add_edge(executor_a, executor_b) + .build() + ) - # Verify that the workflow was built with the correct output executors assert workflow._output_executors == ["ExecutorB"] # type: ignore @@ -531,14 +277,17 @@ def test_with_output_from_can_be_set_to_different_value(): assert workflow._output_executors == ["executor_b"] # type: ignore -def test_with_output_from_with_registered_agents(): - """Test with_output_from with registered agent factory names.""" - builder = WorkflowBuilder(start_executor="WriterAgent", output_executors=["ReviewerAgent"]) - builder.register_agent(lambda: DummyAgent(id="agent1", name="writer"), name="WriterAgent") - builder.register_agent(lambda: DummyAgent(id="agent2", name="reviewer"), name="ReviewerAgent") - workflow = builder.add_edge("WriterAgent", "ReviewerAgent").build() +def test_with_output_from_with_agent_instances_resolves_name(): + """Test with_output_from with agent instances resolves to agent names.""" + agent_writer = DummyAgent(id="agent1", name="writer") + agent_reviewer = DummyAgent(id="agent2", name="reviewer") + + workflow = ( + WorkflowBuilder(start_executor=agent_writer, output_executors=[agent_reviewer]) + .add_edge(agent_writer, agent_reviewer) + .build() + ) - # Verify that the workflow was built with the agent's resolved name assert workflow._output_executors == ["reviewer"] # type: ignore diff --git a/python/packages/core/tests/workflow/test_workflow_kwargs.py b/python/packages/core/tests/workflow/test_workflow_kwargs.py index 2e46454601..83e13975db 100644 --- a/python/packages/core/tests/workflow/test_workflow_kwargs.py +++ b/python/packages/core/tests/workflow/test_workflow_kwargs.py @@ -10,8 +10,8 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, + Message, ResponseStream, WorkflowRunState, tool, @@ -52,7 +52,7 @@ class _KwargsCapturingAgent(BaseAgent): def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -67,7 +67,7 @@ class _KwargsCapturingAgent(BaseAgent): return ResponseStream(_stream(), finalizer=AgentResponse.from_updates) async def _run() -> AgentResponse: - return AgentResponse(messages=[ChatMessage("assistant", [f"{self.name} response"])]) + return AgentResponse(messages=[Message("assistant", [f"{self.name} response"])]) return _run() @@ -222,7 +222,7 @@ async def test_kwargs_stored_in_state() -> None: class _StateInspector(Executor): @handler - async def inspect(self, msgs: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None: + async def inspect(self, msgs: list[Message], ctx: WorkflowContext[list[Message]]) -> None: nonlocal stored_kwargs stored_kwargs = ctx.get_state(WORKFLOW_RUN_KWARGS_KEY) await ctx.send_message(msgs) @@ -247,7 +247,7 @@ async def test_empty_kwargs_stored_as_empty_dict() -> None: class _StateChecker(Executor): @handler - async def check(self, msgs: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None: + async def check(self, msgs: list[Message], ctx: WorkflowContext[list[Message]]) -> None: nonlocal stored_kwargs stored_kwargs = ctx.get_state(WORKFLOW_RUN_KWARGS_KEY) await ctx.send_message(msgs) @@ -388,11 +388,11 @@ async def test_magentic_kwargs_flow_to_agents() -> None: super().__init__(max_stall_count=3, max_reset_count=None, max_round_count=2) self.task_ledger = None - async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role="assistant", text="Plan: Test task", author_name="manager") + async def plan(self, magentic_context: MagenticContext) -> Message: + return Message(role="assistant", text="Plan: Test task", author_name="manager") - async def replan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role="assistant", text="Replan: Test task", author_name="manager") + async def replan(self, magentic_context: MagenticContext) -> Message: + return Message(role="assistant", text="Replan: Test task", author_name="manager") async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: # Return completed on first call @@ -404,8 +404,8 @@ async def test_magentic_kwargs_flow_to_agents() -> None: next_speaker=MagenticProgressLedgerItem(answer="agent1", reason="First"), ) - async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role="assistant", text="Final answer", author_name="manager") + async def prepare_final_answer(self, magentic_context: MagenticContext) -> Message: + return Message(role="assistant", text="Final answer", author_name="manager") agent = _KwargsCapturingAgent(name="agent1") manager = _MockManager() @@ -439,11 +439,11 @@ async def test_magentic_kwargs_stored_in_state() -> None: super().__init__(max_stall_count=3, max_reset_count=None, max_round_count=1) self.task_ledger = None - async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role="assistant", text="Plan", author_name="manager") + async def plan(self, magentic_context: MagenticContext) -> Message: + return Message(role="assistant", text="Plan", author_name="manager") - async def replan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role="assistant", text="Replan", author_name="manager") + async def replan(self, magentic_context: MagenticContext) -> Message: + return Message(role="assistant", text="Replan", author_name="manager") async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: return MagenticProgressLedger( @@ -454,8 +454,8 @@ async def test_magentic_kwargs_stored_in_state() -> None: next_speaker=MagenticProgressLedgerItem(answer="agent1", reason="First"), ) - async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role="assistant", text="Final", author_name="manager") + async def prepare_final_answer(self, magentic_context: MagenticContext) -> Message: + return Message(role="assistant", text="Final", author_name="manager") agent = _KwargsCapturingAgent(name="agent1") manager = _MockManager() @@ -660,7 +660,7 @@ async def test_subworkflow_kwargs_accessible_via_state() -> None: """Executor that reads kwargs from State for verification.""" @handler - async def read_kwargs(self, msgs: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None: + async def read_kwargs(self, msgs: list[Message], ctx: WorkflowContext[list[Message]]) -> None: kwargs_from_state = ctx.get_state(WORKFLOW_RUN_KWARGS_KEY) captured_kwargs_from_state.append(kwargs_from_state or {}) await ctx.send_message(msgs) diff --git a/python/packages/core/tests/workflow/test_workflow_observability.py b/python/packages/core/tests/workflow/test_workflow_observability.py index b81e0acae0..d5c20ad429 100644 --- a/python/packages/core/tests/workflow/test_workflow_observability.py +++ b/python/packages/core/tests/workflow/test_workflow_observability.py @@ -8,7 +8,7 @@ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanE from agent_framework import InMemoryCheckpointStorage, WorkflowBuilder from agent_framework._workflows._executor import Executor, handler -from agent_framework._workflows._runner_context import InProcRunnerContext, Message, MessageType +from agent_framework._workflows._runner_context import InProcRunnerContext, MessageType, WorkflowMessage from agent_framework._workflows._state import State from agent_framework._workflows._workflow import Workflow from agent_framework._workflows._workflow_context import WorkflowContext @@ -440,7 +440,7 @@ async def test_message_trace_context_serialization(span_exporter: InMemorySpanEx ctx = InProcRunnerContext(InMemoryCheckpointStorage()) # Create message with trace context - message = Message( + message = WorkflowMessage( data="test", source_id="source", target_id="target", @@ -474,8 +474,9 @@ async def test_message_trace_context_serialization(span_exporter: InMemorySpanEx async def test_workflow_build_error_tracing(span_exporter: InMemorySpanExporter) -> None: """Test that build errors are properly recorded in build spans.""" - # Test validation error by referencing a non-existent start executor - builder = WorkflowBuilder(start_executor="NonExistent") + # Create a valid builder, then clear the start executor to trigger a build-time ValueError + builder = WorkflowBuilder(start_executor=MockExecutor(id="mock")) + builder._start_executor = None # type: ignore[assignment] with pytest.raises(ValueError): builder.build() diff --git a/python/packages/declarative/agent_framework_declarative/_loader.py b/python/packages/declarative/agent_framework_declarative/_loader.py index 493787350e..f3bbb6d87a 100644 --- a/python/packages/declarative/agent_framework_declarative/_loader.py +++ b/python/packages/declarative/agent_framework_declarative/_loader.py @@ -5,19 +5,12 @@ from __future__ import annotations import sys from collections.abc import Callable, Mapping from pathlib import Path -from typing import Any, Literal, cast +from typing import Any, cast import yaml from agent_framework import ( - ChatAgent, - ChatClientProtocol, - Content, - HostedCodeInterpreterTool, - HostedFileSearchTool, - HostedMCPSpecificApproval, - HostedMCPTool, - HostedWebSearchTool, - ToolProtocol, + Agent, + SupportsChatGetResponse, ) from agent_framework import ( FunctionTool as AFFunctionTool, @@ -124,10 +117,10 @@ class ProviderLookupError(DeclarativeLoaderError): class AgentFactory: - """Factory for creating ChatAgent instances from declarative YAML definitions. + """Factory for creating Agent instances from declarative YAML definitions. AgentFactory parses YAML agent definitions (PromptAgent kind) and creates - configured ChatAgent instances with the appropriate chat client, tools, + configured Agent instances with the appropriate chat client, tools, and response format. Examples: @@ -150,7 +143,7 @@ class AgentFactory: # With pre-configured chat client client = AzureOpenAIChatClient() - factory = AgentFactory(chat_client=client) + factory = AgentFactory(client=client) agent = factory.create_agent_from_yaml_path("agent.yaml") .. code-block:: python @@ -174,7 +167,7 @@ class AgentFactory: def __init__( self, *, - chat_client: ChatClientProtocol | None = None, + client: SupportsChatGetResponse | None = None, bindings: Mapping[str, Any] | None = None, connections: Mapping[str, Any] | None = None, client_kwargs: Mapping[str, Any] | None = None, @@ -187,8 +180,8 @@ class AgentFactory: """Create the agent factory. Args: - chat_client: An optional ChatClientProtocol instance to use as a dependency. - This will be passed to the ChatAgent that gets created. + client: An optional SupportsChatGetResponse instance to use as a dependency. + This will be passed to the Agent that gets created. If you need to create multiple agents with different chat clients, do not pass this and instead provide the chat client in the YAML definition. bindings: An optional dictionary of bindings to use when creating agents. @@ -210,9 +203,9 @@ class AgentFactory: Here, "Provider.ApiType" is the lookup key used when both provider and apiType are specified in the model, "Provider" is also allowed. - Package refers to which model needs to be imported, Name is the class name of the ChatClientProtocol - implementation, and model_id_field is the name of the field in the constructor - that accepts the model.id value. + Package refers to which model needs to be imported, Name is the class name of the + SupportsChatGetResponse implementation, and model_id_field is the name of the field in the + constructor that accepts the model.id value. default_provider: The default provider used when model.provider is not specified, default is "AzureAIClient". safe_mode: Whether to run in safe mode, default is True. @@ -241,7 +234,7 @@ class AgentFactory: # With shared chat client client = AzureOpenAIChatClient() factory = AgentFactory( - chat_client=client, + client=client, env_file_path=".env", ) @@ -260,7 +253,7 @@ class AgentFactory: }, ) """ - self.chat_client = chat_client + self.client = client self.bindings = bindings self.connections = connections self.client_kwargs = client_kwargs or {} @@ -269,8 +262,8 @@ class AgentFactory: self.safe_mode = safe_mode load_dotenv(dotenv_path=env_file_path, encoding=env_file_encoding) - def create_agent_from_yaml_path(self, yaml_path: str | Path) -> ChatAgent: - """Create a ChatAgent from a YAML file path. + def create_agent_from_yaml_path(self, yaml_path: str | Path) -> Agent: + """Create a Agent from a YAML file path. This method does the following things: @@ -278,13 +271,13 @@ class AgentFactory: 2. Validates that the loaded object is a PromptAgent. 3. Creates the appropriate ChatClient based on the model provider and apiType. 4. Parses the tools, options, and response format from the PromptAgent. - 5. Creates and returns a ChatAgent instance with the configured properties. + 5. Creates and returns a Agent instance with the configured properties. Args: yaml_path: Path to the YAML file representation of a PromptAgent. Returns: - The ``ChatAgent`` instance created from the YAML file. + The ``Agent`` instance created from the YAML file. Raises: DeclarativeLoaderError: If the YAML does not represent a PromptAgent. @@ -323,8 +316,8 @@ class AgentFactory: yaml_str = f.read() return self.create_agent_from_yaml(yaml_str) - def create_agent_from_yaml(self, yaml_str: str) -> ChatAgent: - """Create a ChatAgent from a YAML string. + def create_agent_from_yaml(self, yaml_str: str) -> Agent: + """Create a Agent from a YAML string. This method does the following things: @@ -332,13 +325,13 @@ class AgentFactory: 2. Validates that the loaded object is a PromptAgent. 3. Creates the appropriate ChatClient based on the model provider and apiType. 4. Parses the tools, options, and response format from the PromptAgent. - 5. Creates and returns a ChatAgent instance with the configured properties. + 5. Creates and returns a Agent instance with the configured properties. Args: yaml_str: YAML string representation of a PromptAgent. Returns: - The ``ChatAgent`` instance created from the YAML string. + The ``Agent`` instance created from the YAML string. Raises: DeclarativeLoaderError: If the YAML does not represent a PromptAgent. @@ -396,8 +389,8 @@ class AgentFactory: """ return self.create_agent_from_dict(yaml.safe_load(yaml_str)) - def create_agent_from_dict(self, agent_def: dict[str, Any]) -> ChatAgent: - """Create a ChatAgent from a dictionary definition. + def create_agent_from_dict(self, agent_def: dict[str, Any]) -> Agent: + """Create a Agent from a dictionary definition. This method does the following things: @@ -405,13 +398,13 @@ class AgentFactory: 2. Validates that the loaded object is a PromptAgent. 3. Creates the appropriate ChatClient based on the model provider and apiType. 4. Parses the tools, options, and response format from the PromptAgent. - 5. Creates and returns a ChatAgent instance with the configured properties. + 5. Creates and returns a Agent instance with the configured properties. Args: agent_def: Dictionary representation of a PromptAgent. Returns: - The `ChatAgent` instance created from the dictionary. + The `Agent` instance created from the dictionary. Raises: DeclarativeLoaderError: If the dictionary does not represent a PromptAgent. @@ -454,16 +447,16 @@ class AgentFactory: if output_schema := prompt_agent.outputSchema: chat_options["response_format"] = _create_model_from_json_schema("agent", output_schema.to_json_schema()) # Step 3: Create the agent instance - return ChatAgent( - chat_client=client, + return Agent( + client=client, name=prompt_agent.name, description=prompt_agent.description, instructions=prompt_agent.instructions, **chat_options, ) - async def create_agent_from_yaml_path_async(self, yaml_path: str | Path) -> ChatAgent: - """Async version: Create a ChatAgent from a YAML file path. + async def create_agent_from_yaml_path_async(self, yaml_path: str | Path) -> Agent: + """Async version: Create a Agent from a YAML file path. Use this method when the provider requires async initialization, such as AzureAI.ProjectProvider which creates agents on the Azure AI Agent Service. @@ -472,7 +465,7 @@ class AgentFactory: yaml_path: Path to the YAML file representation of a PromptAgent. Returns: - The ``ChatAgent`` instance created from the YAML file. + The ``Agent`` instance created from the YAML file. Examples: .. code-block:: python @@ -492,8 +485,8 @@ class AgentFactory: yaml_str = yaml_path.read_text() return await self.create_agent_from_yaml_async(yaml_str) - async def create_agent_from_yaml_async(self, yaml_str: str) -> ChatAgent: - """Async version: Create a ChatAgent from a YAML string. + async def create_agent_from_yaml_async(self, yaml_str: str) -> Agent: + """Async version: Create a Agent from a YAML string. Use this method when the provider requires async initialization, such as AzureAI.ProjectProvider which creates agents on the Azure AI Agent Service. @@ -502,7 +495,7 @@ class AgentFactory: yaml_str: YAML string representation of a PromptAgent. Returns: - The ``ChatAgent`` instance created from the YAML string. + The ``Agent`` instance created from the YAML string. Examples: .. code-block:: python @@ -523,8 +516,8 @@ class AgentFactory: """ return await self.create_agent_from_dict_async(yaml.safe_load(yaml_str)) - async def create_agent_from_dict_async(self, agent_def: dict[str, Any]) -> ChatAgent: - """Async version: Create a ChatAgent from a dictionary definition. + async def create_agent_from_dict_async(self, agent_def: dict[str, Any]) -> Agent: + """Async version: Create a Agent from a dictionary definition. Use this method when the provider requires async initialization, such as AzureAI.ProjectProvider which creates agents on the Azure AI Agent Service. @@ -533,7 +526,7 @@ class AgentFactory: agent_def: Dictionary representation of a PromptAgent. Returns: - The ``ChatAgent`` instance created from the dictionary. + The ``Agent`` instance created from the dictionary. Examples: .. code-block:: python @@ -571,20 +564,20 @@ class AgentFactory: chat_options["tools"] = tools if output_schema := prompt_agent.outputSchema: chat_options["response_format"] = _create_model_from_json_schema("agent", output_schema.to_json_schema()) - return ChatAgent( - chat_client=client, + return Agent( + client=client, name=prompt_agent.name, description=prompt_agent.description, instructions=prompt_agent.instructions, **chat_options, ) - async def _create_agent_with_provider(self, prompt_agent: PromptAgent, mapping: ProviderTypeMapping) -> ChatAgent: - """Create a ChatAgent using AzureAIProjectAgentProvider. + async def _create_agent_with_provider(self, prompt_agent: PromptAgent, mapping: ProviderTypeMapping) -> Agent: + """Create a Agent using AzureAIProjectAgentProvider. This method handles the special case where we use a provider that creates agents on a remote service (like Azure AI Agent Service) and returns - ChatAgent instances directly. + Agent instances directly. """ # Import the provider class module_name = mapping["package"] @@ -618,9 +611,9 @@ class AgentFactory: response_format = _create_model_from_json_schema("agent", prompt_agent.outputSchema.to_json_schema()) # Create the agent using the provider - # The provider's create_agent returns a ChatAgent directly + # The provider's create_agent returns a Agent directly return cast( - ChatAgent, + Agent, await provider.create_agent( name=prompt_agent.name, model=prompt_agent.model.id if prompt_agent.model else None, @@ -631,12 +624,12 @@ class AgentFactory: ), ) - def _get_client(self, prompt_agent: PromptAgent) -> ChatClientProtocol: - """Create the ChatClientProtocol instance based on the PromptAgent model.""" + def _get_client(self, prompt_agent: PromptAgent) -> SupportsChatGetResponse: + """Create the SupportsChatGetResponse instance based on the PromptAgent model.""" if not prompt_agent.model: - # if no model is defined, use the supplied chat_client - if self.chat_client: - return self.chat_client + # if no model is defined, use the supplied client + if self.client: + return self.client raise DeclarativeLoaderError( "ChatClient must be provided to create agent from PromptAgent, " "alternatively define a model in the PromptAgent." @@ -670,9 +663,9 @@ class AgentFactory: # Any client we create, needs a model.id if not prompt_agent.model.id: - # if prompt_agent.model is defined, but no id, use the supplied chat_client - if self.chat_client: - return self.chat_client + # if prompt_agent.model is defined, but no id, use the supplied client + if self.client: + return self.client # or raise, since we cannot create a client without model id raise DeclarativeLoaderError( "ChatClient must be provided to create agent from PromptAgent, or define model.id in the PromptAgent." @@ -714,14 +707,14 @@ class AgentFactory: chat_options["additional_chat_options"] = options.additionalProperties return chat_options - def _parse_tools(self, tools: list[Tool] | None) -> list[ToolProtocol] | None: - """Parse tool resources into ToolProtocol instances.""" + def _parse_tools(self, tools: list[Tool] | None) -> list[AFFunctionTool | dict[str, Any]] | None: + """Parse tool resources into AFFunctionTool instances or dict-based tools.""" if not tools: return None return [self._parse_tool(tool_resource) for tool_resource in tools] - def _parse_tool(self, tool_resource: Tool) -> ToolProtocol: - """Parse a single tool resource into a ToolProtocol instance.""" + def _parse_tool(self, tool_resource: Tool) -> AFFunctionTool | dict[str, Any]: + """Parse a single tool resource into an AFFunctionTool instance.""" match tool_resource: case FunctionTool(): func: Callable[..., Any] | None = None @@ -736,88 +729,81 @@ class AgentFactory: func=func, ) case WebSearchTool(): - return HostedWebSearchTool( - description=tool_resource.description, additional_properties=tool_resource.options - ) + result: dict[str, Any] = {"type": "web_search_preview"} + if tool_resource.description: + result["description"] = tool_resource.description + if tool_resource.options: + result.update(tool_resource.options) + return result case FileSearchTool(): - add_props: dict[str, Any] = {} + result = { + "type": "file_search", + "vector_store_ids": tool_resource.vectorStoreIds or [], + } + if tool_resource.maximumResultCount is not None: + result["max_num_results"] = tool_resource.maximumResultCount + if tool_resource.description: + result["description"] = tool_resource.description if tool_resource.ranker is not None: - add_props["ranker"] = tool_resource.ranker + result["ranker"] = tool_resource.ranker if tool_resource.scoreThreshold is not None: - add_props["score_threshold"] = tool_resource.scoreThreshold + result["score_threshold"] = tool_resource.scoreThreshold if tool_resource.filters: - add_props["filters"] = tool_resource.filters - return HostedFileSearchTool( - inputs=[Content.from_hosted_vector_store(id) for id in tool_resource.vectorStoreIds or []], - description=tool_resource.description, - max_results=tool_resource.maximumResultCount, - additional_properties=add_props, - ) + result["filters"] = tool_resource.filters + return result case CodeInterpreterTool(): - return HostedCodeInterpreterTool( - inputs=[Content.from_hosted_file(file_id=file) for file in tool_resource.fileIds or []], - description=tool_resource.description, - ) + result = {"type": "code_interpreter"} + if tool_resource.fileIds: + result["file_ids"] = tool_resource.fileIds + if tool_resource.description: + result["description"] = tool_resource.description + return result case McpTool(): - approval_mode: HostedMCPSpecificApproval | Literal["always_require", "never_require"] | None = None + result = { + "type": "mcp", + "server_label": tool_resource.name.replace(" ", "_") if tool_resource.name else "", + "server_url": str(tool_resource.url) if tool_resource.url else "", + } + if tool_resource.description: + result["server_description"] = tool_resource.description + if tool_resource.allowedTools: + result["allowed_tools"] = list(tool_resource.allowedTools) + + # Handle approval mode if tool_resource.approvalMode is not None: if tool_resource.approvalMode.kind == "always": - approval_mode = "always_require" + result["require_approval"] = "always" elif tool_resource.approvalMode.kind == "never": - approval_mode = "never_require" + result["require_approval"] = "never" elif isinstance(tool_resource.approvalMode, McpServerToolSpecifyApprovalMode): - approval_mode = {} + approval_config: dict[str, Any] = {} if tool_resource.approvalMode.alwaysRequireApprovalTools: - approval_mode["always_require_approval"] = ( - tool_resource.approvalMode.alwaysRequireApprovalTools - ) + approval_config["always"] = { + "tool_names": list(tool_resource.approvalMode.alwaysRequireApprovalTools) + } if tool_resource.approvalMode.neverRequireApprovalTools: - approval_mode["never_require_approval"] = ( - tool_resource.approvalMode.neverRequireApprovalTools - ) - if not approval_mode: - approval_mode = None + approval_config["never"] = { + "tool_names": list(tool_resource.approvalMode.neverRequireApprovalTools) + } + if approval_config: + result["require_approval"] = approval_config # Handle connection settings - headers: dict[str, str] | None = None - additional_properties: dict[str, Any] | None = None - if tool_resource.connection is not None: match tool_resource.connection: case ApiKeyConnection(): if tool_resource.connection.apiKey: - headers = {"Authorization": f"Bearer {tool_resource.connection.apiKey}"} + result["headers"] = {"Authorization": f"Bearer {tool_resource.connection.apiKey}"} case RemoteConnection(): - additional_properties = { - "connection": { - "kind": tool_resource.connection.kind, - "name": tool_resource.connection.name, - "authenticationMode": tool_resource.connection.authenticationMode, - "endpoint": tool_resource.connection.endpoint, - } - } + result["project_connection_id"] = tool_resource.connection.name case ReferenceConnection(): - additional_properties = { - "connection": { - "kind": tool_resource.connection.kind, - "name": tool_resource.connection.name, - "authenticationMode": tool_resource.connection.authenticationMode, - } - } + result["project_connection_id"] = tool_resource.connection.name case AnonymousConnection(): pass case _: raise ValueError(f"Unsupported connection kind: {tool_resource.connection.kind}") - return HostedMCPTool( - name=tool_resource.name, # type: ignore - description=tool_resource.description, - url=tool_resource.url, # type: ignore - allowed_tools=tool_resource.allowedTools, - approval_mode=approval_mode, - headers=headers, - additional_properties=additional_properties, - ) + return result case _: raise ValueError(f"Unsupported tool kind: {tool_resource.kind}") diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_actions_agents.py b/python/packages/declarative/agent_framework_declarative/_workflows/_actions_agents.py index 1a49f9b89d..b7c05b8607 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_actions_agents.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_actions_agents.py @@ -14,7 +14,7 @@ from collections.abc import AsyncGenerator from typing import Any, cast from agent_framework import get_logger -from agent_framework._types import AgentResponse, ChatMessage +from agent_framework._types import AgentResponse, Message from ._handlers import ( ActionContext, @@ -162,7 +162,7 @@ def _extract_json_from_response(text: str) -> Any: raise json.JSONDecodeError("No valid JSON found in response", text, 0) -def _build_messages_from_state(ctx: ActionContext) -> list[ChatMessage]: +def _build_messages_from_state(ctx: ActionContext) -> list[Message]: """Build the message list to send to an agent. This collects messages from: @@ -174,9 +174,9 @@ def _build_messages_from_state(ctx: ActionContext) -> list[ChatMessage]: ctx: The action context Returns: - List of ChatMessage objects to send to the agent + List of Message objects to send to the agent """ - messages: list[ChatMessage] = [] + messages: list[Message] = [] # Get conversation history history = ctx.state.get("conversation.messages", []) @@ -287,23 +287,23 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl evaluated_input = ctx.state.eval_if_expression(input_messages) if evaluated_input: if isinstance(evaluated_input, str): - messages.append(ChatMessage(role="user", text=evaluated_input)) + messages.append(Message(role="user", text=evaluated_input)) elif isinstance(evaluated_input, list): for msg_item in evaluated_input: # type: ignore if isinstance(msg_item, str): - messages.append(ChatMessage(role="user", text=msg_item)) - elif isinstance(msg_item, ChatMessage): + messages.append(Message(role="user", text=msg_item)) + elif isinstance(msg_item, Message): messages.append(msg_item) elif isinstance(msg_item, dict) and "content" in msg_item: item_dict = cast(dict[str, Any], msg_item) role: str = str(item_dict.get("role", "user")) content: str = str(item_dict.get("content", "")) if role == "user": - messages.append(ChatMessage(role="user", text=content)) + messages.append(Message(role="user", text=content)) elif role == "assistant": - messages.append(ChatMessage(role="assistant", text=content)) + messages.append(Message(role="assistant", text=content)) elif role == "system": - messages.append(ChatMessage(role="system", text=content)) + messages.append(Message(role="system", text=content)) # Evaluate and include input arguments evaluated_args: dict[str, Any] = {} @@ -365,7 +365,7 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl # Add to conversation history if text: - ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text)) + ctx.state.add_conversation_message(Message(role="assistant", text=text)) # Store in output variables (.NET style) if output_messages_var: @@ -418,7 +418,7 @@ async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[Workfl # Add to conversation history if text: - ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text)) + ctx.state.add_conversation_message(Message(role="assistant", text=text)) # Store in output variables (.NET style) if output_messages_var: @@ -564,8 +564,8 @@ async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[Workf # Add input as user message if provided if input_value: if isinstance(input_value, str): - messages.append(ChatMessage(role="user", text=input_value)) - elif isinstance(input_value, ChatMessage): + messages.append(Message(role="user", text=input_value)) + elif isinstance(input_value, Message): messages.append(input_value) logger.debug(f"InvokePromptAgent: calling '{agent_name}' with {len(messages)} messages") @@ -594,7 +594,7 @@ async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[Workf ctx.state.set_agent_result(text=text, messages=response_messages) if text: - ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text)) + ctx.state.add_conversation_message(Message(role="assistant", text=text)) if output_path: ctx.state.set(output_path, text) @@ -614,7 +614,7 @@ async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[Workf ctx.state.set_agent_result(text=text, messages=response_messages) if text: - ctx.state.add_conversation_message(ChatMessage(role="assistant", text=text)) + ctx.state.add_conversation_message(Message(role="assistant", text=text)) if output_path: ctx.state.set(output_path, text) diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py index 11b6868ad1..9bb868135b 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py @@ -102,7 +102,7 @@ def _make_powerfx_safe(value: Any) -> Any: """Convert a value to a PowerFx-serializable form. PowerFx can only serialize primitive types, dicts, and lists. - Custom objects (like ChatMessage) must be converted to dicts or excluded. + Custom objects (like Message) must be converted to dicts or excluded. Args: value: Any Python value @@ -558,8 +558,8 @@ class DeclarativeWorkflowState: # Try "text" key first (simple dict format) if "text" in last_msg: return str(last_msg["text"]) - # Try extracting from "contents" (ChatMessage dict format) - # ChatMessage.text concatenates text from all TextContent items + # Try extracting from "contents" (Message dict format) + # Message.text concatenates text from all TextContent items contents = last_msg.get("contents", []) if isinstance(contents, list): text_parts = [] diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py index 1f1069c02c..c4f9ecff59 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py @@ -148,33 +148,26 @@ class DeclarativeWorkflowBuilder: if self._validate: self._validate_workflow(actions) - # Use a placeholder for start_executor; it will be overwritten below via _set_start_executor + # Create a stable entry node as the start executor, then wire it to the first action. + # This avoids needing a placeholder since the entry executor isn't known until after + # _create_executors_for_actions runs (which itself needs the builder to add edges). + entry_node = JoinExecutor({"kind": "Entry"}, id="_workflow_entry") + self._executors[entry_node.id] = entry_node builder = WorkflowBuilder( - start_executor="_declarative_placeholder", + start_executor=entry_node, name=self._workflow_id, checkpoint_storage=self._checkpoint_storage, ) - # First pass: create all executors - entry_executor = self._create_executors_for_actions(actions, builder) + # Create all executors and wire sequential edges + first_executor = self._create_executors_for_actions(actions, builder) - # Set the entry point - if entry_executor: - # Check if entry is a control flow structure (If/Switch) - if getattr(entry_executor, "_is_if_structure", False) or getattr( - entry_executor, "_is_switch_structure", False - ): - # Create an entry passthrough node and wire to the structure's branches - entry_node = JoinExecutor({"kind": "Entry"}, id="_workflow_entry") - self._executors[entry_node.id] = entry_node - builder._set_start_executor(entry_node) - # Use _add_sequential_edge which knows how to wire to structures - self._add_sequential_edge(builder, entry_node, entry_executor) - else: - builder._set_start_executor(entry_executor) - else: + if not first_executor: raise ValueError("Failed to create any executors from actions.") + # Wire entry node to the first action (handles both regular and control flow targets) + self._add_sequential_edge(builder, entry_node, first_executor) + # Resolve pending gotos (back-edges for loops, forward-edges for jumps) self._resolve_pending_gotos(builder) @@ -223,9 +216,11 @@ class DeclarativeWorkflowBuilder: for action_def in actions: kind = action_def.get("kind", "") - # Check for duplicate explicit IDs + # Check for duplicate or reserved explicit IDs explicit_id = action_def.get("id") if explicit_id: + if explicit_id == "_workflow_entry": + raise ValueError(f"Action ID '{explicit_id}' is reserved for internal use. Choose a different ID.") if explicit_id in seen_ids: raise ValueError(f"Duplicate action ID '{explicit_id}'. Action IDs must be unique.") seen_ids.add(explicit_id) diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py index d4300a9909..f28d283e60 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py @@ -20,8 +20,8 @@ from dataclasses import dataclass, field from typing import Any, cast from agent_framework import ( - ChatMessage, Content, + Message, WorkflowContext, handler, response_handler, @@ -170,7 +170,7 @@ def _extract_json_from_response(text: str) -> Any: raise json.JSONDecodeError("No valid JSON found in response", text, 0) -def _validate_conversation_history(messages: list[ChatMessage], agent_name: str) -> None: +def _validate_conversation_history(messages: list[Message], agent_name: str) -> None: """Validate that conversation history has matching tool calls and results. This helps catch issues where tool call messages are stored without their @@ -263,7 +263,7 @@ class AgentResult: success: bool response: str agent_name: str - messages: list[ChatMessage] = field(default_factory=lambda: cast(list[ChatMessage], [])) + messages: list[Message] = field(default_factory=lambda: cast(list[Message], [])) tool_calls: list[Content] = field(default_factory=lambda: cast(list[Content], [])) error: str | None = None @@ -309,7 +309,7 @@ class AgentExternalInputRequest: agent_name: str agent_response: str iteration: int = 0 - messages: list[ChatMessage] = field(default_factory=lambda: cast(list[ChatMessage], [])) + messages: list[Message] = field(default_factory=lambda: cast(list[Message], [])) function_calls: list[Content] = field(default_factory=lambda: cast(list[Content], [])) @@ -340,7 +340,7 @@ class AgentExternalInputResponse: """ user_input: str - messages: list[ChatMessage] = field(default_factory=lambda: cast(list[ChatMessage], [])) + messages: list[Message] = field(default_factory=lambda: cast(list[Message], [])) function_results: dict[str, Content] = field(default_factory=lambda: cast(dict[str, Content], {})) @@ -637,20 +637,20 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor): Tuple of (accumulated_response, all_messages, tool_calls) """ accumulated_response = "" - all_messages: list[ChatMessage] = [] + all_messages: list[Message] = [] tool_calls: list[Content] = [] # Add user input to conversation history first (via state.append only) if input_text: - user_message = ChatMessage(role="user", text=input_text) + user_message = Message(role="user", text=input_text) state.append(messages_path, user_message) # Get conversation history from state AFTER adding user message # Note: We get a fresh copy to avoid mutation issues - conversation_history: list[ChatMessage] = state.get(messages_path) or [] + conversation_history: list[Message] = state.get(messages_path) or [] # Build messages list for agent (use history if available, otherwise just input) - messages_for_agent: list[ChatMessage] | str = conversation_history if conversation_history else input_text + messages_for_agent: list[Message] | str = conversation_history if conversation_history else input_text # Validate conversation history before invoking agent if isinstance(messages_for_agent, list) and messages_for_agent: @@ -672,7 +672,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor): if not isinstance(result, str): result_messages: Any = getattr(result, "messages", None) if result_messages is not None: - all_messages = list(cast(list[ChatMessage], result_messages)) + all_messages = list(cast(list[Message], result_messages)) result_tool_calls: Any = getattr(result, "tool_calls", None) if result_tool_calls is not None: tool_calls = list(cast(list[Content], result_tool_calls)) @@ -707,7 +707,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor): "Agent '%s': No messages in response, creating simple assistant message", agent_name, ) - assistant_message = ChatMessage(role="assistant", text=accumulated_response) + assistant_message = Message(role="assistant", text=accumulated_response) state.append(messages_path, assistant_message) # Store results in state - support both schema formats: diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py b/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py index c3cfff1d21..576ef73ac6 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py @@ -73,8 +73,8 @@ class WorkflowFactory: from agent_framework.declarative import WorkflowFactory # Pre-register agents for InvokeAzureAgent actions - chat_client = AzureOpenAIChatClient() - agent = chat_client.as_agent(name="MyAgent", instructions="You are helpful.") + client = AzureOpenAIChatClient() + agent = client.as_agent(name="MyAgent", instructions="You are helpful.") factory = WorkflowFactory(agents={"MyAgent": agent}) workflow = factory.create_workflow_from_yaml_path("workflow.yaml") @@ -517,7 +517,7 @@ class WorkflowFactory: Args: name: The name to register the agent under. Must match the agent name referenced in InvokeAzureAgent actions. - agent: The agent instance (typically a ChatAgent or similar). + agent: The agent instance (typically a Agent or similar). Returns: Self for method chaining. diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_state.py b/python/packages/declarative/agent_framework_declarative/_workflows/_state.py index 7d1f9e4945..31ad4124da 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_state.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_state.py @@ -314,7 +314,7 @@ class WorkflowState: """Add a message to the conversation history. Args: - message: The message to add (typically a ChatMessage or similar) + message: The message to add (typically a Message or similar) """ self._conversation["messages"].append(message) self._conversation["history"].append(message) diff --git a/python/packages/declarative/pyproject.toml b/python/packages/declarative/pyproject.toml index d8dcfa2f5f..93d5c806c0 100644 --- a/python/packages/declarative/pyproject.toml +++ b/python/packages/declarative/pyproject.toml @@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,7 +22,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "powerfx>=0.0.31; python_version < '3.14'", "pyyaml>=6.0,<7.0", ] diff --git a/python/packages/declarative/tests/test_declarative_loader.py b/python/packages/declarative/tests/test_declarative_loader.py index 2d31a66d58..8b70634aeb 100644 --- a/python/packages/declarative/tests/test_declarative_loader.py +++ b/python/packages/declarative/tests/test_declarative_loader.py @@ -483,7 +483,7 @@ class TestAgentFactoryCreateFromDict: mock_client = MagicMock() mock_client.create_agent.return_value = MagicMock() - factory = AgentFactory(chat_client=mock_client) + factory = AgentFactory(client=mock_client) agent = factory.create_agent_from_dict(agent_def) assert agent is not None @@ -512,7 +512,7 @@ instructions: You are a helpful assistant. mock_client = MagicMock() mock_client.create_agent.return_value = MagicMock() - factory = AgentFactory(chat_client=mock_client) + factory = AgentFactory(client=mock_client) # Create from YAML string agent_from_yaml = factory.create_agent_from_yaml(yaml_content) @@ -540,7 +540,7 @@ instructions: You are a helpful assistant. factory.create_agent_from_dict(agent_def) def test_create_agent_from_dict_without_model_or_client_raises(self): - """Test that missing both model and chat_client raises DeclarativeLoaderError.""" + """Test that missing both model and client raises DeclarativeLoaderError.""" from agent_framework_declarative import AgentFactory from agent_framework_declarative._loader import DeclarativeLoaderError @@ -592,7 +592,7 @@ instructions: Hello world monkeypatch.setenv("TEST_DESCRIPTION", "Description from env") # With safe_mode=True (default), Env access should fail and return original value - factory = AgentFactory(chat_client=mock_client, safe_mode=True) + factory = AgentFactory(client=mock_client, safe_mode=True) agent = factory.create_agent_from_yaml(yaml_content) # The description should NOT be resolved from env (PowerFx fails, returns original) @@ -618,7 +618,7 @@ instructions: Hello world """ # With safe_mode=False, Env access should work - factory = AgentFactory(chat_client=mock_client, safe_mode=False) + factory = AgentFactory(client=mock_client, safe_mode=False) agent = factory.create_agent_from_yaml(yaml_content) # The description should be resolved from env @@ -698,11 +698,9 @@ class TestAgentFactoryMcpToolConnection: """Tests for MCP tool connection handling in AgentFactory._parse_tool.""" def _get_mcp_tools(self, agent): - """Helper to get MCP tools from agent's default_options.""" - from agent_framework import HostedMCPTool - + """Helper to get MCP dict tools from agent's default_options.""" tools = agent.default_options.get("tools", []) - return [t for t in tools if isinstance(t, HostedMCPTool)] + return [t for t in tools if isinstance(t, dict) and t.get("type") == "mcp"] def test_mcp_tool_with_api_key_connection_sets_headers(self): """Test that MCP tool with ApiKeyConnection sets headers correctly.""" @@ -726,7 +724,7 @@ tools: mock_client = MagicMock() mock_client.create_agent.return_value = MagicMock() - factory = AgentFactory(chat_client=mock_client) + factory = AgentFactory(client=mock_client) agent = factory.create_agent_from_yaml(yaml_content) # Find the MCP tool in the agent's tools @@ -735,11 +733,11 @@ tools: mcp_tool = mcp_tools[0] # Verify headers are set with the API key - assert mcp_tool.headers is not None - assert mcp_tool.headers == {"Authorization": "Bearer my-secret-api-key"} + assert mcp_tool.get("headers") is not None + assert mcp_tool.get("headers") == {"Authorization": "Bearer my-secret-api-key"} def test_mcp_tool_with_remote_connection_sets_additional_properties(self): - """Test that MCP tool with RemoteConnection sets additional_properties correctly.""" + """Test that MCP tool with RemoteConnection sets project_connection_id correctly.""" from unittest.mock import MagicMock from agent_framework_declarative import AgentFactory @@ -761,7 +759,7 @@ tools: mock_client = MagicMock() mock_client.create_agent.return_value = MagicMock() - factory = AgentFactory(chat_client=mock_client) + factory = AgentFactory(client=mock_client) agent = factory.create_agent_from_yaml(yaml_content) # Find the MCP tool in the agent's tools @@ -769,16 +767,11 @@ tools: assert len(mcp_tools) == 1 mcp_tool = mcp_tools[0] - # Verify additional_properties are set with connection info - assert mcp_tool.additional_properties is not None - assert "connection" in mcp_tool.additional_properties - conn = mcp_tool.additional_properties["connection"] - assert conn["kind"] == "remote" - assert conn["authenticationMode"] == "oauth" - assert conn["name"] == "github-mcp-oauth-connection" + # Verify project_connection_id is set from connection name + assert mcp_tool.get("project_connection_id") == "github-mcp-oauth-connection" def test_mcp_tool_with_reference_connection_sets_additional_properties(self): - """Test that MCP tool with ReferenceConnection sets additional_properties correctly.""" + """Test that MCP tool with ReferenceConnection sets project_connection_id correctly.""" from unittest.mock import MagicMock from agent_framework_declarative import AgentFactory @@ -800,7 +793,7 @@ tools: mock_client = MagicMock() mock_client.create_agent.return_value = MagicMock() - factory = AgentFactory(chat_client=mock_client) + factory = AgentFactory(client=mock_client) agent = factory.create_agent_from_yaml(yaml_content) # Find the MCP tool in the agent's tools @@ -808,15 +801,11 @@ tools: assert len(mcp_tools) == 1 mcp_tool = mcp_tools[0] - # Verify additional_properties are set with connection info - assert mcp_tool.additional_properties is not None - assert "connection" in mcp_tool.additional_properties - conn = mcp_tool.additional_properties["connection"] - assert conn["kind"] == "reference" - assert conn["name"] == "my-connection-ref" + # Verify project_connection_id is set from connection name + assert mcp_tool.get("project_connection_id") == "my-connection-ref" def test_mcp_tool_with_anonymous_connection_no_headers_or_properties(self): - """Test that MCP tool with AnonymousConnection doesn't set headers or additional_properties.""" + """Test that MCP tool with AnonymousConnection doesn't set headers or project_connection_id.""" from unittest.mock import MagicMock from agent_framework_declarative import AgentFactory @@ -836,7 +825,7 @@ tools: mock_client = MagicMock() mock_client.create_agent.return_value = MagicMock() - factory = AgentFactory(chat_client=mock_client) + factory = AgentFactory(client=mock_client) agent = factory.create_agent_from_yaml(yaml_content) # Find the MCP tool in the agent's tools @@ -844,9 +833,9 @@ tools: assert len(mcp_tools) == 1 mcp_tool = mcp_tools[0] - # Verify no headers or additional_properties are set - assert mcp_tool.headers is None - assert mcp_tool.additional_properties is None + # Verify no headers or project_connection_id are set + assert mcp_tool.get("headers") is None + assert mcp_tool.get("project_connection_id") is None def test_mcp_tool_without_connection_preserves_existing_behavior(self): """Test that MCP tool without connection works as before (no headers or additional_properties).""" @@ -868,7 +857,7 @@ tools: mock_client = MagicMock() mock_client.create_agent.return_value = MagicMock() - factory = AgentFactory(chat_client=mock_client) + factory = AgentFactory(client=mock_client) agent = factory.create_agent_from_yaml(yaml_content) # Find the MCP tool in the agent's tools @@ -877,14 +866,13 @@ tools: mcp_tool = mcp_tools[0] # Verify tool is created correctly without connection - assert mcp_tool.name == "simple-mcp-tool" - assert str(mcp_tool.url) == "https://api.example.com/mcp" - assert mcp_tool.approval_mode == "never_require" - assert mcp_tool.headers is None - assert mcp_tool.additional_properties is None + assert mcp_tool["server_label"] == "simple-mcp-tool" + assert mcp_tool["server_url"] == "https://api.example.com/mcp" + assert mcp_tool.get("require_approval") == "never" + assert mcp_tool.get("headers") is None def test_mcp_tool_with_remote_connection_with_endpoint(self): - """Test that MCP tool with RemoteConnection including endpoint sets it in additional_properties.""" + """Test that MCP tool with RemoteConnection including endpoint sets project_connection_id.""" from unittest.mock import MagicMock from agent_framework_declarative import AgentFactory @@ -907,7 +895,7 @@ tools: mock_client = MagicMock() mock_client.create_agent.return_value = MagicMock() - factory = AgentFactory(chat_client=mock_client) + factory = AgentFactory(client=mock_client) agent = factory.create_agent_from_yaml(yaml_content) # Find the MCP tool in the agent's tools @@ -915,7 +903,5 @@ tools: assert len(mcp_tools) == 1 mcp_tool = mcp_tools[0] - # Verify additional_properties include endpoint - assert mcp_tool.additional_properties is not None - conn = mcp_tool.additional_properties["connection"] - assert conn["endpoint"] == "https://auth.example.com" + # Verify project_connection_id is set from connection name + assert mcp_tool.get("project_connection_id") == "my-oauth-connection" diff --git a/python/packages/declarative/tests/test_graph_coverage.py b/python/packages/declarative/tests/test_graph_coverage.py index fd01faf2a4..cf622f6467 100644 --- a/python/packages/declarative/tests/test_graph_coverage.py +++ b/python/packages/declarative/tests/test_graph_coverage.py @@ -1835,8 +1835,8 @@ class TestAgentExternalLoopCoverage: } executor = InvokeAzureAgentExecutor(action_def, agents={"TestAgent": mock_agent}) - # Mock the internal method to avoid storing ChatMessage objects in state - # (PowerFx cannot serialize ChatMessage) + # Mock the internal method to avoid storing Message objects in state + # (PowerFx cannot serialize Message) with patch.object( executor, "_invoke_agent_and_store_results", @@ -2012,7 +2012,9 @@ class TestBuilderControlFlowCreation: # Create builder with minimal yaml definition yaml_def = {"name": "test_workflow", "actions": []} graph_builder = DeclarativeWorkflowBuilder(yaml_def) - wb = WorkflowBuilder(start_executor="dummy") + from agent_framework_declarative._workflows._executors_control_flow import JoinExecutor + + wb = WorkflowBuilder(start_executor=JoinExecutor({"kind": "Dummy"}, id="dummy")) action_def = { "kind": "GotoAction", @@ -2036,7 +2038,9 @@ class TestBuilderControlFlowCreation: yaml_def = {"name": "test_workflow", "actions": []} graph_builder = DeclarativeWorkflowBuilder(yaml_def) - wb = WorkflowBuilder(start_executor="dummy") + from agent_framework_declarative._workflows._executors_control_flow import JoinExecutor + + wb = WorkflowBuilder(start_executor=JoinExecutor({"kind": "Dummy"}, id="dummy")) action_def = { "kind": "GotoAction", @@ -2056,7 +2060,9 @@ class TestBuilderControlFlowCreation: yaml_def = {"name": "test_workflow", "actions": []} graph_builder = DeclarativeWorkflowBuilder(yaml_def) - wb = WorkflowBuilder(start_executor="dummy") + from agent_framework_declarative._workflows._executors_control_flow import JoinExecutor + + wb = WorkflowBuilder(start_executor=JoinExecutor({"kind": "Dummy"}, id="dummy")) action_def = { "kind": "GotoAction", @@ -2094,7 +2100,9 @@ class TestBuilderControlFlowCreation: yaml_def = {"name": "test_workflow", "actions": []} graph_builder = DeclarativeWorkflowBuilder(yaml_def) - wb = WorkflowBuilder(start_executor="dummy") + from agent_framework_declarative._workflows._executors_control_flow import JoinExecutor + + wb = WorkflowBuilder(start_executor=JoinExecutor({"kind": "Dummy"}, id="dummy")) # Create a mock loop_next executor loop_next = ForeachNextExecutor( @@ -2124,7 +2132,9 @@ class TestBuilderControlFlowCreation: yaml_def = {"name": "test_workflow", "actions": []} graph_builder = DeclarativeWorkflowBuilder(yaml_def) - wb = WorkflowBuilder(start_executor="dummy") + from agent_framework_declarative._workflows._executors_control_flow import JoinExecutor + + wb = WorkflowBuilder(start_executor=JoinExecutor({"kind": "Dummy"}, id="dummy")) action_def = { "kind": "BreakLoop", @@ -2149,7 +2159,9 @@ class TestBuilderControlFlowCreation: yaml_def = {"name": "test_workflow", "actions": []} graph_builder = DeclarativeWorkflowBuilder(yaml_def) - wb = WorkflowBuilder(start_executor="dummy") + from agent_framework_declarative._workflows._executors_control_flow import JoinExecutor + + wb = WorkflowBuilder(start_executor=JoinExecutor({"kind": "Dummy"}, id="dummy")) # Create a mock loop_next executor loop_next = ForeachNextExecutor( @@ -2179,7 +2191,9 @@ class TestBuilderControlFlowCreation: yaml_def = {"name": "test_workflow", "actions": []} graph_builder = DeclarativeWorkflowBuilder(yaml_def) - wb = WorkflowBuilder(start_executor="dummy") + from agent_framework_declarative._workflows._executors_control_flow import JoinExecutor + + wb = WorkflowBuilder(start_executor=JoinExecutor({"kind": "Dummy"}, id="dummy")) action_def = { "kind": "ContinueLoop", @@ -2203,7 +2217,9 @@ class TestBuilderEdgeWiring: yaml_def = {"name": "test_workflow", "actions": []} graph_builder = DeclarativeWorkflowBuilder(yaml_def) - wb = WorkflowBuilder(start_executor="dummy") + from agent_framework_declarative._workflows._executors_control_flow import JoinExecutor + + wb = WorkflowBuilder(start_executor=JoinExecutor({"kind": "Dummy"}, id="dummy")) # Create a mock source executor source = SendActivityExecutor({"kind": "SendActivity", "activity": {"text": "test"}}, id="source") @@ -2236,7 +2252,9 @@ class TestBuilderEdgeWiring: yaml_def = {"name": "test_workflow", "actions": []} graph_builder = DeclarativeWorkflowBuilder(yaml_def) - wb = WorkflowBuilder(start_executor="dummy") + from agent_framework_declarative._workflows._executors_control_flow import JoinExecutor + + wb = WorkflowBuilder(start_executor=JoinExecutor({"kind": "Dummy"}, id="dummy")) source = SendActivityExecutor({"kind": "SendActivity", "activity": {"text": "source"}}, id="source") target = SendActivityExecutor({"kind": "SendActivity", "activity": {"text": "target"}}, id="target") diff --git a/python/packages/declarative/tests/test_graph_workflow_integration.py b/python/packages/declarative/tests/test_graph_workflow_integration.py index 00ee3a154f..b7bd08e60a 100644 --- a/python/packages/declarative/tests/test_graph_workflow_integration.py +++ b/python/packages/declarative/tests/test_graph_workflow_integration.py @@ -223,11 +223,11 @@ class TestGraphWorkflowCheckpointing: builder = DeclarativeWorkflowBuilder(yaml_def) _workflow = builder.build() # noqa: F841 - # Verify multiple executors were created + # Verify multiple executors were created (+ _workflow_entry node) assert "step1" in builder._executors assert "step2" in builder._executors assert "step3" in builder._executors - assert len(builder._executors) == 3 + assert len(builder._executors) == 4 def test_workflow_executor_connectivity(self): """Test that executors are properly connected in sequence.""" @@ -243,8 +243,8 @@ class TestGraphWorkflowCheckpointing: builder = DeclarativeWorkflowBuilder(yaml_def) workflow = builder.build() - # Verify all executors exist - assert len(builder._executors) == 3 + # Verify all executors exist (+ _workflow_entry node) + assert len(builder._executors) == 4 # Verify the workflow can be inspected assert workflow is not None diff --git a/python/packages/devui/AGENTS.md b/python/packages/devui/AGENTS.md index c478c11e2d..5213095244 100644 --- a/python/packages/devui/AGENTS.md +++ b/python/packages/devui/AGENTS.md @@ -20,7 +20,7 @@ Interactive developer UI for testing and debugging agents and workflows. ```python from agent_framework.devui import serve -agent = ChatAgent(...) +agent = Agent(...) serve(entities=[agent], port=8080, auto_open=True) ``` diff --git a/python/packages/devui/README.md b/python/packages/devui/README.md index f984c56799..6c3745236a 100644 --- a/python/packages/devui/README.md +++ b/python/packages/devui/README.md @@ -17,7 +17,7 @@ pip install agent-framework-devui --pre You can also launch it programmatically ```python -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient from agent_framework.devui import serve @@ -26,9 +26,9 @@ def get_weather(location: str) -> str: return f"Weather in {location}: 72°F and sunny" # Create your agent -agent = ChatAgent( +agent = Agent( name="WeatherAgent", - chat_client=OpenAIChatClient(), + client=OpenAIChatClient(), tools=[get_weather] ) @@ -55,8 +55,8 @@ When DevUI starts with no discovered entities, it displays a **sample entity gal ```python # ✅ Correct - DevUI handles cleanup automatically -mcp_tool = MCPStreamableHTTPTool(url="http://localhost:8011/mcp", chat_client=chat_client) -agent = ChatAgent(tools=mcp_tool) +mcp_tool = MCPStreamableHTTPTool(url="http://localhost:8011/mcp", client=client) +agent = Agent(tools=mcp_tool) serve(entities=[agent]) ``` @@ -68,13 +68,13 @@ Register cleanup hooks to properly close credentials and resources on shutdown: ```python from azure.identity.aio import DefaultAzureCredential -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureOpenAIChatClient from agent_framework_devui import register_cleanup, serve credential = DefaultAzureCredential() client = AzureOpenAIChatClient() -agent = ChatAgent(name="MyAgent", chat_client=client) +agent = Agent(name="MyAgent", client=client) # Register cleanup hook - credential will be closed on shutdown register_cleanup(agent, credential.close) @@ -92,7 +92,7 @@ For your agents to be discovered by the DevUI, they must be organized in a direc ``` agents/ ├── weather_agent/ -│ ├── __init__.py # Must export: agent = ChatAgent(...) +│ ├── __init__.py # Must export: agent = Agent(...) │ ├── agent.py │ └── .env # Optional: API keys, config vars ├── my_workflow/ diff --git a/python/packages/devui/agent_framework_devui/__init__.py b/python/packages/devui/agent_framework_devui/__init__.py index 50010cd9cd..f703e85a63 100644 --- a/python/packages/devui/agent_framework_devui/__init__.py +++ b/python/packages/devui/agent_framework_devui/__init__.py @@ -41,7 +41,7 @@ def register_cleanup(entity: Any, *hooks: Callable[[], Any]) -> None: Single cleanup hook: >>> from agent_framework.devui import serve, register_cleanup >>> credential = DefaultAzureCredential() - >>> agent = ChatAgent(...) + >>> agent = Agent(...) >>> register_cleanup(agent, credential.close) >>> serve(entities=[agent]) @@ -52,7 +52,7 @@ def register_cleanup(entity: Any, *hooks: Callable[[], Any]) -> None: >>> # In agents/my_agent/agent.py >>> from agent_framework.devui import register_cleanup >>> credential = DefaultAzureCredential() - >>> agent = ChatAgent(...) + >>> agent = Agent(...) >>> register_cleanup(agent, credential.close) >>> # Run: devui ./agents """ diff --git a/python/packages/devui/agent_framework_devui/_conversations.py b/python/packages/devui/agent_framework_devui/_conversations.py index 6b271ddff5..2ea28f6e6a 100644 --- a/python/packages/devui/agent_framework_devui/_conversations.py +++ b/python/packages/devui/agent_framework_devui/_conversations.py @@ -13,11 +13,11 @@ import uuid from abc import ABC, abstractmethod from typing import Any, Literal, cast -from agent_framework import AgentThread, ChatMessage +from agent_framework import AgentThread, Message from agent_framework._workflows._checkpoint import InMemoryCheckpointStorage from openai.types.conversations import Conversation, ConversationDeletedResource from openai.types.conversations.conversation_item import ConversationItem -from openai.types.conversations.message import Message +from openai.types.conversations.message import Message as OpenAIMessage from openai.types.conversations.text_content import TextContent from openai.types.responses import ( ResponseFunctionToolCallItem, @@ -305,7 +305,7 @@ class InMemoryConversationStore(ConversationStore): content = item.get("content", []) text = content[0].get("text", "") if content else "" - chat_msg = ChatMessage(role=role, text=text) # type: ignore[arg-type] + chat_msg = Message(role=role, text=text) # type: ignore[arg-type] chat_messages.append(chat_msg) # Add messages to AgentThread @@ -320,7 +320,7 @@ class InMemoryConversationStore(ConversationStore): role_str = msg.role if hasattr(msg.role, "value") else str(msg.role) role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles - # Convert ChatMessage contents to OpenAI TextContent format + # Convert Message contents to OpenAI TextContent format message_content = [] for content_item in msg.contents: if content_item.type == "text": @@ -329,7 +329,7 @@ class InMemoryConversationStore(ConversationStore): message_content.append(TextContent(type="text", text=text_value)) # Create Message object (concrete type from ConversationItem union) - message = Message( + message = OpenAIMessage( id=item_id, type="message", # Required discriminator for union role=role, @@ -372,14 +372,14 @@ class InMemoryConversationStore(ConversationStore): if thread.message_store: af_messages = await thread.message_store.list_messages() - # Convert each AgentFramework ChatMessage to appropriate ConversationItem type(s) + # Convert each AgentFramework Message to appropriate ConversationItem type(s) for i, msg in enumerate(af_messages): item_id = f"item_{i}" role_str = msg.role if hasattr(msg.role, "value") else str(msg.role) role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles # Process each content item in the message - # A single ChatMessage may produce multiple ConversationItems + # A single Message may produce multiple ConversationItems # (e.g., a message with both text and a function call) message_contents: list[TextContent | ResponseInputImage | ResponseInputFile] = [] function_calls = [] @@ -464,7 +464,7 @@ class InMemoryConversationStore(ConversationStore): # Create ConversationItems based on what we found # If message has text/images/files, create a Message item if message_contents: - message = Message( + message = OpenAIMessage( id=item_id, type="message", role=role, # type: ignore diff --git a/python/packages/devui/agent_framework_devui/_discovery.py b/python/packages/devui/agent_framework_devui/_discovery.py index 8058d31083..a5fada1ba9 100644 --- a/python/packages/devui/agent_framework_devui/_discovery.py +++ b/python/packages/devui/agent_framework_devui/_discovery.py @@ -541,7 +541,7 @@ class EntityDiscovery: """Check if a Python file has entity exports (agent or workflow) using AST parsing. This safely checks for module-level assignments like: - - agent = ChatAgent(...) + - agent = Agent(...) - workflow = WorkflowBuilder(start_executor=...)... Args: diff --git a/python/packages/devui/agent_framework_devui/_executor.py b/python/packages/devui/agent_framework_devui/_executor.py index c70c123983..b55a57cf44 100644 --- a/python/packages/devui/agent_framework_devui/_executor.py +++ b/python/packages/devui/agent_framework_devui/_executor.py @@ -305,7 +305,7 @@ class AgentFrameworkExecutor: yield AgentStartedEvent() - # Convert input to proper ChatMessage or string + # Convert input to proper Message or string user_message = self._convert_input_to_chat_message(request.input) # Get thread from conversation parameter (OpenAI standard!) @@ -321,7 +321,7 @@ class AgentFrameworkExecutor: if isinstance(user_message, str): logger.debug(f"Executing agent with text input: {user_message[:100]}...") else: - logger.debug(f"Executing agent with multimodal ChatMessage: {type(user_message)}") + logger.debug(f"Executing agent with multimodal Message: {type(user_message)}") # Workaround for MCP tool stale connection bug (GitHub issue pending) # When HTTP streaming ends, GeneratorExit can close MCP stdio streams @@ -534,7 +534,7 @@ class AgentFrameworkExecutor: yield {"type": "error", "message": f"Workflow execution error: {e!s}"} def _convert_input_to_chat_message(self, input_data: Any) -> Any: - """Convert OpenAI Responses API input to Agent Framework ChatMessage or string. + """Convert OpenAI Responses API input to Agent Framework Message or string. Handles various input formats including text, images, files, and multimodal content. Falls back to string extraction for simple cases. @@ -543,11 +543,11 @@ class AgentFrameworkExecutor: input_data: OpenAI ResponseInputParam (List[ResponseInputItemParam]) Returns: - ChatMessage for multimodal content, or string for simple text + Message for multimodal content, or string for simple text """ # Import Agent Framework types try: - from agent_framework import ChatMessage, Role + from agent_framework import Message, Role except ImportError: # Fallback to string extraction if Agent Framework not available return self._extract_user_message_fallback(input_data) @@ -558,24 +558,24 @@ class AgentFrameworkExecutor: # Handle OpenAI ResponseInputParam (List[ResponseInputItemParam]) if isinstance(input_data, list): - return self._convert_openai_input_to_chat_message(input_data, ChatMessage, Role) + return self._convert_openai_input_to_chat_message(input_data, Message, Role) # Fallback for other formats return self._extract_user_message_fallback(input_data) - def _convert_openai_input_to_chat_message(self, input_items: list[Any], ChatMessage: Any, Role: Any) -> Any: - """Convert OpenAI ResponseInputParam to Agent Framework ChatMessage. + def _convert_openai_input_to_chat_message(self, input_items: list[Any], Message: Any, Role: Any) -> Any: + """Convert OpenAI ResponseInputParam to Agent Framework Message. Processes text, images, files, and other content types from OpenAI format - to Agent Framework ChatMessage with appropriate content objects. + to Agent Framework Message with appropriate content objects. Args: input_items: List of OpenAI ResponseInputItemParam objects (dicts or objects) - ChatMessage: ChatMessage class for creating chat messages + Message: Message class for creating chat messages Role: Role enum for message roles Returns: - ChatMessage with converted content + Message with converted content """ contents: list[Content] = [] @@ -705,9 +705,9 @@ class AgentFrameworkExecutor: if not contents: contents.append(Content.from_text(text="")) - chat_message = ChatMessage(role="user", contents=contents) + chat_message = Message(role="user", contents=contents) - logger.info(f"Created ChatMessage with {len(contents)} contents:") + logger.info(f"Created Message with {len(contents)} contents:") for idx, content in enumerate(contents): content_type = content.__class__.__name__ if hasattr(content, "media_type"): @@ -772,9 +772,9 @@ class AgentFrameworkExecutor: pass # Check for OpenAI multimodal format (list with type: "message") - # This handles ChatMessage inputs with images, files, etc. + # This handles Message inputs with images, files, etc. if self._is_openai_multimodal_format(raw_input): - logger.debug("Detected OpenAI multimodal format, converting to ChatMessage") + logger.debug("Detected OpenAI multimodal format, converting to Message") return self._convert_input_to_chat_message(raw_input) # Handle structured input (dict) diff --git a/python/packages/devui/agent_framework_devui/_mapper.py b/python/packages/devui/agent_framework_devui/_mapper.py index cb2ecacdd0..bcb99634cb 100644 --- a/python/packages/devui/agent_framework_devui/_mapper.py +++ b/python/packages/devui/agent_framework_devui/_mapper.py @@ -14,7 +14,7 @@ from datetime import datetime from typing import Any, Union from uuid import uuid4 -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from openai.types.responses import ( Response, ResponseContentPartAddedEvent, @@ -453,7 +453,7 @@ class MessageMapper: Handles: - Primitives (str, int, float, bool, None) - Collections (list, tuple, set, dict) - - SerializationMixin objects (ChatMessage, etc.) - calls to_dict() + - SerializationMixin objects (Message, etc.) - calls to_dict() - Pydantic models - calls model_dump() - Dataclasses - recursively serializes with asdict() - Enums - extracts value @@ -502,7 +502,7 @@ class MessageMapper: if isinstance(value, dict): return {k: self._serialize_value(v) for k, v in value.items()} - # Handle SerializationMixin (like ChatMessage) - call to_dict() + # Handle SerializationMixin (like Message) - call to_dict() if hasattr(value, "to_dict") and callable(getattr(value, "to_dict", None)): try: return value.to_dict() # type: ignore[attr-defined, no-any-return] @@ -536,7 +536,7 @@ class MessageMapper: def _serialize_request_data(self, request_data: Any) -> dict[str, Any]: """Serialize RequestInfoMessage to dict for JSON transmission. - Handles nested SerializationMixin objects (like ChatMessage) within dataclasses. + Handles nested SerializationMixin objects (like Message) within dataclasses. Args: request_data: The RequestInfoMessage instance @@ -554,7 +554,7 @@ class MessageMapper: return {k: self._serialize_value(v) for k, v in request_data.items()} # Handle dataclasses with nested SerializationMixin objects - # We can't use asdict() directly because it doesn't handle ChatMessage + # We can't use asdict() directly because it doesn't handle Message if is_dataclass(request_data) and not isinstance(request_data, type): try: # Manually serialize each field to handle nested SerializationMixin @@ -892,17 +892,17 @@ class MessageMapper: # Extract text from output data based on type text = None - if isinstance(output_data, ChatMessage): - # Handle ChatMessage (from Magentic and AgentExecutor with output_response=True) + if isinstance(output_data, Message): + # Handle Message (from Magentic and AgentExecutor with output_response=True) text = getattr(output_data, "text", None) if not text: # Fallback to string representation text = str(output_data) elif isinstance(output_data, list): - # Handle list of ChatMessage objects (from Magentic yield_output([final_answer])) + # Handle list of Message objects (from Magentic yield_output([final_answer])) text_parts = [] for item in output_data: - if isinstance(item, ChatMessage): + if isinstance(item, Message): item_text = getattr(item, "text", None) if item_text: text_parts.append(item_text) @@ -1047,7 +1047,7 @@ class MessageMapper: # Create ExecutorActionItem with completed status # executor_completed event (type='executor_completed') uses 'data' field, not 'result' # Serialize the result data to ensure it's JSON-serializable - # (AgentExecutorResponse contains AgentResponse/ChatMessage which are SerializationMixin) + # (AgentExecutorResponse contains AgentResponse/Message which are SerializationMixin) raw_result = getattr(event, "data", None) serialized_result = self._serialize_value(raw_result) if raw_result is not None else None executor_item = ExecutorActionItem( diff --git a/python/packages/devui/agent_framework_devui/_server.py b/python/packages/devui/agent_framework_devui/_server.py index 1045c82923..374bab9962 100644 --- a/python/packages/devui/agent_framework_devui/_server.py +++ b/python/packages/devui/agent_framework_devui/_server.py @@ -222,8 +222,8 @@ class DevServer: # Step 2: Close chat clients and their credentials (EXISTING) entity_obj = self.executor.entity_discovery.get_entity_object(entity_id) - if entity_obj and hasattr(entity_obj, "chat_client"): - client = entity_obj.chat_client + if entity_obj and hasattr(entity_obj, "client"): + client = entity_obj.client # Close the chat client itself if hasattr(client, "close") and callable(client.close): diff --git a/python/packages/devui/agent_framework_devui/_utils.py b/python/packages/devui/agent_framework_devui/_utils.py index b715263075..66886b8ea7 100644 --- a/python/packages/devui/agent_framework_devui/_utils.py +++ b/python/packages/devui/agent_framework_devui/_utils.py @@ -9,7 +9,7 @@ from dataclasses import fields, is_dataclass from types import UnionType from typing import Any, Union, get_args, get_origin, get_type_hints -from agent_framework import ChatMessage +from agent_framework import Message logger = logging.getLogger(__name__) @@ -45,7 +45,7 @@ def extract_agent_metadata(entity_object: Any) -> dict[str, Any]: elif hasattr(chat_opts, "instructions"): metadata["instructions"] = chat_opts.instructions - # Try to get model - check both default_options and chat_client + # Try to get model - check both default_options and client if hasattr(entity_object, "default_options"): chat_opts = entity_object.default_options if isinstance(chat_opts, dict): @@ -53,16 +53,12 @@ def extract_agent_metadata(entity_object: Any) -> dict[str, Any]: metadata["model"] = chat_opts.get("model_id") elif hasattr(chat_opts, "model_id") and chat_opts.model_id: metadata["model"] = chat_opts.model_id - if ( - metadata["model"] is None - and hasattr(entity_object, "chat_client") - and hasattr(entity_object.chat_client, "model_id") - ): - metadata["model"] = entity_object.chat_client.model_id + if metadata["model"] is None and hasattr(entity_object, "client") and hasattr(entity_object.client, "model_id"): + metadata["model"] = entity_object.client.model_id # Try to get chat client type - if hasattr(entity_object, "chat_client"): - metadata["chat_client_type"] = entity_object.chat_client.__class__.__name__ + if hasattr(entity_object, "client"): + metadata["chat_client_type"] = entity_object.client.__class__.__name__ # Try to get context providers if ( @@ -124,8 +120,8 @@ def extract_executor_message_types(executor: Any) -> list[Any]: def _contains_chat_message(type_hint: Any) -> bool: - """Check whether the provided type hint directly or indirectly references ChatMessage.""" - if type_hint is ChatMessage: + """Check whether the provided type hint directly or indirectly references Message.""" + if type_hint is Message: return True origin = get_origin(type_hint) @@ -141,7 +137,7 @@ def _contains_chat_message(type_hint: Any) -> bool: def select_primary_input_type(message_types: list[Any]) -> Any | None: """Choose the most user-friendly input type for workflow inputs. - Prefers ChatMessage (or containers thereof) and then falls back to primitives. + Prefers Message (or containers thereof) and then falls back to primitives. Args: message_types: List of possible message types @@ -154,7 +150,7 @@ def select_primary_input_type(message_types: list[Any]) -> Any | None: for message_type in message_types: if _contains_chat_message(message_type): - return ChatMessage + return Message preferred = (str, dict) @@ -427,7 +423,7 @@ def generate_input_schema(input_type: type) -> dict[str, Any]: if hasattr(input_type, "model_json_schema"): return input_type.model_json_schema() # type: ignore - # 3. SerializationMixin classes (ChatMessage, etc.) + # 3. SerializationMixin classes (Message, etc.) if is_serialization_mixin(input_type): return generate_schema_from_serialization_mixin(input_type) @@ -521,7 +517,7 @@ def _parse_string_input(input_str: str, target_type: type) -> Any: except Exception as e: logger.debug(f"Failed to parse string as Pydantic model: {e}") - # SerializationMixin (like ChatMessage) + # SerializationMixin (like Message) if is_serialization_mixin(target_type): try: # Try parsing as JSON dict first @@ -531,7 +527,7 @@ def _parse_string_input(input_str: str, target_type: type) -> Any: return target_type.from_dict(data) # type: ignore return target_type(**data) # type: ignore - # For ChatMessage specifically: create from text + # For Message specifically: create from text # Try common field patterns common_fields = ["text", "message", "content"] sig = inspect.signature(target_type) diff --git a/python/packages/devui/agent_framework_devui/ui/assets/index.js b/python/packages/devui/agent_framework_devui/ui/assets/index.js index 276af33633..c75e2f635b 100644 --- a/python/packages/devui/agent_framework_devui/ui/assets/index.js +++ b/python/packages/devui/agent_framework_devui/ui/assets/index.js @@ -453,7 +453,7 @@ Error generating stack: `+i.message+` and value of this only key should be a state object. Example: { "type": "__setState", "state": { "abc123Store": { "foo": "bar" } } } `);const A=_.state[f];if(A==null)return;JSON.stringify(l.getState())!==JSON.stringify(A)&&b(A);return}l.dispatchFromDevtools&&typeof l.dispatch=="function"&&l.dispatch(_)});case"DISPATCH":switch(N.payload.type){case"RESET":return b(j),f===void 0?g?.init(l.getState()):g?.init(au(m.name));case"COMMIT":if(f===void 0){g?.init(l.getState());return}return g?.init(au(m.name));case"ROLLBACK":return bh(N.state,_=>{if(f===void 0){b(_),g?.init(l.getState());return}b(_[f]),g?.init(au(m.name))});case"JUMP_TO_STATE":case"JUMP_TO_ACTION":return bh(N.state,_=>{if(f===void 0){b(_);return}JSON.stringify(l.getState())!==JSON.stringify(_[f])&&b(_[f])});case"IMPORT_STATE":{const{nextLiftedState:_}=N.payload,A=(S=_.computedStates.slice(-1)[0])==null?void 0:S.state;if(!A)return;b(f===void 0?A:A[f]),g?.send(null,_);return}case"PAUSE_RECORDING":return y=!y}return}}),j},m5=f5,bh=(e,n)=>{let r;try{r=JSON.parse(e)}catch(a){console.error("[zustand devtools middleware] Could not parse the received json",a)}r!==void 0&&n(r)};function h5(e,n){let r;try{r=e()}catch{return}return{getItem:l=>{var c;const d=m=>m===null?null:JSON.parse(m,void 0),f=(c=r.getItem(l))!=null?c:null;return f instanceof Promise?f.then(d):d(f)},setItem:(l,c)=>r.setItem(l,JSON.stringify(c,void 0)),removeItem:l=>r.removeItem(l)}}const ep=e=>n=>{try{const r=e(n);return r instanceof Promise?r:{then(a){return ep(a)(r)},catch(a){return this}}}catch(r){return{then(a){return this},catch(a){return ep(a)(r)}}}},p5=(e,n)=>(r,a,l)=>{let c={storage:h5(()=>localStorage),partialize:N=>N,version:0,merge:(N,S)=>({...S,...N}),...n},d=!1;const f=new Set,m=new Set;let h=c.storage;if(!h)return e((...N)=>{console.warn(`[zustand persist middleware] Unable to update item '${c.name}', the given storage is currently unavailable.`),r(...N)},a,l);const g=()=>{const N=c.partialize({...a()});return h.setItem(c.name,{state:N,version:c.version})},x=l.setState;l.setState=(N,S)=>(x(N,S),g());const y=e((...N)=>(r(...N),g()),a,l);l.getInitialState=()=>y;let b;const j=()=>{var N,S;if(!h)return;d=!1,f.forEach(A=>{var E;return A((E=a())!=null?E:y)});const _=((S=c.onRehydrateStorage)==null?void 0:S.call(c,(N=a())!=null?N:y))||void 0;return ep(h.getItem.bind(h))(c.name).then(A=>{if(A)if(typeof A.version=="number"&&A.version!==c.version){if(c.migrate){const E=c.migrate(A.state,A.version);return E instanceof Promise?E.then(M=>[!0,M]):[!0,E]}console.error("State loaded from storage couldn't be migrated since no migrate function was provided")}else return[!1,A.state];return[!1,void 0]}).then(A=>{var E;const[M,T]=A;if(b=c.merge(T,(E=a())!=null?E:y),r(b,!0),M)return g()}).then(()=>{_?.(b,void 0),b=a(),d=!0,m.forEach(A=>A(b))}).catch(A=>{_?.(void 0,A)})};return l.persist={setOptions:N=>{c={...c,...N},N.storage&&(h=N.storage)},clearStorage:()=>{h?.removeItem(c.name)},getOptions:()=>c,rehydrate:()=>j(),hasHydrated:()=>d,onHydrate:N=>(f.add(N),()=>{f.delete(N)}),onFinishHydration:N=>(m.add(N),()=>{m.delete(N)})},c.skipHydration||j(),b||y},g5=p5,le=l5()(m5(g5(e=>({agents:[],workflows:[],entities:[],selectedAgent:void 0,isLoadingEntities:!0,entityError:null,currentConversation:void 0,availableConversations:[],chatItems:[],isStreaming:!1,isSubmitting:!1,loadingConversations:!1,inputValue:"",attachments:[],conversationUsage:{total_tokens:0,message_count:0},pendingApprovals:[],currentSession:void 0,availableSessions:[],sessionCheckpoints:[],loadingSessions:!1,loadingCheckpoints:!1,showDebugPanel:!0,debugPanelMinimized:!1,debugPanelWidth:320,debugEvents:[],isResizing:!1,showToolCalls:!0,streamingEnabled:!0,debugPanelTab:"events",debugTraceSubTab:"spans",contextInspectorViewMode:"tokens",contextInspectorCumulative:!1,showAboutModal:!1,showGallery:!1,showDeployModal:!1,showEntityNotFoundToast:!1,toasts:[],oaiMode:{enabled:!1,model:"gpt-4o-mini"},uiMode:"developer",runtime:"python",serverCapabilities:{instrumentation:!1,openai_proxy:!1,deployment:!1},authRequired:!1,serverVersion:null,isDeploying:!1,deploymentLogs:[],lastDeployment:null,azureDeploymentEnabled:!1,setAgents:n=>e({agents:n}),setWorkflows:n=>e({workflows:n}),setEntities:n=>e({entities:n}),setSelectedAgent:n=>e({selectedAgent:n}),addAgent:n=>e(r=>({agents:[...r.agents,n]})),addWorkflow:n=>e(r=>({workflows:[...r.workflows,n]})),updateAgent:n=>e(r=>({agents:r.agents.map(a=>a.id===n.id?n:a),selectedAgent:r.selectedAgent?.id===n.id&&r.selectedAgent.type==="agent"?n:r.selectedAgent})),updateWorkflow:n=>e(r=>({workflows:r.workflows.map(a=>a.id===n.id?n:a),selectedAgent:r.selectedAgent?.id===n.id&&r.selectedAgent.type==="workflow"?n:r.selectedAgent})),removeEntity:n=>e(r=>({agents:r.agents.filter(a=>a.id!==n),workflows:r.workflows.filter(a=>a.id!==n),selectedAgent:r.selectedAgent?.id===n?void 0:r.selectedAgent})),setEntityError:n=>e({entityError:n}),setIsLoadingEntities:n=>e({isLoadingEntities:n}),setCurrentConversation:n=>e({currentConversation:n}),setAvailableConversations:n=>e({availableConversations:n}),setChatItems:n=>e({chatItems:n}),setIsStreaming:n=>e({isStreaming:n}),setIsSubmitting:n=>e({isSubmitting:n}),setLoadingConversations:n=>e({loadingConversations:n}),setInputValue:n=>e({inputValue:n}),setAttachments:n=>e({attachments:n}),updateConversationUsage:n=>e(r=>({conversationUsage:{total_tokens:r.conversationUsage.total_tokens+n,message_count:r.conversationUsage.message_count+1}})),setPendingApprovals:n=>e({pendingApprovals:n}),setCurrentSession:n=>e({currentSession:n}),setAvailableSessions:n=>e({availableSessions:n}),setSessionCheckpoints:n=>e({sessionCheckpoints:n}),setLoadingSessions:n=>e({loadingSessions:n}),setLoadingCheckpoints:n=>e({loadingCheckpoints:n}),addSession:n=>e(r=>({availableSessions:[n,...r.availableSessions]})),removeSession:n=>e(r=>({availableSessions:r.availableSessions.filter(a=>a.conversation_id!==n),currentSession:r.currentSession?.conversation_id===n?void 0:r.currentSession,sessionCheckpoints:r.currentSession?.conversation_id===n?[]:r.sessionCheckpoints})),setShowDebugPanel:n=>e({showDebugPanel:n}),setDebugPanelMinimized:n=>e({debugPanelMinimized:n}),setDebugPanelWidth:n=>e({debugPanelWidth:n}),setShowToolCalls:n=>e({showToolCalls:n}),setStreamingEnabled:n=>e({streamingEnabled:n}),addDebugEvent:n=>e(r=>{const a=Math.floor(Date.now()/1e3),c=(r.debugEvents.length>0?r.debugEvents[r.debugEvents.length-1]:null)?._uiTimestamp??0,d=Math.max(a,c+1);return{debugEvents:[...r.debugEvents,{...n,_uiTimestamp:"created_at"in n&&n.created_at?n.created_at:d}]}}),clearDebugEvents:()=>e({debugEvents:[]}),setIsResizing:n=>e({isResizing:n}),setDebugPanelTab:n=>e({debugPanelTab:n}),setDebugTraceSubTab:n=>e({debugTraceSubTab:n}),setContextInspectorViewMode:n=>e({contextInspectorViewMode:n}),setContextInspectorCumulative:n=>e({contextInspectorCumulative:n}),setShowAboutModal:n=>e({showAboutModal:n}),setShowGallery:n=>e({showGallery:n}),setShowDeployModal:n=>e({showDeployModal:n}),setShowEntityNotFoundToast:n=>e({showEntityNotFoundToast:n}),addToast:n=>e(r=>({toasts:[...r.toasts,{id:`toast-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,type:n.type||"info",duration:n.duration||4e3,...n}]})),removeToast:n=>e(r=>({toasts:r.toasts.filter(a=>a.id!==n)})),setOAIMode:n=>e(r=>n.enabled&&!r.oaiMode.enabled?(Object.keys(localStorage).forEach(a=>{a.startsWith("devui_convs_")&&localStorage.removeItem(a)}),{oaiMode:n,currentConversation:void 0,availableConversations:[],chatItems:[],inputValue:"",attachments:[],conversationUsage:{total_tokens:0,message_count:0},isStreaming:!1,isSubmitting:!1,pendingApprovals:[],debugEvents:[]}):!n.enabled&&r.oaiMode.enabled?(Object.keys(localStorage).forEach(a=>{a.startsWith("devui_convs_")&&localStorage.removeItem(a)}),{oaiMode:n,currentConversation:void 0,availableConversations:[],chatItems:[],inputValue:"",attachments:[],conversationUsage:{total_tokens:0,message_count:0},isStreaming:!1,isSubmitting:!1,pendingApprovals:[],debugEvents:[]}):{oaiMode:n}),toggleOAIMode:()=>e(n=>{const r=!n.oaiMode.enabled;return{oaiMode:{...n.oaiMode,enabled:r},currentConversation:void 0,availableConversations:[],chatItems:[],inputValue:"",attachments:[],conversationUsage:{total_tokens:0,message_count:0},isStreaming:!1,isSubmitting:!1,pendingApprovals:[],debugEvents:[]}}),setServerMeta:n=>e({uiMode:n.uiMode,runtime:n.runtime,serverCapabilities:n.capabilities,authRequired:n.authRequired,serverVersion:n.version||null}),startDeployment:()=>e({isDeploying:!0,deploymentLogs:[],lastDeployment:null}),addDeploymentLog:n=>e(r=>({deploymentLogs:[...r.deploymentLogs,n]})),setDeploymentResult:n=>e({isDeploying:!1,lastDeployment:n}),stopDeployment:()=>e({isDeploying:!1}),clearDeploymentState:()=>e({isDeploying:!1,deploymentLogs:[],lastDeployment:null}),setAzureDeploymentEnabled:n=>e({azureDeploymentEnabled:n}),selectEntity:n=>{e({selectedAgent:n,currentConversation:void 0,availableConversations:[],chatItems:[],inputValue:"",attachments:[],conversationUsage:{total_tokens:0,message_count:0},isStreaming:!1,isSubmitting:!1,pendingApprovals:[],currentSession:void 0,availableSessions:[],sessionCheckpoints:[],debugEvents:[]});const r=new URL(window.location.href);r.searchParams.set("entity_id",n.id),window.history.pushState({},"",r)}}),{name:"devui-storage",partialize:e=>({showDebugPanel:e.showDebugPanel,debugPanelMinimized:e.debugPanelMinimized,debugPanelWidth:e.debugPanelWidth,showToolCalls:e.showToolCalls,streamingEnabled:e.streamingEnabled,oaiMode:e.oaiMode,azureDeploymentEnabled:e.azureDeploymentEnabled,debugPanelTab:e.debugPanelTab,debugTraceSubTab:e.debugTraceSubTab,contextInspectorViewMode:e.contextInspectorViewMode,contextInspectorCumulative:e.contextInspectorCumulative})}),{name:"DevUI Store"})),wu=Object.freeze(Object.defineProperty({__proto__:null,useDevUIStore:le},Symbol.toStringTag,{value:"Module"}));function ab({agents:e,workflows:n,entities:r,selectedItem:a,onSelect:l,onBrowseGallery:c,isLoading:d=!1,onSettingsClick:f}){const{oaiMode:m,serverVersion:h}=le();return o.jsxs("header",{className:"flex h-14 items-center gap-4 border-b px-4",children:[o.jsxs("div",{className:"flex items-center gap-2 font-semibold",children:[o.jsxs("svg",{width:"24",height:"24",viewBox:"0 0 805 805",fill:"none",xmlns:"http://www.w3.org/2000/svg",className:"flex-shrink-0",children:[o.jsx("path",{d:"M402.488 119.713C439.197 119.713 468.955 149.472 468.955 186.18C468.955 192.086 471.708 197.849 476.915 200.635L546.702 237.977C555.862 242.879 566.95 240.96 576.092 236.023C585.476 230.955 596.218 228.078 607.632 228.078C644.341 228.078 674.098 257.836 674.099 294.545C674.099 316.95 663.013 336.765 646.028 348.806C637.861 354.595 631.412 363.24 631.412 373.251V430.818C631.412 440.83 637.861 449.475 646.028 455.264C663.013 467.305 674.099 487.121 674.099 509.526C674.099 546.235 644.341 575.994 607.632 575.994C598.598 575.994 589.985 574.191 582.133 570.926C573.644 567.397 563.91 566.393 555.804 570.731L469.581 616.867C469.193 617.074 468.955 617.479 468.955 617.919C468.955 654.628 439.197 684.386 402.488 684.386C365.779 684.386 336.021 654.628 336.021 617.919C336.021 616.802 335.423 615.765 334.439 615.238L249.895 570C241.61 565.567 231.646 566.713 223.034 570.472C214.898 574.024 205.914 575.994 196.47 575.994C159.761 575.994 130.002 546.235 130.002 509.526C130.002 486.66 141.549 466.49 159.13 454.531C167.604 448.766 174.349 439.975 174.349 429.726V372.538C174.349 362.289 167.604 353.498 159.13 347.734C141.549 335.774 130.002 315.604 130.002 292.738C130.002 256.029 159.761 226.271 196.47 226.271C208.223 226.271 219.263 229.322 228.843 234.674C238.065 239.827 249.351 241.894 258.666 236.91L328.655 199.459C333.448 196.895 336.021 191.616 336.021 186.18C336.021 149.471 365.779 119.713 402.488 119.713ZM475.716 394.444C471.337 396.787 468.955 401.586 468.955 406.552C468.955 429.68 457.142 450.048 439.221 461.954C430.571 467.7 423.653 476.574 423.653 486.959V537.511C423.653 547.896 430.746 556.851 439.379 562.622C449 569.053 461.434 572.052 471.637 566.592L527.264 536.826C536.887 531.677 541.164 520.44 541.164 509.526C541.164 485.968 553.42 465.272 571.904 453.468C580.846 447.757 588.054 438.749 588.054 428.139V371.427C588.054 363.494 582.671 356.676 575.716 352.862C569.342 349.366 561.663 348.454 555.253 351.884L475.716 394.444ZM247.992 349.841C241.997 346.633 234.806 347.465 228.873 350.785C222.524 354.337 217.706 360.639 217.706 367.915V429.162C217.706 439.537 224.611 448.404 233.248 454.152C251.144 466.062 262.937 486.417 262.937 509.526C262.937 519.654 267.026 529.991 275.955 534.769L334.852 566.284C344.582 571.49 356.362 568.81 365.528 562.667C373.735 557.166 380.296 548.643 380.296 538.764V486.305C380.296 476.067 373.564 467.282 365.103 461.516C347.548 449.552 336.021 429.398 336.021 406.552C336.021 400.967 333.389 395.536 328.465 392.902L247.992 349.841ZM270.019 280.008C265.421 282.469 262.936 287.522 262.937 292.738C262.937 293.308 262.929 293.876 262.915 294.443C262.615 306.354 266.961 318.871 277.466 324.492L334.017 354.751C344.13 360.163 356.442 357.269 366.027 350.969C376.495 344.088 389.024 340.085 402.488 340.085C416.203 340.085 428.947 344.239 439.532 351.357C449.163 357.834 461.63 360.861 471.864 355.385L526.625 326.083C537.106 320.474 541.458 307.999 541.182 296.115C541.17 295.593 541.164 295.069 541.164 294.545C541.164 288.551 538.376 282.696 533.091 279.868L463.562 242.664C454.384 237.753 443.274 239.688 434.123 244.65C424.716 249.75 413.941 252.647 402.488 252.647C390.83 252.647 379.873 249.646 370.348 244.373C361.148 239.281 349.917 237.256 340.646 242.217L270.019 280.008Z",fill:"url(#paint0_linear_510_1294)"}),o.jsx("defs",{children:o.jsxs("linearGradient",{id:"paint0_linear_510_1294",x1:"255.628",y1:"-34.3245",x2:"618.483",y2:"632.032",gradientUnits:"userSpaceOnUse",children:[o.jsx("stop",{stopColor:"#D59FFF"}),o.jsx("stop",{offset:"1",stopColor:"#8562C5"})]})})]}),"Dev UI",h&&o.jsxs("span",{className:"text-xs text-muted-foreground ml-1",children:["v",h]}),m.enabled&&o.jsxs(ut,{variant:"secondary",className:"gap-1 ml-2",children:[o.jsx(og,{className:"h-3 w-3"}),"OpenAI: ",m.model]})]}),!m.enabled&&o.jsx(YM,{agents:e,workflows:n,entities:r,selectedItem:a,onSelect:l,onBrowseGallery:c,isLoading:d}),o.jsx("div",{className:"flex-1"}),o.jsxs("div",{className:"flex items-center gap-2 ml-auto",children:[o.jsx(s5,{}),o.jsx(Le,{variant:"ghost",size:"sm",onClick:g=>{g.stopPropagation(),f?.()},children:o.jsx(Jh,{className:"h-4 w-4"})})]})]})}function tp(e,[n,r]){return Math.min(r,Math.max(n,e))}function x5(e,n){return w.useReducer((r,a)=>n[r][a]??r,e)}var ig="ScrollArea",[dN,W7]=Kn(ig),[y5,$n]=dN(ig),fN=w.forwardRef((e,n)=>{const{__scopeScrollArea:r,type:a="hover",dir:l,scrollHideDelay:c=600,...d}=e,[f,m]=w.useState(null),[h,g]=w.useState(null),[x,y]=w.useState(null),[b,j]=w.useState(null),[N,S]=w.useState(null),[_,A]=w.useState(0),[E,M]=w.useState(0),[T,D]=w.useState(!1),[z,H]=w.useState(!1),q=rt(n,W=>m(W)),X=jl(l);return o.jsx(y5,{scope:r,type:a,dir:X,scrollHideDelay:c,scrollArea:f,viewport:h,onViewportChange:g,content:x,onContentChange:y,scrollbarX:b,onScrollbarXChange:j,scrollbarXEnabled:T,onScrollbarXEnabledChange:D,scrollbarY:N,onScrollbarYChange:S,scrollbarYEnabled:z,onScrollbarYEnabledChange:H,onCornerWidthChange:A,onCornerHeightChange:M,children:o.jsx(Ye.div,{dir:X,...d,ref:q,style:{position:"relative","--radix-scroll-area-corner-width":_+"px","--radix-scroll-area-corner-height":E+"px",...e.style}})})});fN.displayName=ig;var mN="ScrollAreaViewport",hN=w.forwardRef((e,n)=>{const{__scopeScrollArea:r,children:a,nonce:l,...c}=e,d=$n(mN,r),f=w.useRef(null),m=rt(n,f,d.onViewportChange);return o.jsxs(o.Fragment,{children:[o.jsx("style",{dangerouslySetInnerHTML:{__html:"[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}"},nonce:l}),o.jsx(Ye.div,{"data-radix-scroll-area-viewport":"",...c,ref:m,style:{overflowX:d.scrollbarXEnabled?"scroll":"hidden",overflowY:d.scrollbarYEnabled?"scroll":"hidden",...e.style},children:o.jsx("div",{ref:d.onContentChange,style:{minWidth:"100%",display:"table"},children:a})})]})});hN.displayName=mN;var xs="ScrollAreaScrollbar",lg=w.forwardRef((e,n)=>{const{forceMount:r,...a}=e,l=$n(xs,e.__scopeScrollArea),{onScrollbarXEnabledChange:c,onScrollbarYEnabledChange:d}=l,f=e.orientation==="horizontal";return w.useEffect(()=>(f?c(!0):d(!0),()=>{f?c(!1):d(!1)}),[f,c,d]),l.type==="hover"?o.jsx(v5,{...a,ref:n,forceMount:r}):l.type==="scroll"?o.jsx(b5,{...a,ref:n,forceMount:r}):l.type==="auto"?o.jsx(pN,{...a,ref:n,forceMount:r}):l.type==="always"?o.jsx(cg,{...a,ref:n}):null});lg.displayName=xs;var v5=w.forwardRef((e,n)=>{const{forceMount:r,...a}=e,l=$n(xs,e.__scopeScrollArea),[c,d]=w.useState(!1);return w.useEffect(()=>{const f=l.scrollArea;let m=0;if(f){const h=()=>{window.clearTimeout(m),d(!0)},g=()=>{m=window.setTimeout(()=>d(!1),l.scrollHideDelay)};return f.addEventListener("pointerenter",h),f.addEventListener("pointerleave",g),()=>{window.clearTimeout(m),f.removeEventListener("pointerenter",h),f.removeEventListener("pointerleave",g)}}},[l.scrollArea,l.scrollHideDelay]),o.jsx(Cn,{present:r||c,children:o.jsx(pN,{"data-state":c?"visible":"hidden",...a,ref:n})})}),b5=w.forwardRef((e,n)=>{const{forceMount:r,...a}=e,l=$n(xs,e.__scopeScrollArea),c=e.orientation==="horizontal",d=Sd(()=>m("SCROLL_END"),100),[f,m]=x5("hidden",{hidden:{SCROLL:"scrolling"},scrolling:{SCROLL_END:"idle",POINTER_ENTER:"interacting"},interacting:{SCROLL:"interacting",POINTER_LEAVE:"idle"},idle:{HIDE:"hidden",SCROLL:"scrolling",POINTER_ENTER:"interacting"}});return w.useEffect(()=>{if(f==="idle"){const h=window.setTimeout(()=>m("HIDE"),l.scrollHideDelay);return()=>window.clearTimeout(h)}},[f,l.scrollHideDelay,m]),w.useEffect(()=>{const h=l.viewport,g=c?"scrollLeft":"scrollTop";if(h){let x=h[g];const y=()=>{const b=h[g];x!==b&&(m("SCROLL"),d()),x=b};return h.addEventListener("scroll",y),()=>h.removeEventListener("scroll",y)}},[l.viewport,c,m,d]),o.jsx(Cn,{present:r||f!=="hidden",children:o.jsx(cg,{"data-state":f==="hidden"?"hidden":"visible",...a,ref:n,onPointerEnter:ke(e.onPointerEnter,()=>m("POINTER_ENTER")),onPointerLeave:ke(e.onPointerLeave,()=>m("POINTER_LEAVE"))})})}),pN=w.forwardRef((e,n)=>{const r=$n(xs,e.__scopeScrollArea),{forceMount:a,...l}=e,[c,d]=w.useState(!1),f=e.orientation==="horizontal",m=Sd(()=>{if(r.viewport){const h=r.viewport.offsetWidth{const{orientation:r="vertical",...a}=e,l=$n(xs,e.__scopeScrollArea),c=w.useRef(null),d=w.useRef(0),[f,m]=w.useState({content:0,viewport:0,scrollbar:{size:0,paddingStart:0,paddingEnd:0}}),h=bN(f.viewport,f.content),g={...a,sizes:f,onSizesChange:m,hasThumb:h>0&&h<1,onThumbChange:y=>c.current=y,onThumbPointerUp:()=>d.current=0,onThumbPointerDown:y=>d.current=y};function x(y,b){return E5(y,d.current,f,b)}return r==="horizontal"?o.jsx(w5,{...g,ref:n,onThumbPositionChange:()=>{if(l.viewport&&c.current){const y=l.viewport.scrollLeft,b=ib(y,f,l.dir);c.current.style.transform=`translate3d(${b}px, 0, 0)`}},onWheelScroll:y=>{l.viewport&&(l.viewport.scrollLeft=y)},onDragScroll:y=>{l.viewport&&(l.viewport.scrollLeft=x(y,l.dir))}}):r==="vertical"?o.jsx(N5,{...g,ref:n,onThumbPositionChange:()=>{if(l.viewport&&c.current){const y=l.viewport.scrollTop,b=ib(y,f);c.current.style.transform=`translate3d(0, ${b}px, 0)`}},onWheelScroll:y=>{l.viewport&&(l.viewport.scrollTop=y)},onDragScroll:y=>{l.viewport&&(l.viewport.scrollTop=x(y))}}):null}),w5=w.forwardRef((e,n)=>{const{sizes:r,onSizesChange:a,...l}=e,c=$n(xs,e.__scopeScrollArea),[d,f]=w.useState(),m=w.useRef(null),h=rt(n,m,c.onScrollbarXChange);return w.useEffect(()=>{m.current&&f(getComputedStyle(m.current))},[m]),o.jsx(xN,{"data-orientation":"horizontal",...l,ref:h,sizes:r,style:{bottom:0,left:c.dir==="rtl"?"var(--radix-scroll-area-corner-width)":0,right:c.dir==="ltr"?"var(--radix-scroll-area-corner-width)":0,"--radix-scroll-area-thumb-width":jd(r)+"px",...e.style},onThumbPointerDown:g=>e.onThumbPointerDown(g.x),onDragScroll:g=>e.onDragScroll(g.x),onWheelScroll:(g,x)=>{if(c.viewport){const y=c.viewport.scrollLeft+g.deltaX;e.onWheelScroll(y),NN(y,x)&&g.preventDefault()}},onResize:()=>{m.current&&c.viewport&&d&&a({content:c.viewport.scrollWidth,viewport:c.viewport.offsetWidth,scrollbar:{size:m.current.clientWidth,paddingStart:Fu(d.paddingLeft),paddingEnd:Fu(d.paddingRight)}})}})}),N5=w.forwardRef((e,n)=>{const{sizes:r,onSizesChange:a,...l}=e,c=$n(xs,e.__scopeScrollArea),[d,f]=w.useState(),m=w.useRef(null),h=rt(n,m,c.onScrollbarYChange);return w.useEffect(()=>{m.current&&f(getComputedStyle(m.current))},[m]),o.jsx(xN,{"data-orientation":"vertical",...l,ref:h,sizes:r,style:{top:0,right:c.dir==="ltr"?0:void 0,left:c.dir==="rtl"?0:void 0,bottom:"var(--radix-scroll-area-corner-height)","--radix-scroll-area-thumb-height":jd(r)+"px",...e.style},onThumbPointerDown:g=>e.onThumbPointerDown(g.y),onDragScroll:g=>e.onDragScroll(g.y),onWheelScroll:(g,x)=>{if(c.viewport){const y=c.viewport.scrollTop+g.deltaY;e.onWheelScroll(y),NN(y,x)&&g.preventDefault()}},onResize:()=>{m.current&&c.viewport&&d&&a({content:c.viewport.scrollHeight,viewport:c.viewport.offsetHeight,scrollbar:{size:m.current.clientHeight,paddingStart:Fu(d.paddingTop),paddingEnd:Fu(d.paddingBottom)}})}})}),[j5,gN]=dN(xs),xN=w.forwardRef((e,n)=>{const{__scopeScrollArea:r,sizes:a,hasThumb:l,onThumbChange:c,onThumbPointerUp:d,onThumbPointerDown:f,onThumbPositionChange:m,onDragScroll:h,onWheelScroll:g,onResize:x,...y}=e,b=$n(xs,r),[j,N]=w.useState(null),S=rt(n,q=>N(q)),_=w.useRef(null),A=w.useRef(""),E=b.viewport,M=a.content-a.viewport,T=Zt(g),D=Zt(m),z=Sd(x,10);function H(q){if(_.current){const X=q.clientX-_.current.left,W=q.clientY-_.current.top;h({x:X,y:W})}}return w.useEffect(()=>{const q=X=>{const W=X.target;j?.contains(W)&&T(X,M)};return document.addEventListener("wheel",q,{passive:!1}),()=>document.removeEventListener("wheel",q,{passive:!1})},[E,j,M,T]),w.useEffect(D,[a,D]),Ca(j,z),Ca(b.content,z),o.jsx(j5,{scope:r,scrollbar:j,hasThumb:l,onThumbChange:Zt(c),onThumbPointerUp:Zt(d),onThumbPositionChange:D,onThumbPointerDown:Zt(f),children:o.jsx(Ye.div,{...y,ref:S,style:{position:"absolute",...y.style},onPointerDown:ke(e.onPointerDown,q=>{q.button===0&&(q.target.setPointerCapture(q.pointerId),_.current=j.getBoundingClientRect(),A.current=document.body.style.webkitUserSelect,document.body.style.webkitUserSelect="none",b.viewport&&(b.viewport.style.scrollBehavior="auto"),H(q))}),onPointerMove:ke(e.onPointerMove,H),onPointerUp:ke(e.onPointerUp,q=>{const X=q.target;X.hasPointerCapture(q.pointerId)&&X.releasePointerCapture(q.pointerId),document.body.style.webkitUserSelect=A.current,b.viewport&&(b.viewport.style.scrollBehavior=""),_.current=null})})})}),qu="ScrollAreaThumb",yN=w.forwardRef((e,n)=>{const{forceMount:r,...a}=e,l=gN(qu,e.__scopeScrollArea);return o.jsx(Cn,{present:r||l.hasThumb,children:o.jsx(S5,{ref:n,...a})})}),S5=w.forwardRef((e,n)=>{const{__scopeScrollArea:r,style:a,...l}=e,c=$n(qu,r),d=gN(qu,r),{onThumbPositionChange:f}=d,m=rt(n,x=>d.onThumbChange(x)),h=w.useRef(void 0),g=Sd(()=>{h.current&&(h.current(),h.current=void 0)},100);return w.useEffect(()=>{const x=c.viewport;if(x){const y=()=>{if(g(),!h.current){const b=C5(x,f);h.current=b,f()}};return f(),x.addEventListener("scroll",y),()=>x.removeEventListener("scroll",y)}},[c.viewport,g,f]),o.jsx(Ye.div,{"data-state":d.hasThumb?"visible":"hidden",...l,ref:m,style:{width:"var(--radix-scroll-area-thumb-width)",height:"var(--radix-scroll-area-thumb-height)",...a},onPointerDownCapture:ke(e.onPointerDownCapture,x=>{const b=x.target.getBoundingClientRect(),j=x.clientX-b.left,N=x.clientY-b.top;d.onThumbPointerDown({x:j,y:N})}),onPointerUp:ke(e.onPointerUp,d.onThumbPointerUp)})});yN.displayName=qu;var ug="ScrollAreaCorner",vN=w.forwardRef((e,n)=>{const r=$n(ug,e.__scopeScrollArea),a=!!(r.scrollbarX&&r.scrollbarY);return r.type!=="scroll"&&a?o.jsx(_5,{...e,ref:n}):null});vN.displayName=ug;var _5=w.forwardRef((e,n)=>{const{__scopeScrollArea:r,...a}=e,l=$n(ug,r),[c,d]=w.useState(0),[f,m]=w.useState(0),h=!!(c&&f);return Ca(l.scrollbarX,()=>{const g=l.scrollbarX?.offsetHeight||0;l.onCornerHeightChange(g),m(g)}),Ca(l.scrollbarY,()=>{const g=l.scrollbarY?.offsetWidth||0;l.onCornerWidthChange(g),d(g)}),h?o.jsx(Ye.div,{...a,ref:n,style:{width:c,height:f,position:"absolute",right:l.dir==="ltr"?0:void 0,left:l.dir==="rtl"?0:void 0,bottom:0,...e.style}}):null});function Fu(e){return e?parseInt(e,10):0}function bN(e,n){const r=e/n;return isNaN(r)?0:r}function jd(e){const n=bN(e.viewport,e.content),r=e.scrollbar.paddingStart+e.scrollbar.paddingEnd,a=(e.scrollbar.size-r)*n;return Math.max(a,18)}function E5(e,n,r,a="ltr"){const l=jd(r),c=l/2,d=n||c,f=l-d,m=r.scrollbar.paddingStart+d,h=r.scrollbar.size-r.scrollbar.paddingEnd-f,g=r.content-r.viewport,x=a==="ltr"?[0,g]:[g*-1,0];return wN([m,h],x)(e)}function ib(e,n,r="ltr"){const a=jd(n),l=n.scrollbar.paddingStart+n.scrollbar.paddingEnd,c=n.scrollbar.size-l,d=n.content-n.viewport,f=c-a,m=r==="ltr"?[0,d]:[d*-1,0],h=tp(e,m);return wN([0,d],[0,f])(h)}function wN(e,n){return r=>{if(e[0]===e[1]||n[0]===n[1])return n[0];const a=(n[1]-n[0])/(e[1]-e[0]);return n[0]+a*(r-e[0])}}function NN(e,n){return e>0&&e{})=>{let r={left:e.scrollLeft,top:e.scrollTop},a=0;return(function l(){const c={left:e.scrollLeft,top:e.scrollTop},d=r.left!==c.left,f=r.top!==c.top;(d||f)&&n(),r=c,a=window.requestAnimationFrame(l)})(),()=>window.cancelAnimationFrame(a)};function Sd(e,n){const r=Zt(e),a=w.useRef(0);return w.useEffect(()=>()=>window.clearTimeout(a.current),[]),w.useCallback(()=>{window.clearTimeout(a.current),a.current=window.setTimeout(r,n)},[r,n])}function Ca(e,n){const r=Zt(n);Wt(()=>{let a=0;if(e){const l=new ResizeObserver(()=>{cancelAnimationFrame(a),a=window.requestAnimationFrame(r)});return l.observe(e),()=>{window.cancelAnimationFrame(a),l.unobserve(e)}}},[e,r])}var jN=fN,k5=hN,T5=vN;const Wn=w.forwardRef(({className:e,children:n,...r},a)=>o.jsxs(jN,{ref:a,className:We("relative overflow-hidden",e),...r,children:[o.jsx(k5,{className:"h-full w-full rounded-[inherit]",children:n}),o.jsx(SN,{}),o.jsx(T5,{})]}));Wn.displayName=jN.displayName;const SN=w.forwardRef(({className:e,orientation:n="vertical",...r},a)=>o.jsx(lg,{ref:a,orientation:n,className:We("flex touch-none select-none transition-colors",n==="vertical"&&"h-full w-2.5 border-l border-l-transparent p-[1px]",n==="horizontal"&&"h-2.5 flex-col border-t border-t-transparent p-[1px]",e),...r,children:o.jsx(yN,{className:"relative flex-1 rounded-full bg-border"})}));SN.displayName=lg.displayName;var _d="Tabs",[A5,K7]=Kn(_d,[md]),_N=md(),[M5,dg]=A5(_d),EN=w.forwardRef((e,n)=>{const{__scopeTabs:r,value:a,onValueChange:l,defaultValue:c,orientation:d="horizontal",dir:f,activationMode:m="automatic",...h}=e,g=jl(f),[x,y]=Ar({prop:a,onChange:l,defaultProp:c??"",caller:_d});return o.jsx(M5,{scope:r,baseId:Mr(),value:x,onValueChange:y,orientation:d,dir:g,activationMode:m,children:o.jsx(Ye.div,{dir:g,"data-orientation":d,...h,ref:n})})});EN.displayName=_d;var CN="TabsList",kN=w.forwardRef((e,n)=>{const{__scopeTabs:r,loop:a=!0,...l}=e,c=dg(CN,r),d=_N(r);return o.jsx(d1,{asChild:!0,...d,orientation:c.orientation,dir:c.dir,loop:a,children:o.jsx(Ye.div,{role:"tablist","aria-orientation":c.orientation,...l,ref:n})})});kN.displayName=CN;var TN="TabsTrigger",AN=w.forwardRef((e,n)=>{const{__scopeTabs:r,value:a,disabled:l=!1,...c}=e,d=dg(TN,r),f=_N(r),m=DN(d.baseId,a),h=ON(d.baseId,a),g=a===d.value;return o.jsx(f1,{asChild:!0,...f,focusable:!l,active:g,children:o.jsx(Ye.button,{type:"button",role:"tab","aria-selected":g,"aria-controls":h,"data-state":g?"active":"inactive","data-disabled":l?"":void 0,disabled:l,id:m,...c,ref:n,onMouseDown:ke(e.onMouseDown,x=>{!l&&x.button===0&&x.ctrlKey===!1?d.onValueChange(a):x.preventDefault()}),onKeyDown:ke(e.onKeyDown,x=>{[" ","Enter"].includes(x.key)&&d.onValueChange(a)}),onFocus:ke(e.onFocus,()=>{const x=d.activationMode!=="manual";!g&&!l&&x&&d.onValueChange(a)})})})});AN.displayName=TN;var MN="TabsContent",RN=w.forwardRef((e,n)=>{const{__scopeTabs:r,value:a,forceMount:l,children:c,...d}=e,f=dg(MN,r),m=DN(f.baseId,a),h=ON(f.baseId,a),g=a===f.value,x=w.useRef(g);return w.useEffect(()=>{const y=requestAnimationFrame(()=>x.current=!1);return()=>cancelAnimationFrame(y)},[]),o.jsx(Cn,{present:l||g,children:({present:y})=>o.jsx(Ye.div,{"data-state":g?"active":"inactive","data-orientation":f.orientation,role:"tabpanel","aria-labelledby":m,hidden:!y,id:h,tabIndex:0,...d,ref:n,style:{...e.style,animationDuration:x.current?"0s":void 0},children:y&&c})})});RN.displayName=MN;function DN(e,n){return`${e}-trigger-${n}`}function ON(e,n){return`${e}-content-${n}`}var R5=EN,zN=kN,IN=AN,LN=RN;const D5=R5,$N=w.forwardRef(({className:e,...n},r)=>o.jsx(zN,{ref:r,className:We("inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",e),...n}));$N.displayName=zN.displayName;const Nu=w.forwardRef(({className:e,...n},r)=>o.jsx(IN,{ref:r,className:We("inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",e),...n}));Nu.displayName=IN.displayName;const ju=w.forwardRef(({className:e,...n},r)=>o.jsx(LN,{ref:r,className:We("mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",e),...n}));ju.displayName=LN.displayName;function fg(e){const n=w.useRef({value:e,previous:e});return w.useMemo(()=>(n.current.value!==e&&(n.current.previous=n.current.value,n.current.value=e),n.current.previous),[e])}var Ed="Checkbox",[O5,Q7]=Kn(Ed),[z5,mg]=O5(Ed);function I5(e){const{__scopeCheckbox:n,checked:r,children:a,defaultChecked:l,disabled:c,form:d,name:f,onCheckedChange:m,required:h,value:g="on",internal_do_not_use_render:x}=e,[y,b]=Ar({prop:r,defaultProp:l??!1,onChange:m,caller:Ed}),[j,N]=w.useState(null),[S,_]=w.useState(null),A=w.useRef(!1),E=j?!!d||!!j.closest("form"):!0,M={checked:y,disabled:c,setChecked:b,control:j,setControl:N,name:f,form:d,value:g,hasConsumerStoppedPropagationRef:A,required:h,defaultChecked:Tr(l)?!1:l,isFormControl:E,bubbleInput:S,setBubbleInput:_};return o.jsx(z5,{scope:n,...M,children:L5(x)?x(M):a})}var PN="CheckboxTrigger",HN=w.forwardRef(({__scopeCheckbox:e,onKeyDown:n,onClick:r,...a},l)=>{const{control:c,value:d,disabled:f,checked:m,required:h,setControl:g,setChecked:x,hasConsumerStoppedPropagationRef:y,isFormControl:b,bubbleInput:j}=mg(PN,e),N=rt(l,g),S=w.useRef(m);return w.useEffect(()=>{const _=c?.form;if(_){const A=()=>x(S.current);return _.addEventListener("reset",A),()=>_.removeEventListener("reset",A)}},[c,x]),o.jsx(Ye.button,{type:"button",role:"checkbox","aria-checked":Tr(m)?"mixed":m,"aria-required":h,"data-state":YN(m),"data-disabled":f?"":void 0,disabled:f,value:d,...a,ref:N,onKeyDown:ke(n,_=>{_.key==="Enter"&&_.preventDefault()}),onClick:ke(r,_=>{x(A=>Tr(A)?!0:!A),j&&b&&(y.current=_.isPropagationStopped(),y.current||_.stopPropagation())})})});HN.displayName=PN;var UN=w.forwardRef((e,n)=>{const{__scopeCheckbox:r,name:a,checked:l,defaultChecked:c,required:d,disabled:f,value:m,onCheckedChange:h,form:g,...x}=e;return o.jsx(I5,{__scopeCheckbox:r,checked:l,defaultChecked:c,disabled:f,required:d,onCheckedChange:h,name:a,form:g,value:m,internal_do_not_use_render:({isFormControl:y})=>o.jsxs(o.Fragment,{children:[o.jsx(HN,{...x,ref:n,__scopeCheckbox:r}),y&&o.jsx(FN,{__scopeCheckbox:r})]})})});UN.displayName=Ed;var BN="CheckboxIndicator",VN=w.forwardRef((e,n)=>{const{__scopeCheckbox:r,forceMount:a,...l}=e,c=mg(BN,r);return o.jsx(Cn,{present:a||Tr(c.checked)||c.checked===!0,children:o.jsx(Ye.span,{"data-state":YN(c.checked),"data-disabled":c.disabled?"":void 0,...l,ref:n,style:{pointerEvents:"none",...e.style}})})});VN.displayName=BN;var qN="CheckboxBubbleInput",FN=w.forwardRef(({__scopeCheckbox:e,...n},r)=>{const{control:a,hasConsumerStoppedPropagationRef:l,checked:c,defaultChecked:d,required:f,disabled:m,name:h,value:g,form:x,bubbleInput:y,setBubbleInput:b}=mg(qN,e),j=rt(r,b),N=fg(c),S=Lp(a);w.useEffect(()=>{const A=y;if(!A)return;const E=window.HTMLInputElement.prototype,T=Object.getOwnPropertyDescriptor(E,"checked").set,D=!l.current;if(N!==c&&T){const z=new Event("click",{bubbles:D});A.indeterminate=Tr(c),T.call(A,Tr(c)?!1:c),A.dispatchEvent(z)}},[y,N,c,l]);const _=w.useRef(Tr(c)?!1:c);return o.jsx(Ye.input,{type:"checkbox","aria-hidden":!0,defaultChecked:d??_.current,required:f,disabled:m,name:h,value:g,form:x,...n,tabIndex:-1,ref:j,style:{...n.style,...S,position:"absolute",pointerEvents:"none",opacity:0,margin:0,transform:"translateX(-100%)"}})});FN.displayName=qN;function L5(e){return typeof e=="function"}function Tr(e){return e==="indeterminate"}function YN(e){return Tr(e)?"indeterminate":e?"checked":"unchecked"}function co({className:e,...n}){return o.jsx(UN,{"data-slot":"checkbox",className:We("peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",e),...n,children:o.jsx(VN,{"data-slot":"checkbox-indicator",className:"flex items-center justify-center text-current transition-none",children:o.jsx(jo,{className:"size-3.5"})})})}var GN=Object.freeze({position:"absolute",border:0,width:1,height:1,padding:0,margin:-1,overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",wordWrap:"normal"}),$5="VisuallyHidden",XN=w.forwardRef((e,n)=>o.jsx(Ye.span,{...e,ref:n,style:{...GN,...e.style}}));XN.displayName=$5;var P5=XN,[Cd,J7]=Kn("Tooltip",[Ua]),kd=Ua(),ZN="TooltipProvider",H5=700,np="tooltip.open",[U5,hg]=Cd(ZN),WN=e=>{const{__scopeTooltip:n,delayDuration:r=H5,skipDelayDuration:a=300,disableHoverableContent:l=!1,children:c}=e,d=w.useRef(!0),f=w.useRef(!1),m=w.useRef(0);return w.useEffect(()=>{const h=m.current;return()=>window.clearTimeout(h)},[]),o.jsx(U5,{scope:n,isOpenDelayedRef:d,delayDuration:r,onOpen:w.useCallback(()=>{window.clearTimeout(m.current),d.current=!1},[]),onClose:w.useCallback(()=>{window.clearTimeout(m.current),m.current=window.setTimeout(()=>d.current=!0,a)},[a]),isPointerInTransitRef:f,onPointerInTransitChange:w.useCallback(h=>{f.current=h},[]),disableHoverableContent:l,children:c})};WN.displayName=ZN;var ul="Tooltip",[B5,Tl]=Cd(ul),KN=e=>{const{__scopeTooltip:n,children:r,open:a,defaultOpen:l,onOpenChange:c,disableHoverableContent:d,delayDuration:f}=e,m=hg(ul,e.__scopeTooltip),h=kd(n),[g,x]=w.useState(null),y=Mr(),b=w.useRef(0),j=d??m.disableHoverableContent,N=f??m.delayDuration,S=w.useRef(!1),[_,A]=Ar({prop:a,defaultProp:l??!1,onChange:z=>{z?(m.onOpen(),document.dispatchEvent(new CustomEvent(np))):m.onClose(),c?.(z)},caller:ul}),E=w.useMemo(()=>_?S.current?"delayed-open":"instant-open":"closed",[_]),M=w.useCallback(()=>{window.clearTimeout(b.current),b.current=0,S.current=!1,A(!0)},[A]),T=w.useCallback(()=>{window.clearTimeout(b.current),b.current=0,A(!1)},[A]),D=w.useCallback(()=>{window.clearTimeout(b.current),b.current=window.setTimeout(()=>{S.current=!0,A(!0),b.current=0},N)},[N,A]);return w.useEffect(()=>()=>{b.current&&(window.clearTimeout(b.current),b.current=0)},[]),o.jsx(Hp,{...h,children:o.jsx(B5,{scope:n,contentId:y,open:_,stateAttribute:E,trigger:g,onTriggerChange:x,onTriggerEnter:w.useCallback(()=>{m.isOpenDelayedRef.current?D():M()},[m.isOpenDelayedRef,D,M]),onTriggerLeave:w.useCallback(()=>{j?T():(window.clearTimeout(b.current),b.current=0)},[T,j]),onOpen:M,onClose:T,disableHoverableContent:j,children:r})})};KN.displayName=ul;var sp="TooltipTrigger",QN=w.forwardRef((e,n)=>{const{__scopeTooltip:r,...a}=e,l=Tl(sp,r),c=hg(sp,r),d=kd(r),f=w.useRef(null),m=rt(n,f,l.onTriggerChange),h=w.useRef(!1),g=w.useRef(!1),x=w.useCallback(()=>h.current=!1,[]);return w.useEffect(()=>()=>document.removeEventListener("pointerup",x),[x]),o.jsx(Up,{asChild:!0,...d,children:o.jsx(Ye.button,{"aria-describedby":l.open?l.contentId:void 0,"data-state":l.stateAttribute,...a,ref:m,onPointerMove:ke(e.onPointerMove,y=>{y.pointerType!=="touch"&&!g.current&&!c.isPointerInTransitRef.current&&(l.onTriggerEnter(),g.current=!0)}),onPointerLeave:ke(e.onPointerLeave,()=>{l.onTriggerLeave(),g.current=!1}),onPointerDown:ke(e.onPointerDown,()=>{l.open&&l.onClose(),h.current=!0,document.addEventListener("pointerup",x,{once:!0})}),onFocus:ke(e.onFocus,()=>{h.current||l.onOpen()}),onBlur:ke(e.onBlur,l.onClose),onClick:ke(e.onClick,l.onClose)})})});QN.displayName=sp;var pg="TooltipPortal",[V5,q5]=Cd(pg,{forceMount:void 0}),JN=e=>{const{__scopeTooltip:n,forceMount:r,children:a,container:l}=e,c=Tl(pg,n);return o.jsx(V5,{scope:n,forceMount:r,children:o.jsx(Cn,{present:r||c.open,children:o.jsx(fd,{asChild:!0,container:l,children:a})})})};JN.displayName=pg;var ka="TooltipContent",e2=w.forwardRef((e,n)=>{const r=q5(ka,e.__scopeTooltip),{forceMount:a=r.forceMount,side:l="top",...c}=e,d=Tl(ka,e.__scopeTooltip);return o.jsx(Cn,{present:a||d.open,children:d.disableHoverableContent?o.jsx(t2,{side:l,...c,ref:n}):o.jsx(F5,{side:l,...c,ref:n})})}),F5=w.forwardRef((e,n)=>{const r=Tl(ka,e.__scopeTooltip),a=hg(ka,e.__scopeTooltip),l=w.useRef(null),c=rt(n,l),[d,f]=w.useState(null),{trigger:m,onClose:h}=r,g=l.current,{onPointerInTransitChange:x}=a,y=w.useCallback(()=>{f(null),x(!1)},[x]),b=w.useCallback((j,N)=>{const S=j.currentTarget,_={x:j.clientX,y:j.clientY},A=W5(_,S.getBoundingClientRect()),E=K5(_,A),M=Q5(N.getBoundingClientRect()),T=eR([...E,...M]);f(T),x(!0)},[x]);return w.useEffect(()=>()=>y(),[y]),w.useEffect(()=>{if(m&&g){const j=S=>b(S,g),N=S=>b(S,m);return m.addEventListener("pointerleave",j),g.addEventListener("pointerleave",N),()=>{m.removeEventListener("pointerleave",j),g.removeEventListener("pointerleave",N)}}},[m,g,b,y]),w.useEffect(()=>{if(d){const j=N=>{const S=N.target,_={x:N.clientX,y:N.clientY},A=m?.contains(S)||g?.contains(S),E=!J5(_,d);A?y():E&&(y(),h())};return document.addEventListener("pointermove",j),()=>document.removeEventListener("pointermove",j)}},[m,g,d,h,y]),o.jsx(t2,{...e,ref:c})}),[Y5,G5]=Cd(ul,{isInside:!1}),X5=cC("TooltipContent"),t2=w.forwardRef((e,n)=>{const{__scopeTooltip:r,children:a,"aria-label":l,onEscapeKeyDown:c,onPointerDownOutside:d,...f}=e,m=Tl(ka,r),h=kd(r),{onClose:g}=m;return w.useEffect(()=>(document.addEventListener(np,g),()=>document.removeEventListener(np,g)),[g]),w.useEffect(()=>{if(m.trigger){const x=y=>{y.target?.contains(m.trigger)&&g()};return window.addEventListener("scroll",x,{capture:!0}),()=>window.removeEventListener("scroll",x,{capture:!0})}},[m.trigger,g]),o.jsx(id,{asChild:!0,disableOutsidePointerEvents:!1,onEscapeKeyDown:c,onPointerDownOutside:d,onFocusOutside:x=>x.preventDefault(),onDismiss:g,children:o.jsxs(Bp,{"data-state":m.stateAttribute,...h,...f,ref:n,style:{...f.style,"--radix-tooltip-content-transform-origin":"var(--radix-popper-transform-origin)","--radix-tooltip-content-available-width":"var(--radix-popper-available-width)","--radix-tooltip-content-available-height":"var(--radix-popper-available-height)","--radix-tooltip-trigger-width":"var(--radix-popper-anchor-width)","--radix-tooltip-trigger-height":"var(--radix-popper-anchor-height)"},children:[o.jsx(X5,{children:a}),o.jsx(Y5,{scope:r,isInside:!0,children:o.jsx(P5,{id:m.contentId,role:"tooltip",children:l||a})})]})})});e2.displayName=ka;var n2="TooltipArrow",Z5=w.forwardRef((e,n)=>{const{__scopeTooltip:r,...a}=e,l=kd(r);return G5(n2,r).isInside?null:o.jsx(Vp,{...l,...a,ref:n})});Z5.displayName=n2;function W5(e,n){const r=Math.abs(n.top-e.y),a=Math.abs(n.bottom-e.y),l=Math.abs(n.right-e.x),c=Math.abs(n.left-e.x);switch(Math.min(r,a,l,c)){case c:return"left";case l:return"right";case r:return"top";case a:return"bottom";default:throw new Error("unreachable")}}function K5(e,n,r=5){const a=[];switch(n){case"top":a.push({x:e.x-r,y:e.y+r},{x:e.x+r,y:e.y+r});break;case"bottom":a.push({x:e.x-r,y:e.y-r},{x:e.x+r,y:e.y-r});break;case"left":a.push({x:e.x+r,y:e.y-r},{x:e.x+r,y:e.y+r});break;case"right":a.push({x:e.x-r,y:e.y-r},{x:e.x-r,y:e.y+r});break}return a}function Q5(e){const{top:n,right:r,bottom:a,left:l}=e;return[{x:l,y:n},{x:r,y:n},{x:r,y:a},{x:l,y:a}]}function J5(e,n){const{x:r,y:a}=e;let l=!1;for(let c=0,d=n.length-1;ca!=y>a&&r<(x-h)*(a-g)/(y-g)+h&&(l=!l)}return l}function eR(e){const n=e.slice();return n.sort((r,a)=>r.xa.x?1:r.ya.y?1:0),tR(n)}function tR(e){if(e.length<=1)return e.slice();const n=[];for(let a=0;a=2;){const c=n[n.length-1],d=n[n.length-2];if((c.x-d.x)*(l.y-d.y)>=(c.y-d.y)*(l.x-d.x))n.pop();else break}n.push(l)}n.pop();const r=[];for(let a=e.length-1;a>=0;a--){const l=e[a];for(;r.length>=2;){const c=r[r.length-1],d=r[r.length-2];if((c.x-d.x)*(l.y-d.y)>=(c.y-d.y)*(l.x-d.x))r.pop();else break}r.push(l)}return r.pop(),n.length===1&&r.length===1&&n[0].x===r[0].x&&n[0].y===r[0].y?n:n.concat(r)}var nR=WN,sR=KN,rR=QN,oR=JN,s2=e2;const aR=nR,iR=sR,lR=rR,r2=w.forwardRef(({className:e,sideOffset:n=4,...r},a)=>o.jsx(oR,{children:o.jsx(s2,{ref:a,sideOffset:n,className:We("z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",e),...r})}));r2.displayName=s2.displayName;const fa={MODEL:"gen_ai.request.model",INPUT_TOKENS:"gen_ai.usage.input_tokens",OUTPUT_TOKENS:"gen_ai.usage.output_tokens",INPUT_MESSAGES:"gen_ai.input.messages",SYSTEM_INSTRUCTIONS:"gen_ai.system_instructions"};function cR(e){return e.type==="text"}function uR(e){return e.type==="tool_call"||e.type==="function_call"}function dR(e){return e.type==="tool_result"||e.type==="function_result"||e.type==="tool_call_response"}function fR(e){if(!e)return[];try{return JSON.parse(e)}catch{return[]}}function mR(e){const n={system:0,user:0,assistant:0,toolCalls:0,toolResults:0,total:0};try{let r;if(typeof e=="string")r=fR(e);else if(Array.isArray(e))r=e;else return n;for(const a of r){if(!a||typeof a!="object")continue;const l=a.role,c=a.parts;let d=0;if(Array.isArray(c)){for(const f of c)if(!(!f||typeof f!="object")){if(cR(f)){const m=f.content||f.text||"";d+=m.length}else if(uR(f)){const m=f.name||"",h=f.arguments||"";n.toolCalls+=m.length+h.length}else if(dR(f)){const m=f.result||f.response||"";n.toolResults+=m.length}}}l==="system"?n.system+=d:l==="user"?n.user+=d:l==="assistant"?n.assistant+=d:l==="tool"&&(n.toolResults+=d)}n.total=n.system+n.user+n.assistant+n.toolCalls+n.toolResults}catch{}return n}function hR(e){const n=e.filter(l=>l.type==="response.trace.completed"),r=new Map;for(const l of n){if(!("data"in l))continue;const c=l.data,d=c.response_id||"unknown";r.has(d)||r.set(d,[]),r.get(d).push(c)}const a=[];for(const[l,c]of r){let d=0,f=0,m,h=Date.now()/1e3,g,x=0,y={system:0,user:0,assistant:0,toolCalls:0,toolResults:0,total:0};for(const b of c){const j=b.attributes||{},N=j[fa.INPUT_TOKENS],S=j[fa.OUTPUT_TOKENS];N!==void 0&&(d+=Number(N)),S!==void 0&&(f+=Number(S)),j[fa.MODEL]&&(m=String(j[fa.MODEL])),b.start_time&&b.start_time0||f>0)&&a.push({response_id:l,timestamp:h,input_tokens:d,output_tokens:f,total_tokens:d+f,model:m,entity_id:g,duration_ms:x,composition:y})}return a.sort((l,c)=>l.timestamp-c.timestamp),a}function pR(e){if(e.length===0)return{totalInput:0,totalOutput:0,totalTokens:0,avgInput:0,avgOutput:0,avgTotal:0,peakInput:0,peakOutput:0,peakTotal:0,turnCount:0};const n=e.reduce((f,m)=>f+m.input_tokens,0),r=e.reduce((f,m)=>f+m.output_tokens,0),a=n+r,l=Math.max(...e.map(f=>f.input_tokens)),c=Math.max(...e.map(f=>f.output_tokens)),d=Math.max(...e.map(f=>f.total_tokens));return{totalInput:n,totalOutput:r,totalTokens:a,avgInput:Math.round(n/e.length),avgOutput:Math.round(r/e.length),avgTotal:Math.round(a/e.length),peakInput:l,peakOutput:c,peakTotal:d,turnCount:e.length}}function gR(e){return e.reduce((n,r)=>({system:n.system+r.composition.system,user:n.user+r.composition.user,assistant:n.assistant+r.composition.assistant,toolCalls:n.toolCalls+r.composition.toolCalls,toolResults:n.toolResults+r.composition.toolResults,total:n.total+r.composition.total}),{system:0,user:0,assistant:0,toolCalls:0,toolResults:0,total:0})}function In(e){return e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}const Pt={input:"bg-blue-500 dark:bg-blue-600",output:"bg-emerald-500 dark:bg-emerald-600",system:"bg-purple-500 dark:bg-purple-600",user:"bg-blue-500 dark:bg-blue-600",assistant:"bg-emerald-500 dark:bg-emerald-600",toolCalls:"bg-amber-500 dark:bg-amber-600",toolResults:"bg-orange-500 dark:bg-orange-600"};function lb({segments:e,maxValue:n,height:r=20,renderLabel:a}){const l=e.reduce((f,m)=>f+m.value,0);if(l===0)return o.jsx("div",{className:"flex items-center gap-2 w-full",children:o.jsx("div",{className:"rounded bg-muted/30 flex-1",style:{height:`${r}px`}})});const c=n>0?l/n*100:100,d=e.filter(f=>f.value>0).map(f=>({...f,percent:Math.round(f.value/l*100)}));return o.jsxs("div",{className:"flex items-center gap-2 w-full",children:[o.jsx("div",{className:"relative rounded overflow-hidden bg-muted/30 flex-1",style:{height:`${r}px`},children:o.jsx(aR,{delayDuration:150,children:o.jsx("div",{className:"h-full flex transition-all duration-300",style:{width:`${c}%`},children:d.map(f=>o.jsxs(iR,{children:[o.jsx(lR,{asChild:!0,children:o.jsx("div",{className:`h-full ${f.color} transition-all duration-150 hover:brightness-110 hover:scale-y-[1.15] origin-bottom cursor-default`,style:{width:`${f.value/l*100}%`}})}),o.jsx(r2,{side:"top",className:"text-xs",children:o.jsxs("div",{className:"flex items-center gap-1.5",children:[o.jsx("div",{className:`w-2 h-2 rounded-sm ${f.color} flex-shrink-0`}),o.jsx("span",{className:"font-medium",children:f.label}),o.jsxs("span",{className:"opacity-80",children:[In(f.value)," (",f.percent,"%)"]})]})})]},f.key))})})}),a?.(l,e)]})}function xR(e,n){return[{key:"input",value:e,color:Pt.input,label:"Input"},{key:"output",value:n,color:Pt.output,label:"Output"}]}function yR(e){return[{key:"system",value:e.system,color:Pt.system,label:"System"},{key:"user",value:e.user,color:Pt.user,label:"User"},{key:"assistant",value:e.assistant,color:Pt.assistant,label:"Assistant"},{key:"toolCalls",value:e.toolCalls,color:Pt.toolCalls,label:"Tool Calls"},{key:"toolResults",value:e.toolResults,color:Pt.toolResults,label:"Tool Results"}]}function o2({composition:e,className:n=""}){const{system:r,user:a,assistant:l,toolCalls:c,toolResults:d,total:f}=e;if(f===0)return o.jsx("div",{className:`text-xs text-muted-foreground ${n}`,children:"No composition data available"});const m=[{label:"System",value:r,color:Pt.system},{label:"User",value:a,color:Pt.user},{label:"Assistant",value:l,color:Pt.assistant},{label:"Tool Calls",value:c,color:Pt.toolCalls},{label:"Tool Results",value:d,color:Pt.toolResults}].filter(h=>h.value>0);return o.jsx("div",{className:`space-y-1.5 ${n}`,children:m.map(h=>{const g=Math.round(h.value/f*100);return o.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[o.jsx("div",{className:`w-2 h-2 rounded-sm ${h.color}`}),o.jsx("span",{className:"text-muted-foreground w-20",children:h.label}),o.jsx("div",{className:"flex-1 h-3 bg-muted/30 rounded overflow-hidden",children:o.jsx("div",{className:`h-full ${h.color} transition-all duration-300`,style:{width:`${g}%`}})}),o.jsxs("span",{className:"font-mono w-10 text-right text-muted-foreground",children:[g,"%"]})]},h.label)})})}function vR({turn:e,index:n,maxValue:r,maxCompositionValue:a,cumulativeInput:l,cumulativeOutput:c,cumulativeComposition:d,showCumulative:f,viewMode:m}){const[h,g]=w.useState(!1),x=f?l:e.input_tokens,y=f?c:e.output_tokens,b=f?d:e.composition,j=new Date(e.timestamp*1e3).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"});return o.jsxs("div",{className:"border-b border-muted/50 last:border-0",children:[o.jsxs("div",{className:"flex items-center gap-3 py-2 px-2 hover:bg-muted/30 cursor-pointer transition-colors",onClick:()=>g(!h),children:[o.jsx("div",{className:"w-6 h-6 rounded-full bg-muted flex items-center justify-center text-xs font-medium flex-shrink-0",children:n+1}),o.jsx("div",{className:"flex-1 min-w-0",children:m==="tokens"?o.jsx(lb,{segments:xR(x,y),maxValue:r,height:20,renderLabel:(N,S)=>o.jsxs("div",{className:"flex items-center gap-1 text-xs font-mono text-muted-foreground min-w-[80px] justify-end",children:[o.jsxs("span",{className:"text-blue-600 dark:text-blue-400",children:["↑",In(S[0]?.value||0)]}),o.jsx("span",{children:"/"}),o.jsxs("span",{className:"text-emerald-600 dark:text-emerald-400",children:["↓",In(S[1]?.value||0)]})]})}):o.jsx(lb,{segments:yR(b),maxValue:a,height:20,renderLabel:N=>o.jsxs("div",{className:"text-xs font-mono text-muted-foreground min-w-[50px] text-right",children:[In(Math.round(N/4)),"~"]})})}),o.jsx("div",{className:"text-muted-foreground flex-shrink-0",children:h?o.jsx(Rt,{className:"h-4 w-4"}):o.jsx(en,{className:"h-4 w-4"})})]}),h&&o.jsx("div",{className:"pb-3",children:o.jsxs("div",{className:"flex items-start gap-3 px-2",children:[o.jsx("div",{className:"w-6 flex justify-center flex-shrink-0",children:o.jsx("div",{className:"w-px h-full bg-muted"})}),o.jsx("div",{className:"flex-1 min-w-0",children:o.jsxs("div",{className:"flex items-start gap-2",children:[o.jsx("div",{className:"text-muted-foreground text-xs mt-1",children:"└─"}),o.jsxs("div",{className:"flex-1 space-y-3",children:[o.jsxs("div",{className:"grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-muted-foreground",children:[o.jsxs("div",{children:["Time: ",o.jsx("span",{className:"font-mono text-foreground",children:j})]}),o.jsxs("div",{children:["Duration: ",o.jsxs("span",{className:"font-mono text-foreground",children:[e.duration_ms.toFixed(0),"ms"]})]}),e.model&&o.jsxs("div",{children:["Model: ",o.jsx("span",{className:"font-mono text-foreground",children:e.model})]}),e.entity_id&&o.jsxs("div",{children:["Entity: ",o.jsx("span",{className:"font-mono text-foreground",children:e.entity_id})]})]}),m==="tokens"&&o.jsxs("div",{className:"flex gap-4 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"text-blue-600 dark:text-blue-400",children:"Input:"})," ",o.jsx("span",{className:"font-mono",children:e.input_tokens.toLocaleString()})]}),o.jsxs("div",{children:[o.jsx("span",{className:"text-emerald-600 dark:text-emerald-400",children:"Output:"})," ",o.jsx("span",{className:"font-mono",children:e.output_tokens.toLocaleString()})]}),o.jsxs("div",{children:[o.jsx("span",{className:"text-muted-foreground",children:"Total:"})," ",o.jsx("span",{className:"font-mono",children:e.total_tokens.toLocaleString()})]})]}),m==="composition"&&e.composition.total>0&&o.jsxs("div",{children:[o.jsxs("div",{className:"text-xs text-muted-foreground mb-2 flex items-center gap-1",children:[o.jsx(Fs,{className:"h-3 w-3"}),"Context Composition (estimated from ~",In(Math.round(e.composition.total/4))," tokens)"]}),o.jsx(o2,{composition:e.composition})]})]})]})})]})})]})}function wh({label:e,value:n,icon:r,color:a="default"}){const l={default:"text-muted-foreground",blue:"text-blue-600 dark:text-blue-400",green:"text-emerald-600 dark:text-emerald-400"}[a];return o.jsxs("div",{className:"flex items-center gap-2 p-2 bg-muted/30 rounded",children:[o.jsx(r,{className:`h-4 w-4 ${l}`}),o.jsxs("div",{className:"flex-1 min-w-0",children:[o.jsx("div",{className:"text-xs text-muted-foreground truncate",children:e}),o.jsx("div",{className:"font-mono text-sm font-medium",children:n})]})]})}function bR({events:e}){const n=le(x=>x.contextInspectorViewMode),r=le(x=>x.setContextInspectorViewMode),a=le(x=>x.contextInspectorCumulative),l=le(x=>x.setContextInspectorCumulative),c=w.useMemo(()=>hR(e),[e]),d=w.useMemo(()=>pR(c),[c]),f=w.useMemo(()=>gR(c),[c]),m=w.useMemo(()=>c.length===0?0:a?d.totalTokens:0,[c,a,d.totalTokens]),h=w.useMemo(()=>c.length===0?0:a?f.total:0,[c,a,f.total]),g=w.useMemo(()=>{let x=0,y=0,b={system:0,user:0,assistant:0,toolCalls:0,toolResults:0,total:0};return c.map(j=>(x+=j.input_tokens,y+=j.output_tokens,b={system:b.system+j.composition.system,user:b.user+j.composition.user,assistant:b.assistant+j.composition.assistant,toolCalls:b.toolCalls+j.composition.toolCalls,toolResults:b.toolResults+j.composition.toolResults,total:b.total+j.composition.total},{input:x,output:y,composition:{...b}}))},[c]);return c.length===0?o.jsxs("div",{className:"flex flex-col items-center text-center p-6 pt-9",children:[o.jsx(ha,{className:"h-8 w-8 text-muted-foreground mb-3"}),o.jsx("div",{className:"text-sm font-medium mb-1",children:"No Data"}),o.jsxs("div",{className:"text-xs text-muted-foreground max-w-[200px]",children:["Run"," ",o.jsx("span",{className:"font-mono bg-accent/10 px-1 rounded",children:"devui --instrumentation"})," ","and start a conversation."]})]}):o.jsxs("div",{className:"h-full flex flex-col",children:[o.jsxs("div",{className:"p-3 border-b flex-shrink-0 space-y-2",children:[o.jsxs("div",{className:"flex items-center justify-between gap-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(ha,{className:"h-4 w-4"}),o.jsx("span",{className:"font-medium text-sm",children:"Context Inspector"}),o.jsxs(ut,{variant:"outline",className:"text-xs",children:[c.length," turn",c.length!==1?"s":""]})]}),o.jsxs("label",{className:"flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer",children:[o.jsx(co,{checked:a,onCheckedChange:x=>l(x===!0),className:"h-3.5 w-3.5"}),o.jsx("span",{children:"Cumulative"})]})]}),o.jsxs("div",{className:"flex items-center bg-muted rounded-md p-1",children:[o.jsx("button",{onClick:()=>r("tokens"),className:`flex-1 px-3 py-1.5 text-xs rounded transition-colors ${n==="tokens"?"bg-background shadow-sm font-medium":"text-muted-foreground hover:text-foreground"}`,children:"Tokens"}),o.jsx("button",{onClick:()=>r("composition"),className:`flex-1 px-3 py-1.5 text-xs rounded transition-colors ${n==="composition"?"bg-background shadow-sm font-medium":"text-muted-foreground hover:text-foreground"}`,children:"Composition"})]}),o.jsx("div",{className:"text-xs text-muted-foreground",children:n==="tokens"?"Token usage per turn":"Context breakdown by message type (chars)"})]}),o.jsx(Wn,{className:"flex-1",children:o.jsxs("div",{className:"p-3 space-y-4",children:[o.jsxs("div",{className:"flex items-center gap-4 text-xs px-1 flex-wrap",children:[n==="tokens"?o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"flex items-center gap-1.5",children:[o.jsx("div",{className:`w-3 h-3 rounded ${Pt.input}`}),o.jsx("span",{className:"text-muted-foreground",children:"Input (↑)"})]}),o.jsxs("div",{className:"flex items-center gap-1.5",children:[o.jsx("div",{className:`w-3 h-3 rounded ${Pt.output}`}),o.jsx("span",{className:"text-muted-foreground",children:"Output (↓)"})]})]}):o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"flex items-center gap-1.5",children:[o.jsx("div",{className:`w-2.5 h-2.5 rounded-sm ${Pt.system}`}),o.jsx("span",{className:"text-muted-foreground",children:"System"})]}),o.jsxs("div",{className:"flex items-center gap-1.5",children:[o.jsx("div",{className:`w-2.5 h-2.5 rounded-sm ${Pt.user}`}),o.jsx("span",{className:"text-muted-foreground",children:"User"})]}),o.jsxs("div",{className:"flex items-center gap-1.5",children:[o.jsx("div",{className:`w-2.5 h-2.5 rounded-sm ${Pt.assistant}`}),o.jsx("span",{className:"text-muted-foreground",children:"Assistant"})]}),o.jsxs("div",{className:"flex items-center gap-1.5",children:[o.jsx("div",{className:`w-2.5 h-2.5 rounded-sm ${Pt.toolCalls}`}),o.jsx("span",{className:"text-muted-foreground",children:"Tools"})]}),o.jsxs("div",{className:"flex items-center gap-1.5",children:[o.jsx("div",{className:`w-2.5 h-2.5 rounded-sm ${Pt.toolResults}`}),o.jsx("span",{className:"text-muted-foreground",children:"Results"})]})]}),o.jsx("div",{className:"flex-1"}),o.jsxs("div",{className:"flex items-center gap-1 text-muted-foreground",children:[o.jsx(Fs,{className:"h-3 w-3"}),o.jsx("span",{children:"Click for details"})]})]}),o.jsx("div",{className:"border rounded-lg overflow-hidden",children:c.map((x,y)=>o.jsx(vR,{turn:x,index:y,maxValue:m,maxCompositionValue:h,cumulativeInput:g[y]?.input||0,cumulativeOutput:g[y]?.output||0,cumulativeComposition:g[y]?.composition||x.composition,showCumulative:a,viewMode:n},x.response_id))}),o.jsxs("div",{className:"border rounded-lg overflow-hidden",children:[o.jsx("div",{className:"p-3 bg-muted/30 border-b",children:o.jsx("span",{className:"text-xs font-medium",children:"Session Summary"})}),o.jsxs("div",{className:"p-3 space-y-3",children:[o.jsxs("div",{className:"grid grid-cols-3 gap-2",children:[o.jsx(wh,{label:"Total Tokens",value:In(d.totalTokens),icon:JA}),o.jsx(wh,{label:"Input",value:In(d.totalInput),icon:ha,color:"blue"}),o.jsx(wh,{label:"Output",value:In(d.totalOutput),icon:ha,color:"green"})]}),c.length>1&&o.jsxs("div",{className:"grid grid-cols-2 gap-x-4 gap-y-1 text-xs pt-2 border-t border-muted/50",children:[o.jsxs("div",{className:"flex justify-between",children:[o.jsx("span",{className:"text-muted-foreground",children:"Avg per turn:"}),o.jsx("span",{className:"font-mono",children:In(d.avgTotal)})]}),o.jsxs("div",{className:"flex justify-between",children:[o.jsx("span",{className:"text-muted-foreground",children:"Peak turn:"}),o.jsx("span",{className:"font-mono",children:In(d.peakTotal)})]}),o.jsxs("div",{className:"flex justify-between",children:[o.jsx("span",{className:"text-muted-foreground",children:"Avg input:"}),o.jsx("span",{className:"font-mono text-blue-600 dark:text-blue-400",children:In(d.avgInput)})]}),o.jsxs("div",{className:"flex justify-between",children:[o.jsx("span",{className:"text-muted-foreground",children:"Avg output:"}),o.jsx("span",{className:"font-mono text-emerald-600 dark:text-emerald-400",children:In(d.avgOutput)})]})]}),f.total>0&&o.jsx("div",{className:"pt-3 border-t border-muted/50",children:o.jsxs("div",{className:"flex items-start gap-2",children:[o.jsx("div",{className:"text-muted-foreground text-xs mt-0.5",children:"└─"}),o.jsxs("div",{className:"flex-1",children:[o.jsxs("div",{className:"text-xs text-muted-foreground mb-2 flex items-center gap-1",children:[o.jsx(Fs,{className:"h-3 w-3"}),"Total Composition (all turns)"]}),o.jsx(o2,{composition:f})]})]})})]})]})]})})]})}function a2(){return o.jsx("div",{className:"flex items-center gap-2 py-3 px-2",children:o.jsx("div",{className:"flex-1 border-t border-border/50"})})}function i2(e){const n=[];let r=!1;for(let a=0;a100&&(l.includes(` -`)||l.trim().match(/[.!?]\s*$/))&&(n.push({type:"response.output_text.delta",delta:l.trim()}),l="");continue}c.type!=="response.usage.complete"&&n.push(c)}for(const[,c]of r)if(c.arguments.trim()&&c.arguments.trim().length>2){const d=a.get(c.callId)||c.name||"unknown";n.push({type:"response.function_call.complete",data:{name:d,arguments:c.arguments,call_id:c.callId}})}return l.trim()&&n.push({type:"response.output_text.delta",delta:l.trim()}),n}function wR(e){switch(e.type){case"response.output_text.delta":if("delta"in e){const n=e.delta||"";return n.length>60?`${n.slice(0,60)}...`:n}return"Text output";case"response.function_call.complete":if("data"in e&&e.data){const n=e.data;let r=n.name||"unknown";(!r||r==="unknown")&&(r="function_call");const a=n.arguments?typeof n.arguments=="string"?n.arguments.slice(0,30):JSON.stringify(n.arguments).slice(0,30):"";return`Calling ${r}(${a}${a.length>=30?"...":""})`}return"Function call";case"response.function_call_arguments.delta":return"delta"in e&&e.delta?`Function arg delta: ${e.delta.slice(0,30)}${e.delta.length>30?"...":""}`:"Function arguments...";case"response.function_result.complete":{const r=e.output.slice(0,40);return`Function result: ${r}${r.length>=40?"...":""}`}case"response.output_item.added":{const n=e;return n.item.type==="function_call"?`Tool call: ${n.item.name}`:"Output item added"}case"response.workflow_event.completed":return"data"in e&&e.data?`Executor: ${e.data.executor_id||"unknown"}`:"Workflow event";case"response.trace.completed":return"data"in e&&e.data?`Trace: ${e.data.operation_name||"unknown"}`:"Trace event";case"response.completed":if("response"in e&&e.response&&"usage"in e.response){const r=e.response.usage;if(r)return`Response complete (${r.total_tokens} tokens)`}return"Response complete";case"response.done":return"Response complete";case"error":return"message"in e&&typeof e.message=="string"?e.message:"Error occurred";default:return`${e.type}`}}function NR(e){switch(e){case"response.output_text.delta":return eg;case"response.function_call.complete":case"response.function_call.delta":case"response.function_call_arguments.delta":return _a;case"response.function_result.complete":return nn;case"response.output_item.added":return nn;case"response.workflow_event.completed":return Qp;case"response.trace.completed":return Bu;case"response.completed":return nn;case"response.done":return nn;case"error":return kl;default:return hs}}function jR(e){switch(e){case"response.output_text.delta":return"text-gray-600 dark:text-gray-400";case"response.function_call.complete":case"response.function_call.delta":case"response.function_call_arguments.delta":return"text-blue-600 dark:text-blue-400";case"response.function_result.complete":return"text-green-600 dark:text-green-400";case"response.output_item.added":return"text-green-600 dark:text-green-400";case"response.workflow_event.completed":return"text-purple-600 dark:text-purple-400";case"response.trace.completed":return"text-orange-600 dark:text-orange-400";case"response.completed":return"text-green-600 dark:text-green-400";case"response.done":return"text-green-600 dark:text-green-400";case"error":return"text-red-600 dark:text-red-400";default:return"text-gray-600 dark:text-gray-400"}}function SR({event:e}){const[n,r]=w.useState(!1),a=e.type||"unknown",l=NR(a),c=jR(a),d="_uiTimestamp"in e&&typeof e._uiTimestamp=="number"?new Date(e._uiTimestamp*1e3).toLocaleTimeString():new Date().toLocaleTimeString(),f=wR(e),m=e.type==="response.function_call.complete"&&"data"in e&&e.data||e.type==="response.function_result.complete"||e.type==="response.output_item.added"&&zr(e)!==null||e.type==="response.workflow_event.completed"&&"data"in e&&e.data||e.type==="response.trace.completed"&&"data"in e&&e.data||e.type==="response.trace.completed"&&"data"in e&&e.data||e.type==="response.output_text.delta"&&"delta"in e&&e.delta&&e.delta.length>100||e.type==="response.completed"&&"response"in e&&e.response||e.type==="error";return o.jsxs("div",{className:"border-l-2 border-muted pl-3 py-2 hover:bg-muted/50 transition-colors",children:[o.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground mb-1",children:[o.jsx(l,{className:`h-3 w-3 ${c}`}),o.jsx("span",{className:"font-mono",children:d}),o.jsx(ut,{variant:"outline",className:"text-xs py-0",children:e.type?e.type.replace("response.",""):"unknown"})]}),o.jsxs("div",{className:"text-sm",children:[o.jsxs("div",{className:`flex items-center gap-2 ${m?"cursor-pointer":""}`,onClick:()=>m&&r(!n),children:[m&&o.jsx("div",{className:"text-muted-foreground",children:n?o.jsx(Rt,{className:"h-3 w-3"}):o.jsx(en,{className:"h-3 w-3"})}),o.jsx("div",{className:"text-muted-foreground flex-1",children:m&&f.length>80?`${f.slice(0,80)}...`:f})]}),n&&m&&o.jsx("div",{className:"mt-2 ml-5 p-3 bg-muted/30 rounded border",children:o.jsx(_R,{event:e})})]})]})}function _R({event:e}){if(e.type==="error"){const n=e;return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(kl,{className:"h-4 w-4 text-red-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Error Details"})]}),o.jsxs("div",{className:"text-xs",children:[n.message&&o.jsxs("div",{className:"mb-2",children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Message:"}),o.jsx("div",{className:"mt-1",children:o.jsx("pre",{className:"text-xs bg-destructive/10 border border-destructive/30 rounded p-2 text-destructive whitespace-pre-wrap break-all",children:n.message})})]}),n.code&&o.jsxs("div",{className:"mb-2",children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Code:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.code})]}),n.param&&o.jsxs("div",{className:"mb-2",children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Parameter:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.param})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Raw Event:"}),o.jsx("div",{className:"mt-1",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap break-all max-h-32 overflow-auto",children:JSON.stringify(e,null,2)})})]})]})]})}switch(e.type){case"response.function_call.complete":if("data"in e&&e.data){const n=e.data;return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(_a,{className:"h-4 w-4 text-blue-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Function Call"})]}),o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Function:"}),o.jsx("span",{className:"ml-2 font-mono bg-blue-100 dark:bg-blue-900 px-2 py-1 rounded",children:n.name||"unknown"})]}),n.call_id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Call ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.call_id})]}),n.arguments&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Arguments:"}),o.jsx("div",{className:"mt-1 max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all",children:typeof n.arguments=="string"?n.arguments:JSON.stringify(n.arguments,null,1)})})]})]})]})}break;case"response.function_result.complete":{const n=e;return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(nn,{className:"h-4 w-4 text-green-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Function Result"})]}),o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Call ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.call_id})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Status:"}),o.jsx("span",{className:`ml-2 px-2 py-1 rounded text-xs font-medium ${n.status==="completed"?"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200":"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"}`,children:n.status})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Output:"}),o.jsx("div",{className:"mt-1 max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all",children:n.output})})]})]})]})}case"response.output_item.added":{const n=zr(e);if(n)return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(nn,{className:"h-4 w-4 text-green-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Function Result"})]}),o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Call ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.call_id})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Status:"}),o.jsx("span",{className:`ml-2 px-2 py-1 rounded text-xs font-medium ${n.status==="completed"?"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200":"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"}`,children:n.status})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Output:"}),o.jsx("div",{className:"mt-1 max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all",children:n.output})})]})]})]});break}case"response.workflow_event.completed":if("data"in e&&e.data){const n=e.data;return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Qp,{className:"h-4 w-4 text-purple-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Workflow Event"})]}),o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Event Type:"}),o.jsx("span",{className:"ml-2 font-mono bg-purple-100 dark:bg-purple-900 px-2 py-1 rounded",children:n.event_type||"unknown"})]}),n.executor_id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Executor:"}),o.jsx("span",{className:"ml-2 font-mono",children:n.executor_id})]}),n.timestamp&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Timestamp:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.timestamp})]}),n.data&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Data:"}),o.jsx("div",{className:"mt-1 max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all",children:typeof n.data=="string"?n.data:JSON.stringify(n.data,null,1)})})]})]})]})}break;case"response.trace.completed":if("data"in e&&e.data){const n=e.data;return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Bu,{className:"h-4 w-4 text-orange-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Trace Event"})]}),o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Operation:"}),o.jsx("span",{className:"ml-2 font-mono bg-orange-100 dark:bg-orange-900 px-2 py-1 rounded",children:n.operation_name||"unknown"})]}),n.span_id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Span ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.span_id})]}),n.trace_id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Trace ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.trace_id})]}),n.duration_ms&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Duration:"}),o.jsxs("span",{className:"ml-2 font-mono text-xs",children:[Number(n.duration_ms).toFixed(2),"ms"]})]}),n.status&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Status:"}),o.jsx("span",{className:`ml-2 px-2 py-1 rounded text-xs font-medium ${n.status==="StatusCode.UNSET"||n.status==="OK"?"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200":"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"}`,children:n.status||"unknown"})]}),n.entity_id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Entity:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.entity_id})]}),n.attributes&&Object.keys(n.attributes).length>0&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Attributes:"}),o.jsx("div",{className:"mt-1 max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap break-all",children:l2(n.attributes)})})]})]})]})}break;case"response.output_text.delta":if("delta"in e&&e.delta)return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(eg,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Text Output"})]}),o.jsx("div",{className:"max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all",children:e.delta})})]});break;case"response.completed":if("response"in e&&e.response){const r=e.response;return o.jsx("div",{className:"space-y-2",children:o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[r.usage&&o.jsxs(o.Fragment,{children:[o.jsx("div",{children:o.jsx("span",{className:"font-medium text-muted-foreground",children:"Usage:"})}),o.jsxs("div",{className:"ml-4 space-y-1",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Input tokens:"}),o.jsx("span",{className:"ml-2 font-mono",children:r.usage.input_tokens})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Output tokens:"}),o.jsx("span",{className:"ml-2 font-mono",children:r.usage.output_tokens})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Total tokens:"}),o.jsx("span",{className:"ml-2 font-mono bg-green-100 dark:bg-green-900 px-2 py-1 rounded",children:r.usage.total_tokens})]})]})]}),r.id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Response ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs break-all",children:r.id})]}),r.model&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Model:"}),o.jsx("span",{className:"ml-2 font-mono text-xs break-all",children:r.model})]})]})})}break;default:return o.jsx("div",{className:"text-xs text-muted-foreground",children:o.jsx("pre",{className:"bg-background border rounded p-2 overflow-auto max-h-32",children:JSON.stringify(e,null,2)})})}return null}function ER({events:e,isStreaming:n}){const r=w.useRef(null),a=gg(e),c=[...i2(a)].reverse();return o.jsxs("div",{className:"h-full flex flex-col",children:[o.jsxs("div",{className:"flex items-center justify-between p-3 border-b",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Qp,{className:"h-4 w-4"}),o.jsx("span",{className:"font-medium",children:"Events"}),o.jsxs(ut,{variant:"outline",children:[a.length,e.length>a.length?` (${e.length} raw)`:""]})]}),n&&o.jsxs("div",{className:"flex items-center gap-1 text-xs text-muted-foreground",children:[o.jsx("div",{className:"h-2 w-2 animate-pulse rounded-full bg-green-500 dark:bg-green-400"}),"Streaming"]})]}),o.jsx(Wn,{ref:r,className:"flex-1",children:o.jsx("div",{className:"p-3",children:a.length===0?o.jsx("div",{className:"text-center text-muted-foreground text-sm py-8",children:e.length===0?"No events yet. Start a conversation to see real-time events.":"Processing events... Accumulated events will appear here."}):o.jsx("div",{className:"space-y-2",children:c.map((d,f)=>"type"in d&&d.type==="separator"?o.jsx(a2,{},d.id):o.jsx(SR,{event:d},`${d.type}-${f}`))})})})]})}function CR(e){const n=new Map;for(const a of e){if(!("data"in a))continue;const c=a.data.response_id||"unknown";n.has(c)||n.set(c,[]),n.get(c).push(a)}const r=[];for(const[a,l]of n){const c=new Map,d=[];for(const y of l){if(!("data"in y))continue;const b=y.data,j=b.span_id||`span_${Math.random()}`;c.set(j,{event:y,data:b,children:[]})}for(const y of l){if(!("data"in y))continue;const b=y.data,j=b.span_id||"",N=b.parent_span_id,S=c.get(j);S&&(N&&c.has(N)?c.get(N).children.push(S):d.push(S))}d.sort((y,b)=>(y.data.start_time||0)-(b.data.start_time||0));const f=y=>{y.children.sort((b,j)=>(b.data.start_time||0)-(j.data.start_time||0)),y.children.forEach(f)};d.forEach(f);const m=l[0],h=m&&"data"in m?m.data:null,g=Math.min(...l.map(y=>("data"in y?y.data:null)?.start_time||Date.now()/1e3)),x=l.reduce((y,b)=>{const j="data"in b?b.data:null;return y+(j?.duration_ms||0)},0);r.push({response_id:a,timestamp:g,traces:d,totalDuration:x,entity_id:h?.entity_id})}return r.sort((a,l)=>l.timestamp-a.timestamp),r}function Su(e){if(typeof e=="string"){const n=e.trim();if(n.startsWith("[")||n.startsWith("{"))try{const r=JSON.parse(e);return Su(r)}catch{return e}return e}if(Array.isArray(e))return e.map(Su);if(e!==null&&typeof e=="object"){const n={};for(const[r,a]of Object.entries(e))n[r]=Su(a);return n}return e}function l2(e){try{const n=Su(e);return JSON.stringify(n,null,2)}catch{return JSON.stringify(e,null,2)}}function kR(e){return e.includes("invoke_agent")||e.includes("Agent")?"bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200":e.includes("chat")||e.includes("Chat")?"bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200":e.includes("tool")||e.includes("execute")?"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200":"bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200"}function c2({node:e,depth:n=0}){const[r,a]=w.useState(n<2),[l,c]=w.useState(!1),{data:d}=e,f=d.operation_name||"Unknown",m=d.duration_ms?`${Number(d.duration_ms).toFixed(1)}ms`:"",h=e.children.length>0,g=d.attributes?.["gen_ai.usage.input_tokens"],x=d.attributes?.["gen_ai.usage.output_tokens"],y=g!==void 0||x!==void 0;return o.jsxs("div",{className:"relative",children:[n>0&&o.jsx("div",{className:"absolute left-0 top-0 bottom-0 border-l-2 border-muted",style:{marginLeft:`${(n-1)*16+8}px`}}),o.jsxs("div",{className:"flex items-center gap-2 py-1.5 hover:bg-muted/50 rounded transition-colors",style:{paddingLeft:`${n*16}px`},children:[o.jsx("button",{onClick:()=>h?a(!r):c(!l),className:"w-4 h-4 flex items-center justify-center text-muted-foreground hover:text-foreground",children:h?r?o.jsx(Rt,{className:"h-3 w-3"}):o.jsx(en,{className:"h-3 w-3"}):l?o.jsx(Rt,{className:"h-3 w-3"}):o.jsx(en,{className:"h-3 w-3"})}),o.jsx("span",{className:`text-xs px-1.5 py-0.5 rounded font-medium ${kR(f)}`,children:f.replace("ChatAgent.","").replace("invoke_agent ","")}),m&&o.jsx("span",{className:"text-xs text-muted-foreground font-mono",children:m}),y&&o.jsxs("span",{className:"text-xs text-muted-foreground font-mono",children:[g!==void 0&&o.jsxs("span",{children:["↑",String(g)]}),g!==void 0&&x!==void 0&&o.jsx("span",{className:"mx-0.5",children:"/"}),x!==void 0&&o.jsxs("span",{children:["↓",String(x)]})]})]}),l&&!h&&o.jsx("div",{className:"ml-4 mt-1 mb-2 p-2 bg-muted/30 rounded border text-xs",style:{marginLeft:`${n*16+20}px`},children:o.jsxs("div",{className:"space-y-1",children:[d.span_id&&o.jsxs("div",{className:"flex gap-2",children:[o.jsx("span",{className:"text-muted-foreground w-20",children:"Span ID:"}),o.jsx("span",{className:"font-mono text-xs break-all",children:d.span_id})]}),d.trace_id&&o.jsxs("div",{className:"flex gap-2",children:[o.jsx("span",{className:"text-muted-foreground w-20",children:"Trace ID:"}),o.jsx("span",{className:"font-mono text-xs break-all",children:d.trace_id})]}),d.status&&o.jsxs("div",{className:"flex gap-2",children:[o.jsx("span",{className:"text-muted-foreground w-20",children:"Status:"}),o.jsx("span",{className:`px-1.5 py-0.5 rounded text-xs ${d.status==="StatusCode.UNSET"||d.status==="OK"?"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200":"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"}`,children:d.status})]}),d.attributes&&Object.keys(d.attributes).length>0&&o.jsxs("div",{className:"mt-2",children:[o.jsx("span",{className:"text-muted-foreground block mb-1",children:"Attributes:"}),o.jsx("pre",{className:"text-xs bg-background border rounded p-2 overflow-auto max-h-32 whitespace-pre-wrap break-all",children:l2(d.attributes)})]})]})}),h&&r&&o.jsx("div",{children:e.children.map((b,j)=>o.jsx(c2,{node:b,depth:n+1},b.data.span_id||j))})]})}function TR({group:e}){const[n,r]=w.useState(!0),a=new Date(e.timestamp*1e3).toLocaleTimeString(),l=e.totalDuration>0?`${e.totalDuration.toFixed(0)}ms`:"",c=e.traces.reduce((d,f)=>{const m=h=>1+h.children.reduce((g,x)=>g+m(x),0);return d+m(f)},0);return o.jsxs("div",{className:"border rounded-lg overflow-hidden",children:[o.jsxs("div",{className:"flex items-center gap-2 p-2 bg-muted/50 cursor-pointer hover:bg-muted/70 transition-colors",onClick:()=>r(!n),children:[o.jsx("div",{className:"text-muted-foreground",children:n?o.jsx(Rt,{className:"h-4 w-4"}):o.jsx(en,{className:"h-4 w-4"})}),o.jsx("span",{className:"font-mono text-xs text-muted-foreground",children:a}),e.entity_id&&o.jsx(ut,{variant:"outline",className:"text-xs py-0",children:e.entity_id.replace("agent_","").replace("workflow_","")}),o.jsx("div",{className:"flex-1"}),l&&o.jsx(ut,{variant:"secondary",className:"text-xs py-0",children:l}),o.jsxs("span",{className:"text-xs text-muted-foreground",children:[c," span",c!==1?"s":""]})]}),n&&o.jsx("div",{className:"p-2 border-t",children:e.traces.map((d,f)=>o.jsx(c2,{node:d,depth:0},d.data.span_id||f))})]})}function AR({events:e}){const n=le(c=>c.debugTraceSubTab),r=le(c=>c.setDebugTraceSubTab),a=e.filter(c=>c.type==="response.trace.completed"),l=CR(a);return o.jsxs("div",{className:"h-full flex flex-col",children:[o.jsxs("div",{className:"flex items-center gap-2 p-3 border-b",children:[o.jsx(Bu,{className:"h-4 w-4"}),o.jsx("span",{className:"font-medium",children:"Traces"}),o.jsx(ut,{variant:"outline",children:a.length}),o.jsx("div",{className:"flex-1"}),o.jsxs("div",{className:"flex items-center bg-muted rounded-md p-1 min-w-0",children:[o.jsx("button",{onClick:()=>r("spans"),className:`px-3 py-1.5 text-xs rounded transition-colors truncate ${n==="spans"?"bg-background shadow-sm font-medium":"text-muted-foreground hover:text-foreground"}`,children:"OTel Spans"}),o.jsxs("button",{onClick:()=>r("context"),className:`px-3 py-1.5 text-xs rounded transition-colors flex items-center gap-1.5 min-w-0 ${n==="context"?"bg-background shadow-sm font-medium":"text-muted-foreground hover:text-foreground"}`,children:[o.jsx(ha,{className:"h-3.5 w-3.5 flex-shrink-0"}),o.jsx("span",{className:"truncate",children:"Context Inspector"})]})]})]}),n==="spans"?o.jsxs("div",{className:"flex-1 flex flex-col min-h-0",children:[a.length>0&&o.jsx("div",{className:"p-3 border-b flex-shrink-0",children:o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Bu,{className:"h-4 w-4"}),o.jsx("span",{className:"font-medium text-sm",children:"OTel Spans"}),o.jsxs(ut,{variant:"outline",className:"text-xs",children:[l.length," turn",l.length!==1?"s":""]})]})}),a.length===0?o.jsxs("div",{className:"flex flex-col items-center text-center p-6 pt-9",children:[o.jsx(ha,{className:"h-8 w-8 text-muted-foreground mb-3"}),o.jsx("div",{className:"text-sm font-medium mb-1",children:"No Data"}),o.jsxs("div",{className:"text-xs text-muted-foreground max-w-[200px]",children:["Run"," ",o.jsx("span",{className:"font-mono bg-accent/10 px-1 rounded",children:"devui --instrumentation"})," ","and start a conversation."]})]}):o.jsx(Wn,{className:"flex-1",children:o.jsx("div",{className:"p-3",children:o.jsx("div",{className:"space-y-3",children:l.map(c=>o.jsx(TR,{group:c},c.response_id))})})})]}):o.jsx(bR,{events:e})]})}function MR({events:e}){const n=gg(e),r=[],a=n.filter(m=>m.type==="response.function_call.complete"),l=e.filter(m=>zr(m)!==null),c=new Map;l.forEach(m=>{const h=zr(m);h&&c.set(h.call_id,m)}),a.forEach(m=>{if(r.push(m),"data"in m&&m.data&&m.data.call_id){const h=String(m.data.call_id),g=c.get(h);g&&(r.push(g),c.delete(h))}}),c.forEach(m=>{r.push(m)});const f=[...i2(r)].reverse();return o.jsxs("div",{className:"h-full flex flex-col",children:[o.jsxs("div",{className:"flex items-center gap-2 p-3 border-b",children:[o.jsx(_a,{className:"h-4 w-4"}),o.jsx("span",{className:"font-medium",children:"Tools"}),o.jsx(ut,{variant:"outline",children:r.length})]}),o.jsx(Wn,{className:"flex-1",children:o.jsx("div",{className:"p-3",children:r.length===0?o.jsx("div",{className:"text-center text-muted-foreground text-sm py-8",children:"No tool executions yet. Tool calls will appear here during conversations."}):o.jsx("div",{className:"space-y-3",children:f.map((m,h)=>"type"in m&&m.type==="separator"?o.jsx(a2,{},m.id):o.jsx(RR,{event:m},h))})})})]})}function RR({event:e}){const n="_uiTimestamp"in e&&typeof e._uiTimestamp=="number"?new Date(e._uiTimestamp*1e3).toLocaleTimeString():new Date().toLocaleTimeString(),r=e.type==="response.function_call.complete",a=zr(e),l=a!==null;if(!r&&!l)return null;const c=r&&"data"in e?e.data:null;return o.jsxs("div",{className:"border rounded p-3",children:[o.jsxs("div",{className:"flex items-center justify-between mb-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(og,{className:"h-4 w-4 text-yellow-600 dark:text-yellow-400"}),o.jsx("span",{className:"font-medium text-sm",children:r?"Tool Call":"Tool Result"}),r&&c&&c.name!==void 0&&o.jsxs("span",{className:"text-xs text-muted-foreground",children:["(",String(c.name),")"]})]}),o.jsx("span",{className:"text-xs text-muted-foreground font-mono",children:n})]}),r&&c&&o.jsxs("div",{className:"p-2 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded",children:[o.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[o.jsx(_a,{className:"h-3 w-3 text-blue-600 dark:text-blue-400"}),o.jsx("span",{className:"text-xs font-mono bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded",children:"CALL"}),o.jsx("span",{className:"font-medium text-sm",children:String(c.name||"unknown")})]}),c.arguments!==void 0&&o.jsxs("div",{className:"text-xs",children:[o.jsx("span",{className:"text-muted-foreground mb-1 block",children:"Arguments:"}),o.jsx("pre",{className:"p-2 bg-background border rounded text-xs overflow-auto max-h-32 max-w-full break-all whitespace-pre-wrap",children:typeof c.arguments=="string"?c.arguments:JSON.stringify(c.arguments,null,1)})]})]}),l&&a&&o.jsxs("div",{className:"p-2 bg-green-50 dark:bg-green-950/50 border border-green-200 dark:border-green-800 rounded",children:[o.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[o.jsx(nn,{className:"h-3 w-3 text-green-600 dark:text-green-400"}),o.jsx("span",{className:"text-xs font-mono bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded",children:"RESULT"}),a.status!=="completed"&&o.jsx("span",{className:"ml-auto px-2 py-1 rounded text-xs font-medium bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200",children:a.status})]}),o.jsxs("div",{className:"text-xs space-y-1",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("span",{className:"text-muted-foreground",children:"Call ID:"}),o.jsx("span",{className:"font-mono text-xs break-all",children:a.call_id})]}),o.jsxs("div",{children:[o.jsx("span",{className:"text-muted-foreground block mb-1",children:"Output:"}),o.jsx("pre",{className:"p-2 bg-background border rounded text-xs overflow-auto max-h-32 break-all whitespace-pre-wrap",children:a.output})]})]})]})]})}function DR({events:e,isStreaming:n=!1,onMinimize:r}){const a=le(d=>d.debugPanelTab),l=le(d=>d.setDebugPanelTab),c=w.useMemo(()=>{const d=gg(e),f=d.length,m=e.filter(g=>g.type==="response.trace.completed").length,h=d.filter(g=>g.type==="response.function_call.complete").length+e.filter(g=>zr(g)!==null).length;return{eventsCount:f,tracesCount:m,toolsCount:h}},[e]);return o.jsx("div",{className:"flex-1 border-l flex flex-col min-h-0",children:o.jsxs(D5,{value:a,onValueChange:d=>l(d),className:"flex-1 flex flex-col min-h-0",children:[o.jsxs("div",{className:"px-3 pt-3 flex items-center gap-2 flex-shrink-0",children:[o.jsxs($N,{className:"flex-1",children:[o.jsxs(Nu,{value:"events",className:"flex-1 gap-1.5",children:["Events",c.eventsCount>0&&o.jsx("span",{className:"text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center",children:c.eventsCount})]}),o.jsxs(Nu,{value:"traces",className:"flex-1 gap-1.5",children:["Traces",c.tracesCount>0&&o.jsx("span",{className:"text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center",children:c.tracesCount})]}),o.jsxs(Nu,{value:"tools",className:"flex-1 gap-1.5",children:["Tools",c.toolsCount>0&&o.jsx("span",{className:"text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center",children:c.toolsCount})]})]}),r&&o.jsx(Le,{variant:"ghost",size:"sm",onClick:r,className:"h-8 w-8 p-0 flex-shrink-0",title:"Minimize debug panel",children:o.jsx(en,{className:"h-4 w-4"})})]}),o.jsx(ju,{value:"events",className:"flex-1 mt-0 overflow-hidden",children:o.jsx(ER,{events:e,isStreaming:n})}),o.jsx(ju,{value:"traces",className:"flex-1 mt-0 overflow-hidden",children:o.jsx(AR,{events:e})}),o.jsx(ju,{value:"tools",className:"flex-1 mt-0 overflow-hidden",children:o.jsx(MR,{events:e})})]})})}function Ir({open:e,onOpenChange:n,children:r}){if(!e)return null;const a=()=>{n(!1)},l=d=>{d.stopPropagation()},c=d=>{d.stopPropagation()};return o.jsxs("div",{className:"fixed inset-0 z-50 flex items-center justify-center",children:[o.jsx("div",{className:"absolute inset-0 bg-black/50",onClick:a}),o.jsx("div",{className:"relative z-10",onClick:l,onMouseDown:c,onMouseUp:d=>d.stopPropagation(),children:r})]})}function Lr({children:e,className:n=""}){const a=n.includes("w-[")||n.includes("w-full")||n.includes("max-w-")?"":"max-w-lg w-full";return o.jsx("div",{className:`relative bg-background border rounded-lg shadow-lg max-h-[90vh] overflow-hidden ${a} ${n}`,children:e})}function $r({children:e,className:n=""}){return o.jsx("div",{className:`space-y-2 ${n}`,children:e})}function Pr({children:e,className:n=""}){return o.jsx("h2",{className:`text-lg font-semibold ${n}`,children:e})}function OR({children:e,className:n=""}){return o.jsx("p",{className:`text-sm text-muted-foreground ${n}`,children:e})}function So({onClose:e}){return o.jsx(Le,{variant:"ghost",size:"sm",onClick:e,className:"absolute top-4 right-4 h-8 w-8 p-0 rounded-sm opacity-70 hover:opacity-100",children:o.jsx(Ea,{className:"h-4 w-4"})})}function zR({children:e}){return o.jsx("div",{className:"flex justify-end gap-2 p-4 border-t bg-muted/50",children:e})}function as({className:e,type:n,...r}){return o.jsx("input",{type:n,"data-slot":"input",className:We("file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm","focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]","aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",e),...r})}var IR="Label",u2=w.forwardRef((e,n)=>o.jsx(Ye.label,{...e,ref:n,onMouseDown:r=>{r.target.closest("button, input, select, textarea")||(e.onMouseDown?.(r),!r.defaultPrevented&&r.detail>1&&r.preventDefault())}}));u2.displayName=IR;var LR=u2;function kt({className:e,...n}){return o.jsx(LR,{"data-slot":"label",className:We("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",e),...n})}var Td="Switch",[$R,e$]=Kn(Td),[PR,HR]=$R(Td),d2=w.forwardRef((e,n)=>{const{__scopeSwitch:r,name:a,checked:l,defaultChecked:c,required:d,disabled:f,value:m="on",onCheckedChange:h,form:g,...x}=e,[y,b]=w.useState(null),j=rt(n,E=>b(E)),N=w.useRef(!1),S=y?g||!!y.closest("form"):!0,[_,A]=Ar({prop:l,defaultProp:c??!1,onChange:h,caller:Td});return o.jsxs(PR,{scope:r,checked:_,disabled:f,children:[o.jsx(Ye.button,{type:"button",role:"switch","aria-checked":_,"aria-required":d,"data-state":p2(_),"data-disabled":f?"":void 0,disabled:f,value:m,...x,ref:j,onClick:ke(e.onClick,E=>{A(M=>!M),S&&(N.current=E.isPropagationStopped(),N.current||E.stopPropagation())})}),S&&o.jsx(h2,{control:y,bubbles:!N.current,name:a,value:m,checked:_,required:d,disabled:f,form:g,style:{transform:"translateX(-100%)"}})]})});d2.displayName=Td;var f2="SwitchThumb",m2=w.forwardRef((e,n)=>{const{__scopeSwitch:r,...a}=e,l=HR(f2,r);return o.jsx(Ye.span,{"data-state":p2(l.checked),"data-disabled":l.disabled?"":void 0,...a,ref:n})});m2.displayName=f2;var UR="SwitchBubbleInput",h2=w.forwardRef(({__scopeSwitch:e,control:n,checked:r,bubbles:a=!0,...l},c)=>{const d=w.useRef(null),f=rt(d,c),m=fg(r),h=Lp(n);return w.useEffect(()=>{const g=d.current;if(!g)return;const x=window.HTMLInputElement.prototype,b=Object.getOwnPropertyDescriptor(x,"checked").set;if(m!==r&&b){const j=new Event("click",{bubbles:a});b.call(g,r),g.dispatchEvent(j)}},[m,r,a]),o.jsx("input",{type:"checkbox","aria-hidden":!0,defaultChecked:r,...l,tabIndex:-1,ref:f,style:{...l.style,...h,position:"absolute",pointerEvents:"none",opacity:0,margin:0}})});h2.displayName=UR;function p2(e){return e?"checked":"unchecked"}var g2=d2,BR=m2;const Wi=w.forwardRef(({className:e,...n},r)=>o.jsx(g2,{className:We("peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",e),...n,ref:r,children:o.jsx(BR,{className:We("pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0")})}));Wi.displayName=g2.displayName;const VR=["gpt-4.1","gpt-4.1-mini","o1","o1-mini","o3-mini"];function cb({open:e,onOpenChange:n,onBackendUrlChange:r}){const[a,l]=w.useState("general"),{oaiMode:c,setOAIMode:d,azureDeploymentEnabled:f,setAzureDeploymentEnabled:m,authRequired:h,serverCapabilities:g,serverVersion:x,runtime:y,uiMode:b,streamingEnabled:j,setStreamingEnabled:N}=le(),S="",[_,A]=w.useState(()=>localStorage.getItem("devui_backend_url")||S),[E,M]=w.useState(_),[T,D]=w.useState(!!localStorage.getItem("devui_auth_token")),[z,H]=w.useState(""),q=()=>{try{new URL(E),localStorage.setItem("devui_backend_url",E),A(E),r?.(E),n(!1),window.location.reload()}catch{alert("Please enter a valid URL (e.g., http://localhost:8080)")}},X=()=>{localStorage.removeItem("devui_backend_url"),M(S),A(S),r?.(S),window.location.reload()},W=()=>{z.trim()&&(localStorage.setItem("devui_auth_token",z.trim()),D(!0),H(""),window.location.reload())},G=()=>{localStorage.removeItem("devui_auth_token"),D(!1),H(""),window.location.reload()},ne=E!==_,B=!localStorage.getItem("devui_backend_url");return o.jsx(Ir,{open:e,onOpenChange:n,children:o.jsxs(Lr,{className:"w-[600px] max-w-[90vw] flex flex-col max-h-[85vh]",children:[o.jsx($r,{className:"p-6 pb-2 flex-shrink-0",children:o.jsx(Pr,{children:"Settings"})}),o.jsx(So,{onClose:()=>n(!1)}),o.jsxs("div",{className:"flex border-b px-6 flex-shrink-0",children:[o.jsxs("button",{onClick:()=>l("general"),className:`px-4 py-2 text-sm font-medium transition-colors relative ${a==="general"?"text-foreground":"text-muted-foreground hover:text-foreground"}`,children:["General",a==="general"&&o.jsx("div",{className:"absolute bottom-0 left-0 right-0 h-0.5 bg-primary"})]}),g.openai_proxy&&o.jsxs("button",{onClick:()=>l("proxy"),className:`px-4 py-2 text-sm font-medium transition-colors relative ${a==="proxy"?"text-foreground":"text-muted-foreground hover:text-foreground"}`,children:["OpenAI Proxy",a==="proxy"&&o.jsx("div",{className:"absolute bottom-0 left-0 right-0 h-0.5 bg-primary"})]}),o.jsxs("button",{onClick:()=>l("about"),className:`px-4 py-2 text-sm font-medium transition-colors relative ${a==="about"?"text-foreground":"text-muted-foreground hover:text-foreground"}`,children:["About",a==="about"&&o.jsx("div",{className:"absolute bottom-0 left-0 right-0 h-0.5 bg-primary"})]})]}),o.jsxs("div",{className:"px-6 pb-6 overflow-y-auto flex-1 min-h-[400px]",children:[a==="general"&&o.jsxs("div",{className:"space-y-6 pt-4",children:[o.jsxs("div",{className:"space-y-3",children:[o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsx(kt,{htmlFor:"backend-url",className:"text-sm font-medium",children:"Backend URL"}),!B&&o.jsxs(Le,{variant:"ghost",size:"sm",onClick:X,className:"h-7 text-xs",title:"Reset to default",children:[o.jsx(sg,{className:"h-3 w-3 mr-1"}),"Reset"]})]}),o.jsx(as,{id:"backend-url",type:"url",value:E,onChange:U=>M(U.target.value),placeholder:"http://localhost:8080",className:"font-mono text-sm"}),o.jsxs("p",{className:"text-xs text-muted-foreground",children:["Default: ",o.jsx("span",{className:"font-mono",children:S})]}),o.jsx("div",{className:"flex gap-2 pt-2 min-h-[36px]",children:ne&&o.jsxs(o.Fragment,{children:[o.jsx(Le,{onClick:q,size:"sm",className:"flex-1",children:"Apply & Reload"}),o.jsx(Le,{onClick:()=>M(_),variant:"outline",size:"sm",className:"flex-1",children:"Cancel"})]})})]}),(h||T)&&o.jsxs("div",{className:"space-y-3 border-t pt-6",children:[o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsx(kt,{className:"text-sm font-medium",children:"Authentication Token"}),!h&&T&&o.jsx("span",{className:"text-xs text-muted-foreground",children:"(Not required by current backend)"})]}),T?o.jsxs("div",{className:"space-y-3",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(as,{type:"password",value:"••••••••••••••••••••",disabled:!0,className:"font-mono text-sm flex-1"}),o.jsx(Le,{variant:"destructive",size:"sm",onClick:G,className:"flex-shrink-0",children:"Clear"})]}),o.jsx("p",{className:"text-xs text-green-600 dark:text-green-400",children:"✓ Token configured and stored locally"})]}):o.jsxs("div",{className:"space-y-3",children:[o.jsx(as,{type:"password",value:z,onChange:U=>H(U.target.value),placeholder:"Enter bearer token",className:"font-mono text-sm",onKeyDown:U=>{U.key==="Enter"&&z.trim()&&W()}}),o.jsx(Le,{onClick:W,size:"sm",disabled:!z.trim(),className:"w-full",children:"Save & Reload"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:h?"Required by backend (started with --auth flag)":"Not required by current backend"})]})]}),g.deployment&&o.jsxs("div",{className:"space-y-3 border-t pt-6",children:[o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{className:"space-y-0.5",children:[o.jsx(kt,{className:"text-sm font-medium",children:"Azure Deployment"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Enable one-click deployment to Azure Container Apps"})]}),o.jsx(Wi,{checked:f,onCheckedChange:m})]}),o.jsxs("details",{className:"group",children:[o.jsxs("summary",{className:"cursor-pointer text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1",children:[o.jsx(en,{className:"h-3 w-3 transition-transform group-open:rotate-90"}),"Learn more about Azure deployment"]}),o.jsxs("div",{className:"mt-3 space-y-3 pl-4",children:[o.jsx("p",{className:"text-xs text-muted-foreground leading-relaxed",children:'When enabled, agents that support deployment will show a "Deploy to Azure" button. This allows you to deploy your agent to Azure Container Apps directly from DevUI.'}),o.jsxs("div",{className:"space-y-1.5",children:[o.jsx("p",{className:"text-xs font-medium",children:"When enabled:"}),o.jsxs("ul",{className:"text-xs text-muted-foreground space-y-0.5 list-disc list-inside",children:[o.jsx("li",{children:'Shows "Deploy to Azure" for supported agents'}),o.jsx("li",{children:"Requires Azure CLI and proper authentication"}),o.jsx("li",{children:"Backend must have deployment capabilities enabled"})]})]}),o.jsxs("div",{className:"space-y-1.5",children:[o.jsx("p",{className:"text-xs font-medium",children:"When disabled:"}),o.jsxs("ul",{className:"text-xs text-muted-foreground space-y-0.5 list-disc list-inside",children:[o.jsx("li",{children:'Shows "Deployment Guide" for all agents'}),o.jsx("li",{children:"Provides Docker templates and manual deployment instructions"}),o.jsx("li",{children:"No backend deployment capabilities required"})]})]})]})]})]}),o.jsx("div",{className:"space-y-3 border-t pt-6",children:o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{className:"space-y-0.5",children:[o.jsx(kt,{className:"text-sm font-medium",children:"Show Tool Calls"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Display function/tool calls and results in chat messages"})]}),o.jsx(Wi,{checked:le.getState().showToolCalls,onCheckedChange:U=>le.getState().setShowToolCalls(U)})]})}),o.jsxs("div",{className:"space-y-3 border-t pt-6",children:[o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{className:"space-y-0.5",children:[o.jsx(kt,{className:"text-sm font-medium",children:"Streaming Mode"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Stream responses token-by-token as they're generated"})]}),o.jsx(Wi,{checked:j,onCheckedChange:N})]}),!j&&o.jsxs("div",{className:"flex items-start gap-2 text-xs text-amber-600 dark:text-amber-400 bg-amber-500/10 p-3 rounded",children:[o.jsx(Fs,{className:"h-3.5 w-3.5 flex-shrink-0 mt-0.5"}),o.jsxs("div",{children:[o.jsx("p",{className:"font-medium",children:"Non-streaming mode limitations:"}),o.jsxs("ul",{className:"mt-1 space-y-0.5 list-disc list-inside text-amber-600/80 dark:text-amber-400/80",children:[o.jsx("li",{children:"Tool calls won't display in real-time"}),o.jsx("li",{children:"No typing indicator during generation"}),o.jsx("li",{children:"Response appears all at once when complete"})]})]})]})]})]}),a==="proxy"&&g.openai_proxy&&o.jsxs("div",{className:"space-y-6 pt-4",children:[o.jsxs("div",{className:"space-y-4",children:[o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{className:"space-y-0.5",children:[o.jsx(kt,{className:"text-base font-medium",children:"OpenAI Proxy Mode"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Route requests through DevUI backend to OpenAI API"})]}),o.jsx(Wi,{checked:c.enabled,onCheckedChange:U=>d({...c,enabled:U})})]}),!c.enabled&&o.jsx("div",{className:"bordder border-muted bg-muted/30 rounded-lg p-4 space-y-3",children:o.jsxs("div",{className:"flex items-start gap-2",children:[o.jsx(Fs,{className:"h-4 w-4 flex-shrink-0 mt-0.5 text-blue-600 dark:text-blue-400"}),o.jsxs("div",{className:"space-y-2",children:[o.jsx("p",{className:"text-sm font-medium",children:"About OpenAI Proxy Mode"}),o.jsxs("p",{className:"text-xs text-muted-foreground leading-relaxed",children:["When enabled, your chat requests are sent to your DevUI backend"," ",o.jsxs("span",{className:"font-mono font-semibold",children:["(",_,")"]}),", which then forwards them to OpenAI's API. This keeps your"," ",o.jsx("span",{className:"font-mono font-semibold",children:"OPENAI_API_KEY"})," ","secure on the server instead of exposing it in the browser."]}),o.jsxs("div",{className:"space-y-1.5 pt-1",children:[o.jsx("p",{className:"text-xs font-medium",children:"Requirements:"}),o.jsxs("ul",{className:"text-xs text-muted-foreground space-y-0.5 list-disc list-inside",children:[o.jsxs("li",{children:["Backend must have"," ",o.jsx("span",{className:"font-mono",children:"OPENAI_API_KEY"})," ","configured"]}),o.jsx("li",{children:"Backend must support OpenAI Responses API proxying (DevUI does)"})]})]}),o.jsxs("div",{className:"space-y-1.5 pt-1",children:[o.jsx("p",{className:"text-xs font-medium",children:"Why use this?"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Quickly test and compare OpenAI models directly through the DevUI interface without creating custom agents or exposing API keys in the browser."})]})]})]})}),c.enabled&&o.jsxs("div",{className:"space-y-4 pl-4 border-l-2 border-muted",children:[o.jsxs("div",{className:"space-y-2",children:[o.jsx(kt,{className:"text-sm font-medium",children:"Model"}),o.jsx(as,{type:"text",value:c.model,onChange:U=>d({...c,model:U.target.value}),placeholder:"gpt-4.1-mini",className:"font-mono text-sm"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Enter any OpenAI model ID (e.g., gpt-4.1, o1, o3-mini)"})]}),o.jsxs("div",{className:"space-y-2",children:[o.jsx(kt,{className:"text-xs text-muted-foreground",children:"Common presets"}),o.jsx("div",{className:"flex flex-wrap gap-2",children:VR.map(U=>o.jsx(Le,{variant:c.model===U?"default":"outline",size:"sm",onClick:()=>d({...c,model:U}),className:"text-xs h-7",children:U},U))})]}),o.jsxs("details",{className:"group",children:[o.jsxs("summary",{className:"cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1",children:[o.jsx(en,{className:"h-3 w-3 transition-transform group-open:rotate-90"}),"Advanced Parameters (optional)"]}),o.jsxs("div",{className:"space-y-3 mt-3 pl-4",children:[o.jsxs("div",{className:"space-y-1",children:[o.jsx(kt,{className:"text-xs",children:"Temperature"}),o.jsx(as,{type:"number",step:"0.1",min:"0",max:"2",value:c.temperature??"",onChange:U=>d({...c,temperature:U.target.value?parseFloat(U.target.value):void 0}),placeholder:"1.0 (default)",className:"text-sm"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Controls randomness (0-2)"})]}),o.jsxs("div",{className:"space-y-1",children:[o.jsx(kt,{className:"text-xs",children:"Max Output Tokens"}),o.jsx(as,{type:"number",min:"1",value:c.max_output_tokens??"",onChange:U=>d({...c,max_output_tokens:U.target.value?parseInt(U.target.value):void 0}),placeholder:"Auto",className:"text-sm"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Maximum tokens in response"})]}),o.jsxs("div",{className:"space-y-1",children:[o.jsx(kt,{className:"text-xs",children:"Top P"}),o.jsx(as,{type:"number",step:"0.1",min:"0",max:"1",value:c.top_p??"",onChange:U=>d({...c,top_p:U.target.value?parseFloat(U.target.value):void 0}),placeholder:"1.0 (default)",className:"text-sm"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Nucleus sampling (0-1)"})]}),o.jsxs("div",{className:"space-y-1",children:[o.jsx(kt,{className:"text-xs",children:"Reasoning Effort (o-series models)"}),o.jsxs("select",{value:c.reasoning_effort??"",onChange:U=>d({...c,reasoning_effort:U.target.value?U.target.value:void 0}),className:"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",children:[o.jsx("option",{value:"",children:"Auto (default)"}),o.jsx("option",{value:"minimal",children:"Minimal"}),o.jsx("option",{value:"low",children:"Low"}),o.jsx("option",{value:"medium",children:"Medium"}),o.jsx("option",{value:"high",children:"High"})]}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Constrains reasoning effort (faster/cheaper vs thorough)"})]})]})]})]})]}),c.enabled&&o.jsxs("div",{className:"flex items-start gap-2 text-xs text-muted-foreground bg-muted/50 p-3 rounded",children:[o.jsx(Fs,{className:"h-3.5 w-3.5 flex-shrink-0 mt-0.5"}),o.jsx("div",{className:"space-y-1",children:o.jsxs("p",{children:["Requests route through"," ",o.jsx("span",{className:"font-mono font-semibold",children:_})," ","to OpenAI API. Server must have"," ",o.jsx("span",{className:"font-mono font-semibold",children:"OPENAI_API_KEY"})," ","configured."]})})]})]}),a==="about"&&o.jsxs("div",{className:"space-y-4 pt-4",children:[o.jsx("p",{className:"text-sm text-muted-foreground",children:"DevUI is a sample app for getting started with Agent Framework."}),o.jsxs("div",{className:"space-y-2 text-sm",children:[o.jsxs("div",{className:"flex justify-between",children:[o.jsx("span",{className:"text-muted-foreground",children:"Version:"}),o.jsx("span",{className:"font-mono",children:x||"Unknown"})]}),o.jsxs("div",{className:"flex justify-between",children:[o.jsx("span",{className:"text-muted-foreground",children:"Runtime:"}),o.jsx("span",{className:"font-mono capitalize",children:y||"Unknown"})]}),o.jsxs("div",{className:"flex justify-between",children:[o.jsx("span",{className:"text-muted-foreground",children:"UI Mode:"}),o.jsx("span",{className:"font-mono capitalize",children:b||"Unknown"})]})]}),(g||h!==void 0)&&o.jsxs("div",{className:"space-y-2 pt-2",children:[o.jsx("p",{className:"text-xs font-medium text-muted-foreground uppercase tracking-wide",children:"Capabilities"}),o.jsxs("div",{className:"space-y-1 text-sm",children:[g?.instrumentation!==void 0&&o.jsxs("div",{className:"flex justify-between items-center",children:[o.jsx("span",{className:"text-muted-foreground",children:"Instrumentation:"}),o.jsx("span",{className:`text-xs px-2 py-0.5 rounded-full ${g.instrumentation?"bg-green-500/10 text-green-600 dark:text-green-400":"bg-muted text-muted-foreground"}`,children:g.instrumentation?"Enabled":"Disabled"})]}),g?.openai_proxy!==void 0&&o.jsxs("div",{className:"flex justify-between items-center",children:[o.jsx("span",{className:"text-muted-foreground",children:"OpenAI Proxy:"}),o.jsx("span",{className:`text-xs px-2 py-0.5 rounded-full ${g.openai_proxy?"bg-green-500/10 text-green-600 dark:text-green-400":"bg-muted text-muted-foreground"}`,children:g.openai_proxy?"Available":"Not Configured"})]}),g?.deployment!==void 0&&o.jsxs("div",{className:"flex justify-between items-center",children:[o.jsx("span",{className:"text-muted-foreground",children:"Deployment:"}),o.jsx("span",{className:`text-xs px-2 py-0.5 rounded-full ${g.deployment?"bg-green-500/10 text-green-600 dark:text-green-400":"bg-muted text-muted-foreground"}`,children:g.deployment?"Available":"Disabled"})]}),h!==void 0&&o.jsxs("div",{className:"flex justify-between items-center",children:[o.jsx("span",{className:"text-muted-foreground",children:"Authentication:"}),o.jsx("span",{className:`text-xs px-2 py-0.5 rounded-full ${h?"bg-blue-500/10 text-blue-600 dark:text-blue-400":"bg-muted text-muted-foreground"}`,children:h?"Required":"Not Required"})]})]})]}),o.jsx("div",{className:"flex justify-center pt-2",children:o.jsxs(Le,{variant:"outline",size:"sm",onClick:()=>window.open("https://github.com/microsoft/agent-framework","_blank"),className:"text-xs",children:[o.jsx(Hu,{className:"h-3 w-3 mr-1"}),"Learn More about Agent Framework"]})})]})]})]})})}const qR="modulepreload",FR=function(e,n){return new URL(e,n).href},ub={},_u=function(n,r,a){let l=Promise.resolve();if(r&&r.length>0){let h=function(g){return Promise.all(g.map(x=>Promise.resolve(x).then(y=>({status:"fulfilled",value:y}),y=>({status:"rejected",reason:y}))))};const d=document.getElementsByTagName("link"),f=document.querySelector("meta[property=csp-nonce]"),m=f?.nonce||f?.getAttribute("nonce");l=h(r.map(g=>{if(g=FR(g,a),g in ub)return;ub[g]=!0;const x=g.endsWith(".css"),y=x?'[rel="stylesheet"]':"";if(a)for(let j=d.length-1;j>=0;j--){const N=d[j];if(N.href===g&&(!x||N.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${g}"]${y}`))return;const b=document.createElement("link");if(b.rel=x?"stylesheet":qR,x||(b.as="script"),b.crossOrigin="",b.href=g,m&&b.setAttribute("nonce",m),document.head.appendChild(b),x)return new Promise((j,N)=>{b.addEventListener("load",j),b.addEventListener("error",()=>N(new Error(`Unable to preload CSS for ${g}`)))})}))}function c(d){const f=new Event("vite:preloadError",{cancelable:!0});if(f.payload=d,window.dispatchEvent(f),!f.defaultPrevented)throw d}return l.then(d=>{for(const f of d||[])f.status==="rejected"&&c(f.reason);return n().catch(c)})},x2="devui_streaming_state_",y2=1440*60*1e3;function Yu(e){return`${x2}${e}`}function YR(e){let n="";for(const r of e)r.type==="response.output_text.delta"&&"delta"in r&&(n+=r.delta);return n}function v2(e){try{const n=Yu(e.conversationId),r=JSON.stringify(e);localStorage.setItem(n,r)}catch(n){console.error("Failed to save streaming state:",n);try{b2();const r=Yu(e.conversationId),a=JSON.stringify(e);localStorage.setItem(r,a)}catch{console.error("Failed to save streaming state even after cleanup")}}}function ba(e){try{const n=Yu(e),r=localStorage.getItem(n);if(!r)return null;const a=JSON.parse(r);return Date.now()-a.timestamp>y2?(Eu(e),null):a.completed?null:a}catch(n){return console.error("Failed to load streaming state:",n),null}}function Nh(e,n,r,a){try{const l=ba(e),c="sequence_number"in n?n.sequence_number:void 0,d=l?[...l.events,n]:[n],f={conversationId:e,responseId:r,lastMessageId:a,lastSequenceNumber:c??l?.lastSequenceNumber??-1,events:d,timestamp:Date.now(),completed:n.type==="response.completed"||n.type==="response.failed",accumulatedText:YR(d)};v2(f)}catch(l){console.error("Failed to update streaming state:",l)}}function jh(e){try{const n=ba(e);n&&(n.completed=!0,n.timestamp=Date.now(),v2(n))}catch(n){console.error("Failed to mark streaming as completed:",n)}}function Eu(e){try{const n=Yu(e);localStorage.removeItem(n)}catch(n){console.error("Failed to clear streaming state:",n)}}function b2(){try{const e=Object.keys(localStorage),n=Date.now();for(const r of e)if(r.startsWith(x2))try{const a=localStorage.getItem(r);if(a){const l=JSON.parse(a);(n-l.timestamp>y2||l.completed)&&localStorage.removeItem(r)}}catch{localStorage.removeItem(r)}}catch(e){console.error("Failed to clear expired streaming states:",e)}}function GR(){b2()}function w2(){const[e,n]=w.useState(!1),r=w.useRef(null),a=w.useCallback(()=>(r.current=new AbortController,n(!1),r.current.signal),[]),l=w.useCallback(()=>{r.current&&(n(!0),r.current.abort(),r.current=null)},[]),c=w.useCallback(()=>{n(!1)},[]),d=w.useCallback(()=>{r.current&&(r.current.abort(),r.current=null)},[]);return{isCancelling:e,createAbortSignal:a,handleCancel:l,resetCancelling:c,cleanup:d}}function Gu(e){return e instanceof DOMException&&e.name==="AbortError"}function XR(e={}){const{onDrop:n,disabled:r=!1}=e,[a,l]=w.useState(!1),[c,d]=w.useState([]),f=w.useRef(0),m=w.useCallback(b=>{b.preventDefault(),b.stopPropagation(),!r&&(f.current++,b.dataTransfer.items&&b.dataTransfer.items.length>0&&l(!0))},[r]),h=w.useCallback(b=>{b.preventDefault(),b.stopPropagation(),!r&&(f.current--,f.current===0&&l(!1))},[r]),g=w.useCallback(b=>{b.preventDefault(),b.stopPropagation()},[]),x=w.useCallback(b=>{if(b.preventDefault(),b.stopPropagation(),l(!1),f.current=0,r)return;const j=Array.from(b.dataTransfer.files);j.length>0&&(d(j),n?.(j))},[r,n]),y=w.useCallback(()=>{d([])},[]);return{isDragOver:a,droppedFiles:c,clearDroppedFiles:y,dragHandlers:{onDragEnter:m,onDragLeave:h,onDragOver:g,onDrop:x}}}const ZR="",WR=1e3,Sh=10;function KR(){const e=localStorage.getItem("devui_backend_url");return e||ZR}function QR(e){return new Promise(n=>setTimeout(n,e))}class JR{baseUrl;authToken=null;constructor(n){this.baseUrl=n||KR(),this.authToken=localStorage.getItem("devui_auth_token")}setBaseUrl(n){this.baseUrl=n}getBaseUrl(){return this.baseUrl}setAuthToken(n){this.authToken=n,n?localStorage.setItem("devui_auth_token",n):localStorage.removeItem("devui_auth_token")}getAuthToken(){return this.authToken}clearAuthToken(){this.setAuthToken(null)}async request(n,r={}){const a=`${this.baseUrl}${n}`,l={"Content-Type":"application/json",...r.headers};this.authToken&&(l.Authorization=`Bearer ${this.authToken}`);const c=await fetch(a,{...r,headers:l});if(!c.ok){if(c.status===401)throw this.clearAuthToken(),new Error("UNAUTHORIZED");let d=`API request failed: ${c.status} ${c.statusText}`;try{const f=await c.json();f.detail?typeof f.detail=="string"?d=f.detail:typeof f.detail=="object"&&f.detail.error?.message&&(d=f.detail.error.message):f.error?.message&&(d=f.error.message)}catch{}throw new Error(d)}return c.json()}async getHealth(){return this.request("/health")}async getMeta(){return this.request("/meta")}async getEntities(){const r=(await this.request("/v1/entities")).entities.map(c=>{if(c.type==="agent")return{id:c.id,name:c.name,description:c.description,type:"agent",source:c.source||"directory",tools:(c.tools||[]).map(d=>typeof d=="string"?d:JSON.stringify(d)),has_env:!!(c.required_env_vars&&c.required_env_vars.length>0),module_path:typeof c.metadata?.module_path=="string"?c.metadata.module_path:void 0,required_env_vars:c.required_env_vars,metadata:c.metadata,deployment_supported:c.deployment_supported,deployment_reason:c.deployment_reason,instructions:c.instructions,model_id:c.model_id,chat_client_type:c.chat_client_type,context_providers:c.context_providers,middleware:c.middleware};{const d=c.executors||c.tools||[];let f=c.start_executor_id||"";if(!f&&d.length>0){const m=d[0];typeof m=="string"&&(f=m)}return{id:c.id,name:c.name,description:c.description,type:"workflow",source:c.source||"directory",executors:d.map(m=>typeof m=="string"?m:JSON.stringify(m)),has_env:!!(c.required_env_vars&&c.required_env_vars.length>0),module_path:typeof c.metadata?.module_path=="string"?c.metadata.module_path:void 0,required_env_vars:c.required_env_vars,metadata:c.metadata,deployment_supported:c.deployment_supported,deployment_reason:c.deployment_reason,input_schema:c.input_schema||{type:"string"},input_type_name:c.input_type_name||"Input",start_executor_id:f,tools:[]}}}),a=r.filter(c=>c.type==="agent"),l=r.filter(c=>c.type==="workflow");return{entities:r,agents:a,workflows:l}}async getAgents(){const{agents:n}=await this.getEntities();return n}async getWorkflows(){const{workflows:n}=await this.getEntities();return n}async getAgentInfo(n){return this.request(`/v1/entities/${n}/info?type=agent`)}async getWorkflowInfo(n){return this.request(`/v1/entities/${n}/info?type=workflow`)}async reloadEntity(n){return this.request(`/v1/entities/${n}/reload`,{method:"POST"})}async createConversation(n){const{oaiMode:r}=await _u(()=>Promise.resolve().then(()=>wu),void 0,import.meta.url).then(c=>({oaiMode:c.useDevUIStore.getState().oaiMode})),a={};r.enabled&&(a["X-Proxy-Backend"]="openai");const l=await this.request("/v1/conversations",{method:"POST",headers:a,body:JSON.stringify({metadata:n})});return{id:l.id,object:"conversation",created_at:l.created_at,metadata:l.metadata}}async listConversations(n){const r=n?`/v1/conversations?agent_id=${encodeURIComponent(n)}`:"/v1/conversations",a=await this.request(r);return{data:a.data.map(l=>({id:l.id,object:"conversation",created_at:l.created_at,metadata:l.metadata})),has_more:a.has_more}}async getConversation(n){const r=await this.request(`/v1/conversations/${n}`);return{id:r.id,object:"conversation",created_at:r.created_at,metadata:r.metadata}}async deleteConversation(n){try{return await this.request(`/v1/conversations/${n}`,{method:"DELETE"}),Eu(n),!0}catch{return!1}}async listConversationItems(n,r){const a=new URLSearchParams;r?.limit&&a.set("limit",r.limit.toString()),r?.after&&a.set("after",r.after),r?.order&&a.set("order",r.order);const l=a.toString(),c=`/v1/conversations/${n}/items${l?`?${l}`:""}`;return this.request(c)}async getConversationItem(n,r){const a=`/v1/conversations/${n}/items/${r}`;return this.request(a)}async deleteConversationItem(n,r){const a=await fetch(`${this.baseUrl}/v1/conversations/${n}/items/${r}`,{method:"DELETE"});if(!a.ok)throw new Error(`Failed to delete item: ${a.statusText}`)}async*streamOpenAIResponse(n,r,a,l){const{oaiMode:c}=await _u(()=>Promise.resolve().then(()=>wu),void 0,import.meta.url).then(x=>({oaiMode:x.useDevUIStore.getState().oaiMode}));c.enabled&&(n.model=c.model,c.temperature!==void 0&&(n.temperature=c.temperature),c.max_output_tokens!==void 0&&(n.max_output_tokens=c.max_output_tokens),c.top_p!==void 0&&(n.top_p=c.top_p),c.instructions!==void 0&&(n.instructions=c.instructions),c.reasoning_effort!==void 0&&(n.reasoning={effort:c.reasoning_effort}));let d=-1,f=0,m=!1,h=l,g;if(r){const x=ba(r);if(x)if(l||(h=x.responseId),d=x.lastSequenceNumber,g=x.lastMessageId,l)m=x.events.length>0;else for(const y of x.events)m=!0,yield y}for(;f<=Sh;)try{let x;if(h){const N=new URLSearchParams;N.set("stream","true"),d>=0&&N.set("starting_after",d.toString());const S=`${this.baseUrl}/v1/responses/${h}?${N.toString()}`,_={Accept:"text/event-stream"};this.authToken&&(_.Authorization=`Bearer ${this.authToken}`),x=await fetch(S,{method:"GET",headers:_,signal:a})}else{const N=`${this.baseUrl}/v1/responses`,S={"Content-Type":"application/json",Accept:"text/event-stream"};c.enabled&&(S["X-Proxy-Backend"]="openai"),this.authToken&&(S.Authorization=`Bearer ${this.authToken}`),x=await fetch(N,{method:"POST",headers:S,body:JSON.stringify(n),signal:a})}if(!x.ok){if(x.status===401)throw this.clearAuthToken(),new Error("UNAUTHORIZED");if(x.status>=400&&x.status<500){let S=`Client error ${x.status}`;try{const _=await x.json();_.error&&_.error.message?S=_.error.message:_.detail&&(S=_.detail)}catch{}throw new Error(`CLIENT_ERROR: ${S}`)}let N=`Request failed with status ${x.status}`;try{const S=await x.json();S.error&&S.error.message?N=S.error.message:S.detail&&(N=S.detail)}catch{}throw new Error(N)}const y=x.body?.getReader();if(!y)throw new Error("Response body is not readable");const b=new TextDecoder;let j="";try{for(;;){if(a?.aborted)throw new DOMException("Request aborted","AbortError");const{done:N,value:S}=await y.read();if(N){r&&jh(r);return}const _=b.decode(S,{stream:!0});j+=_;const A=j.split(` +`)||l.trim().match(/[.!?]\s*$/))&&(n.push({type:"response.output_text.delta",delta:l.trim()}),l="");continue}c.type!=="response.usage.complete"&&n.push(c)}for(const[,c]of r)if(c.arguments.trim()&&c.arguments.trim().length>2){const d=a.get(c.callId)||c.name||"unknown";n.push({type:"response.function_call.complete",data:{name:d,arguments:c.arguments,call_id:c.callId}})}return l.trim()&&n.push({type:"response.output_text.delta",delta:l.trim()}),n}function wR(e){switch(e.type){case"response.output_text.delta":if("delta"in e){const n=e.delta||"";return n.length>60?`${n.slice(0,60)}...`:n}return"Text output";case"response.function_call.complete":if("data"in e&&e.data){const n=e.data;let r=n.name||"unknown";(!r||r==="unknown")&&(r="function_call");const a=n.arguments?typeof n.arguments=="string"?n.arguments.slice(0,30):JSON.stringify(n.arguments).slice(0,30):"";return`Calling ${r}(${a}${a.length>=30?"...":""})`}return"Function call";case"response.function_call_arguments.delta":return"delta"in e&&e.delta?`Function arg delta: ${e.delta.slice(0,30)}${e.delta.length>30?"...":""}`:"Function arguments...";case"response.function_result.complete":{const r=e.output.slice(0,40);return`Function result: ${r}${r.length>=40?"...":""}`}case"response.output_item.added":{const n=e;return n.item.type==="function_call"?`Tool call: ${n.item.name}`:"Output item added"}case"response.workflow_event.completed":return"data"in e&&e.data?`Executor: ${e.data.executor_id||"unknown"}`:"Workflow event";case"response.trace.completed":return"data"in e&&e.data?`Trace: ${e.data.operation_name||"unknown"}`:"Trace event";case"response.completed":if("response"in e&&e.response&&"usage"in e.response){const r=e.response.usage;if(r)return`Response complete (${r.total_tokens} tokens)`}return"Response complete";case"response.done":return"Response complete";case"error":return"message"in e&&typeof e.message=="string"?e.message:"Error occurred";default:return`${e.type}`}}function NR(e){switch(e){case"response.output_text.delta":return eg;case"response.function_call.complete":case"response.function_call.delta":case"response.function_call_arguments.delta":return _a;case"response.function_result.complete":return nn;case"response.output_item.added":return nn;case"response.workflow_event.completed":return Qp;case"response.trace.completed":return Bu;case"response.completed":return nn;case"response.done":return nn;case"error":return kl;default:return hs}}function jR(e){switch(e){case"response.output_text.delta":return"text-gray-600 dark:text-gray-400";case"response.function_call.complete":case"response.function_call.delta":case"response.function_call_arguments.delta":return"text-blue-600 dark:text-blue-400";case"response.function_result.complete":return"text-green-600 dark:text-green-400";case"response.output_item.added":return"text-green-600 dark:text-green-400";case"response.workflow_event.completed":return"text-purple-600 dark:text-purple-400";case"response.trace.completed":return"text-orange-600 dark:text-orange-400";case"response.completed":return"text-green-600 dark:text-green-400";case"response.done":return"text-green-600 dark:text-green-400";case"error":return"text-red-600 dark:text-red-400";default:return"text-gray-600 dark:text-gray-400"}}function SR({event:e}){const[n,r]=w.useState(!1),a=e.type||"unknown",l=NR(a),c=jR(a),d="_uiTimestamp"in e&&typeof e._uiTimestamp=="number"?new Date(e._uiTimestamp*1e3).toLocaleTimeString():new Date().toLocaleTimeString(),f=wR(e),m=e.type==="response.function_call.complete"&&"data"in e&&e.data||e.type==="response.function_result.complete"||e.type==="response.output_item.added"&&zr(e)!==null||e.type==="response.workflow_event.completed"&&"data"in e&&e.data||e.type==="response.trace.completed"&&"data"in e&&e.data||e.type==="response.trace.completed"&&"data"in e&&e.data||e.type==="response.output_text.delta"&&"delta"in e&&e.delta&&e.delta.length>100||e.type==="response.completed"&&"response"in e&&e.response||e.type==="error";return o.jsxs("div",{className:"border-l-2 border-muted pl-3 py-2 hover:bg-muted/50 transition-colors",children:[o.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground mb-1",children:[o.jsx(l,{className:`h-3 w-3 ${c}`}),o.jsx("span",{className:"font-mono",children:d}),o.jsx(ut,{variant:"outline",className:"text-xs py-0",children:e.type?e.type.replace("response.",""):"unknown"})]}),o.jsxs("div",{className:"text-sm",children:[o.jsxs("div",{className:`flex items-center gap-2 ${m?"cursor-pointer":""}`,onClick:()=>m&&r(!n),children:[m&&o.jsx("div",{className:"text-muted-foreground",children:n?o.jsx(Rt,{className:"h-3 w-3"}):o.jsx(en,{className:"h-3 w-3"})}),o.jsx("div",{className:"text-muted-foreground flex-1",children:m&&f.length>80?`${f.slice(0,80)}...`:f})]}),n&&m&&o.jsx("div",{className:"mt-2 ml-5 p-3 bg-muted/30 rounded border",children:o.jsx(_R,{event:e})})]})]})}function _R({event:e}){if(e.type==="error"){const n=e;return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(kl,{className:"h-4 w-4 text-red-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Error Details"})]}),o.jsxs("div",{className:"text-xs",children:[n.message&&o.jsxs("div",{className:"mb-2",children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Message:"}),o.jsx("div",{className:"mt-1",children:o.jsx("pre",{className:"text-xs bg-destructive/10 border border-destructive/30 rounded p-2 text-destructive whitespace-pre-wrap break-all",children:n.message})})]}),n.code&&o.jsxs("div",{className:"mb-2",children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Code:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.code})]}),n.param&&o.jsxs("div",{className:"mb-2",children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Parameter:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.param})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Raw Event:"}),o.jsx("div",{className:"mt-1",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap break-all max-h-32 overflow-auto",children:JSON.stringify(e,null,2)})})]})]})]})}switch(e.type){case"response.function_call.complete":if("data"in e&&e.data){const n=e.data;return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(_a,{className:"h-4 w-4 text-blue-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Function Call"})]}),o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Function:"}),o.jsx("span",{className:"ml-2 font-mono bg-blue-100 dark:bg-blue-900 px-2 py-1 rounded",children:n.name||"unknown"})]}),n.call_id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Call ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.call_id})]}),n.arguments&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Arguments:"}),o.jsx("div",{className:"mt-1 max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all",children:typeof n.arguments=="string"?n.arguments:JSON.stringify(n.arguments,null,1)})})]})]})]})}break;case"response.function_result.complete":{const n=e;return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(nn,{className:"h-4 w-4 text-green-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Function Result"})]}),o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Call ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.call_id})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Status:"}),o.jsx("span",{className:`ml-2 px-2 py-1 rounded text-xs font-medium ${n.status==="completed"?"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200":"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"}`,children:n.status})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Output:"}),o.jsx("div",{className:"mt-1 max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all",children:n.output})})]})]})]})}case"response.output_item.added":{const n=zr(e);if(n)return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(nn,{className:"h-4 w-4 text-green-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Function Result"})]}),o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Call ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.call_id})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Status:"}),o.jsx("span",{className:`ml-2 px-2 py-1 rounded text-xs font-medium ${n.status==="completed"?"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200":"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"}`,children:n.status})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Output:"}),o.jsx("div",{className:"mt-1 max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all",children:n.output})})]})]})]});break}case"response.workflow_event.completed":if("data"in e&&e.data){const n=e.data;return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Qp,{className:"h-4 w-4 text-purple-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Workflow Event"})]}),o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Event Type:"}),o.jsx("span",{className:"ml-2 font-mono bg-purple-100 dark:bg-purple-900 px-2 py-1 rounded",children:n.event_type||"unknown"})]}),n.executor_id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Executor:"}),o.jsx("span",{className:"ml-2 font-mono",children:n.executor_id})]}),n.timestamp&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Timestamp:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.timestamp})]}),n.data&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Data:"}),o.jsx("div",{className:"mt-1 max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all",children:typeof n.data=="string"?n.data:JSON.stringify(n.data,null,1)})})]})]})]})}break;case"response.trace.completed":if("data"in e&&e.data){const n=e.data;return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Bu,{className:"h-4 w-4 text-orange-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Trace Event"})]}),o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Operation:"}),o.jsx("span",{className:"ml-2 font-mono bg-orange-100 dark:bg-orange-900 px-2 py-1 rounded",children:n.operation_name||"unknown"})]}),n.span_id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Span ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.span_id})]}),n.trace_id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Trace ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.trace_id})]}),n.duration_ms&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Duration:"}),o.jsxs("span",{className:"ml-2 font-mono text-xs",children:[Number(n.duration_ms).toFixed(2),"ms"]})]}),n.status&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Status:"}),o.jsx("span",{className:`ml-2 px-2 py-1 rounded text-xs font-medium ${n.status==="StatusCode.UNSET"||n.status==="OK"?"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200":"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"}`,children:n.status||"unknown"})]}),n.entity_id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Entity:"}),o.jsx("span",{className:"ml-2 font-mono text-xs",children:n.entity_id})]}),n.attributes&&Object.keys(n.attributes).length>0&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Attributes:"}),o.jsx("div",{className:"mt-1 max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap break-all",children:l2(n.attributes)})})]})]})]})}break;case"response.output_text.delta":if("delta"in e&&e.delta)return o.jsxs("div",{className:"space-y-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(eg,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"font-semibold text-sm",children:"Text Output"})]}),o.jsx("div",{className:"max-h-32 overflow-auto",children:o.jsx("pre",{className:"text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all",children:e.delta})})]});break;case"response.completed":if("response"in e&&e.response){const r=e.response;return o.jsx("div",{className:"space-y-2",children:o.jsxs("div",{className:"grid grid-cols-1 gap-2 text-xs",children:[r.usage&&o.jsxs(o.Fragment,{children:[o.jsx("div",{children:o.jsx("span",{className:"font-medium text-muted-foreground",children:"Usage:"})}),o.jsxs("div",{className:"ml-4 space-y-1",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Input tokens:"}),o.jsx("span",{className:"ml-2 font-mono",children:r.usage.input_tokens})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Output tokens:"}),o.jsx("span",{className:"ml-2 font-mono",children:r.usage.output_tokens})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Total tokens:"}),o.jsx("span",{className:"ml-2 font-mono bg-green-100 dark:bg-green-900 px-2 py-1 rounded",children:r.usage.total_tokens})]})]})]}),r.id&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Response ID:"}),o.jsx("span",{className:"ml-2 font-mono text-xs break-all",children:r.id})]}),r.model&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-muted-foreground",children:"Model:"}),o.jsx("span",{className:"ml-2 font-mono text-xs break-all",children:r.model})]})]})})}break;default:return o.jsx("div",{className:"text-xs text-muted-foreground",children:o.jsx("pre",{className:"bg-background border rounded p-2 overflow-auto max-h-32",children:JSON.stringify(e,null,2)})})}return null}function ER({events:e,isStreaming:n}){const r=w.useRef(null),a=gg(e),c=[...i2(a)].reverse();return o.jsxs("div",{className:"h-full flex flex-col",children:[o.jsxs("div",{className:"flex items-center justify-between p-3 border-b",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Qp,{className:"h-4 w-4"}),o.jsx("span",{className:"font-medium",children:"Events"}),o.jsxs(ut,{variant:"outline",children:[a.length,e.length>a.length?` (${e.length} raw)`:""]})]}),n&&o.jsxs("div",{className:"flex items-center gap-1 text-xs text-muted-foreground",children:[o.jsx("div",{className:"h-2 w-2 animate-pulse rounded-full bg-green-500 dark:bg-green-400"}),"Streaming"]})]}),o.jsx(Wn,{ref:r,className:"flex-1",children:o.jsx("div",{className:"p-3",children:a.length===0?o.jsx("div",{className:"text-center text-muted-foreground text-sm py-8",children:e.length===0?"No events yet. Start a conversation to see real-time events.":"Processing events... Accumulated events will appear here."}):o.jsx("div",{className:"space-y-2",children:c.map((d,f)=>"type"in d&&d.type==="separator"?o.jsx(a2,{},d.id):o.jsx(SR,{event:d},`${d.type}-${f}`))})})})]})}function CR(e){const n=new Map;for(const a of e){if(!("data"in a))continue;const c=a.data.response_id||"unknown";n.has(c)||n.set(c,[]),n.get(c).push(a)}const r=[];for(const[a,l]of n){const c=new Map,d=[];for(const y of l){if(!("data"in y))continue;const b=y.data,j=b.span_id||`span_${Math.random()}`;c.set(j,{event:y,data:b,children:[]})}for(const y of l){if(!("data"in y))continue;const b=y.data,j=b.span_id||"",N=b.parent_span_id,S=c.get(j);S&&(N&&c.has(N)?c.get(N).children.push(S):d.push(S))}d.sort((y,b)=>(y.data.start_time||0)-(b.data.start_time||0));const f=y=>{y.children.sort((b,j)=>(b.data.start_time||0)-(j.data.start_time||0)),y.children.forEach(f)};d.forEach(f);const m=l[0],h=m&&"data"in m?m.data:null,g=Math.min(...l.map(y=>("data"in y?y.data:null)?.start_time||Date.now()/1e3)),x=l.reduce((y,b)=>{const j="data"in b?b.data:null;return y+(j?.duration_ms||0)},0);r.push({response_id:a,timestamp:g,traces:d,totalDuration:x,entity_id:h?.entity_id})}return r.sort((a,l)=>l.timestamp-a.timestamp),r}function Su(e){if(typeof e=="string"){const n=e.trim();if(n.startsWith("[")||n.startsWith("{"))try{const r=JSON.parse(e);return Su(r)}catch{return e}return e}if(Array.isArray(e))return e.map(Su);if(e!==null&&typeof e=="object"){const n={};for(const[r,a]of Object.entries(e))n[r]=Su(a);return n}return e}function l2(e){try{const n=Su(e);return JSON.stringify(n,null,2)}catch{return JSON.stringify(e,null,2)}}function kR(e){return e.includes("invoke_agent")||e.includes("Agent")?"bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200":e.includes("chat")||e.includes("Chat")?"bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200":e.includes("tool")||e.includes("execute")?"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200":"bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200"}function c2({node:e,depth:n=0}){const[r,a]=w.useState(n<2),[l,c]=w.useState(!1),{data:d}=e,f=d.operation_name||"Unknown",m=d.duration_ms?`${Number(d.duration_ms).toFixed(1)}ms`:"",h=e.children.length>0,g=d.attributes?.["gen_ai.usage.input_tokens"],x=d.attributes?.["gen_ai.usage.output_tokens"],y=g!==void 0||x!==void 0;return o.jsxs("div",{className:"relative",children:[n>0&&o.jsx("div",{className:"absolute left-0 top-0 bottom-0 border-l-2 border-muted",style:{marginLeft:`${(n-1)*16+8}px`}}),o.jsxs("div",{className:"flex items-center gap-2 py-1.5 hover:bg-muted/50 rounded transition-colors",style:{paddingLeft:`${n*16}px`},children:[o.jsx("button",{onClick:()=>h?a(!r):c(!l),className:"w-4 h-4 flex items-center justify-center text-muted-foreground hover:text-foreground",children:h?r?o.jsx(Rt,{className:"h-3 w-3"}):o.jsx(en,{className:"h-3 w-3"}):l?o.jsx(Rt,{className:"h-3 w-3"}):o.jsx(en,{className:"h-3 w-3"})}),o.jsx("span",{className:`text-xs px-1.5 py-0.5 rounded font-medium ${kR(f)}`,children:f.replace("Agent.","").replace("invoke_agent ","")}),m&&o.jsx("span",{className:"text-xs text-muted-foreground font-mono",children:m}),y&&o.jsxs("span",{className:"text-xs text-muted-foreground font-mono",children:[g!==void 0&&o.jsxs("span",{children:["↑",String(g)]}),g!==void 0&&x!==void 0&&o.jsx("span",{className:"mx-0.5",children:"/"}),x!==void 0&&o.jsxs("span",{children:["↓",String(x)]})]})]}),l&&!h&&o.jsx("div",{className:"ml-4 mt-1 mb-2 p-2 bg-muted/30 rounded border text-xs",style:{marginLeft:`${n*16+20}px`},children:o.jsxs("div",{className:"space-y-1",children:[d.span_id&&o.jsxs("div",{className:"flex gap-2",children:[o.jsx("span",{className:"text-muted-foreground w-20",children:"Span ID:"}),o.jsx("span",{className:"font-mono text-xs break-all",children:d.span_id})]}),d.trace_id&&o.jsxs("div",{className:"flex gap-2",children:[o.jsx("span",{className:"text-muted-foreground w-20",children:"Trace ID:"}),o.jsx("span",{className:"font-mono text-xs break-all",children:d.trace_id})]}),d.status&&o.jsxs("div",{className:"flex gap-2",children:[o.jsx("span",{className:"text-muted-foreground w-20",children:"Status:"}),o.jsx("span",{className:`px-1.5 py-0.5 rounded text-xs ${d.status==="StatusCode.UNSET"||d.status==="OK"?"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200":"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"}`,children:d.status})]}),d.attributes&&Object.keys(d.attributes).length>0&&o.jsxs("div",{className:"mt-2",children:[o.jsx("span",{className:"text-muted-foreground block mb-1",children:"Attributes:"}),o.jsx("pre",{className:"text-xs bg-background border rounded p-2 overflow-auto max-h-32 whitespace-pre-wrap break-all",children:l2(d.attributes)})]})]})}),h&&r&&o.jsx("div",{children:e.children.map((b,j)=>o.jsx(c2,{node:b,depth:n+1},b.data.span_id||j))})]})}function TR({group:e}){const[n,r]=w.useState(!0),a=new Date(e.timestamp*1e3).toLocaleTimeString(),l=e.totalDuration>0?`${e.totalDuration.toFixed(0)}ms`:"",c=e.traces.reduce((d,f)=>{const m=h=>1+h.children.reduce((g,x)=>g+m(x),0);return d+m(f)},0);return o.jsxs("div",{className:"border rounded-lg overflow-hidden",children:[o.jsxs("div",{className:"flex items-center gap-2 p-2 bg-muted/50 cursor-pointer hover:bg-muted/70 transition-colors",onClick:()=>r(!n),children:[o.jsx("div",{className:"text-muted-foreground",children:n?o.jsx(Rt,{className:"h-4 w-4"}):o.jsx(en,{className:"h-4 w-4"})}),o.jsx("span",{className:"font-mono text-xs text-muted-foreground",children:a}),e.entity_id&&o.jsx(ut,{variant:"outline",className:"text-xs py-0",children:e.entity_id.replace("agent_","").replace("workflow_","")}),o.jsx("div",{className:"flex-1"}),l&&o.jsx(ut,{variant:"secondary",className:"text-xs py-0",children:l}),o.jsxs("span",{className:"text-xs text-muted-foreground",children:[c," span",c!==1?"s":""]})]}),n&&o.jsx("div",{className:"p-2 border-t",children:e.traces.map((d,f)=>o.jsx(c2,{node:d,depth:0},d.data.span_id||f))})]})}function AR({events:e}){const n=le(c=>c.debugTraceSubTab),r=le(c=>c.setDebugTraceSubTab),a=e.filter(c=>c.type==="response.trace.completed"),l=CR(a);return o.jsxs("div",{className:"h-full flex flex-col",children:[o.jsxs("div",{className:"flex items-center gap-2 p-3 border-b",children:[o.jsx(Bu,{className:"h-4 w-4"}),o.jsx("span",{className:"font-medium",children:"Traces"}),o.jsx(ut,{variant:"outline",children:a.length}),o.jsx("div",{className:"flex-1"}),o.jsxs("div",{className:"flex items-center bg-muted rounded-md p-1 min-w-0",children:[o.jsx("button",{onClick:()=>r("spans"),className:`px-3 py-1.5 text-xs rounded transition-colors truncate ${n==="spans"?"bg-background shadow-sm font-medium":"text-muted-foreground hover:text-foreground"}`,children:"OTel Spans"}),o.jsxs("button",{onClick:()=>r("context"),className:`px-3 py-1.5 text-xs rounded transition-colors flex items-center gap-1.5 min-w-0 ${n==="context"?"bg-background shadow-sm font-medium":"text-muted-foreground hover:text-foreground"}`,children:[o.jsx(ha,{className:"h-3.5 w-3.5 flex-shrink-0"}),o.jsx("span",{className:"truncate",children:"Context Inspector"})]})]})]}),n==="spans"?o.jsxs("div",{className:"flex-1 flex flex-col min-h-0",children:[a.length>0&&o.jsx("div",{className:"p-3 border-b flex-shrink-0",children:o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Bu,{className:"h-4 w-4"}),o.jsx("span",{className:"font-medium text-sm",children:"OTel Spans"}),o.jsxs(ut,{variant:"outline",className:"text-xs",children:[l.length," turn",l.length!==1?"s":""]})]})}),a.length===0?o.jsxs("div",{className:"flex flex-col items-center text-center p-6 pt-9",children:[o.jsx(ha,{className:"h-8 w-8 text-muted-foreground mb-3"}),o.jsx("div",{className:"text-sm font-medium mb-1",children:"No Data"}),o.jsxs("div",{className:"text-xs text-muted-foreground max-w-[200px]",children:["Run"," ",o.jsx("span",{className:"font-mono bg-accent/10 px-1 rounded",children:"devui --instrumentation"})," ","and start a conversation."]})]}):o.jsx(Wn,{className:"flex-1",children:o.jsx("div",{className:"p-3",children:o.jsx("div",{className:"space-y-3",children:l.map(c=>o.jsx(TR,{group:c},c.response_id))})})})]}):o.jsx(bR,{events:e})]})}function MR({events:e}){const n=gg(e),r=[],a=n.filter(m=>m.type==="response.function_call.complete"),l=e.filter(m=>zr(m)!==null),c=new Map;l.forEach(m=>{const h=zr(m);h&&c.set(h.call_id,m)}),a.forEach(m=>{if(r.push(m),"data"in m&&m.data&&m.data.call_id){const h=String(m.data.call_id),g=c.get(h);g&&(r.push(g),c.delete(h))}}),c.forEach(m=>{r.push(m)});const f=[...i2(r)].reverse();return o.jsxs("div",{className:"h-full flex flex-col",children:[o.jsxs("div",{className:"flex items-center gap-2 p-3 border-b",children:[o.jsx(_a,{className:"h-4 w-4"}),o.jsx("span",{className:"font-medium",children:"Tools"}),o.jsx(ut,{variant:"outline",children:r.length})]}),o.jsx(Wn,{className:"flex-1",children:o.jsx("div",{className:"p-3",children:r.length===0?o.jsx("div",{className:"text-center text-muted-foreground text-sm py-8",children:"No tool executions yet. Tool calls will appear here during conversations."}):o.jsx("div",{className:"space-y-3",children:f.map((m,h)=>"type"in m&&m.type==="separator"?o.jsx(a2,{},m.id):o.jsx(RR,{event:m},h))})})})]})}function RR({event:e}){const n="_uiTimestamp"in e&&typeof e._uiTimestamp=="number"?new Date(e._uiTimestamp*1e3).toLocaleTimeString():new Date().toLocaleTimeString(),r=e.type==="response.function_call.complete",a=zr(e),l=a!==null;if(!r&&!l)return null;const c=r&&"data"in e?e.data:null;return o.jsxs("div",{className:"border rounded p-3",children:[o.jsxs("div",{className:"flex items-center justify-between mb-2",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(og,{className:"h-4 w-4 text-yellow-600 dark:text-yellow-400"}),o.jsx("span",{className:"font-medium text-sm",children:r?"Tool Call":"Tool Result"}),r&&c&&c.name!==void 0&&o.jsxs("span",{className:"text-xs text-muted-foreground",children:["(",String(c.name),")"]})]}),o.jsx("span",{className:"text-xs text-muted-foreground font-mono",children:n})]}),r&&c&&o.jsxs("div",{className:"p-2 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded",children:[o.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[o.jsx(_a,{className:"h-3 w-3 text-blue-600 dark:text-blue-400"}),o.jsx("span",{className:"text-xs font-mono bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded",children:"CALL"}),o.jsx("span",{className:"font-medium text-sm",children:String(c.name||"unknown")})]}),c.arguments!==void 0&&o.jsxs("div",{className:"text-xs",children:[o.jsx("span",{className:"text-muted-foreground mb-1 block",children:"Arguments:"}),o.jsx("pre",{className:"p-2 bg-background border rounded text-xs overflow-auto max-h-32 max-w-full break-all whitespace-pre-wrap",children:typeof c.arguments=="string"?c.arguments:JSON.stringify(c.arguments,null,1)})]})]}),l&&a&&o.jsxs("div",{className:"p-2 bg-green-50 dark:bg-green-950/50 border border-green-200 dark:border-green-800 rounded",children:[o.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[o.jsx(nn,{className:"h-3 w-3 text-green-600 dark:text-green-400"}),o.jsx("span",{className:"text-xs font-mono bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded",children:"RESULT"}),a.status!=="completed"&&o.jsx("span",{className:"ml-auto px-2 py-1 rounded text-xs font-medium bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200",children:a.status})]}),o.jsxs("div",{className:"text-xs space-y-1",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("span",{className:"text-muted-foreground",children:"Call ID:"}),o.jsx("span",{className:"font-mono text-xs break-all",children:a.call_id})]}),o.jsxs("div",{children:[o.jsx("span",{className:"text-muted-foreground block mb-1",children:"Output:"}),o.jsx("pre",{className:"p-2 bg-background border rounded text-xs overflow-auto max-h-32 break-all whitespace-pre-wrap",children:a.output})]})]})]})]})}function DR({events:e,isStreaming:n=!1,onMinimize:r}){const a=le(d=>d.debugPanelTab),l=le(d=>d.setDebugPanelTab),c=w.useMemo(()=>{const d=gg(e),f=d.length,m=e.filter(g=>g.type==="response.trace.completed").length,h=d.filter(g=>g.type==="response.function_call.complete").length+e.filter(g=>zr(g)!==null).length;return{eventsCount:f,tracesCount:m,toolsCount:h}},[e]);return o.jsx("div",{className:"flex-1 border-l flex flex-col min-h-0",children:o.jsxs(D5,{value:a,onValueChange:d=>l(d),className:"flex-1 flex flex-col min-h-0",children:[o.jsxs("div",{className:"px-3 pt-3 flex items-center gap-2 flex-shrink-0",children:[o.jsxs($N,{className:"flex-1",children:[o.jsxs(Nu,{value:"events",className:"flex-1 gap-1.5",children:["Events",c.eventsCount>0&&o.jsx("span",{className:"text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center",children:c.eventsCount})]}),o.jsxs(Nu,{value:"traces",className:"flex-1 gap-1.5",children:["Traces",c.tracesCount>0&&o.jsx("span",{className:"text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center",children:c.tracesCount})]}),o.jsxs(Nu,{value:"tools",className:"flex-1 gap-1.5",children:["Tools",c.toolsCount>0&&o.jsx("span",{className:"text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center",children:c.toolsCount})]})]}),r&&o.jsx(Le,{variant:"ghost",size:"sm",onClick:r,className:"h-8 w-8 p-0 flex-shrink-0",title:"Minimize debug panel",children:o.jsx(en,{className:"h-4 w-4"})})]}),o.jsx(ju,{value:"events",className:"flex-1 mt-0 overflow-hidden",children:o.jsx(ER,{events:e,isStreaming:n})}),o.jsx(ju,{value:"traces",className:"flex-1 mt-0 overflow-hidden",children:o.jsx(AR,{events:e})}),o.jsx(ju,{value:"tools",className:"flex-1 mt-0 overflow-hidden",children:o.jsx(MR,{events:e})})]})})}function Ir({open:e,onOpenChange:n,children:r}){if(!e)return null;const a=()=>{n(!1)},l=d=>{d.stopPropagation()},c=d=>{d.stopPropagation()};return o.jsxs("div",{className:"fixed inset-0 z-50 flex items-center justify-center",children:[o.jsx("div",{className:"absolute inset-0 bg-black/50",onClick:a}),o.jsx("div",{className:"relative z-10",onClick:l,onMouseDown:c,onMouseUp:d=>d.stopPropagation(),children:r})]})}function Lr({children:e,className:n=""}){const a=n.includes("w-[")||n.includes("w-full")||n.includes("max-w-")?"":"max-w-lg w-full";return o.jsx("div",{className:`relative bg-background border rounded-lg shadow-lg max-h-[90vh] overflow-hidden ${a} ${n}`,children:e})}function $r({children:e,className:n=""}){return o.jsx("div",{className:`space-y-2 ${n}`,children:e})}function Pr({children:e,className:n=""}){return o.jsx("h2",{className:`text-lg font-semibold ${n}`,children:e})}function OR({children:e,className:n=""}){return o.jsx("p",{className:`text-sm text-muted-foreground ${n}`,children:e})}function So({onClose:e}){return o.jsx(Le,{variant:"ghost",size:"sm",onClick:e,className:"absolute top-4 right-4 h-8 w-8 p-0 rounded-sm opacity-70 hover:opacity-100",children:o.jsx(Ea,{className:"h-4 w-4"})})}function zR({children:e}){return o.jsx("div",{className:"flex justify-end gap-2 p-4 border-t bg-muted/50",children:e})}function as({className:e,type:n,...r}){return o.jsx("input",{type:n,"data-slot":"input",className:We("file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm","focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]","aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",e),...r})}var IR="Label",u2=w.forwardRef((e,n)=>o.jsx(Ye.label,{...e,ref:n,onMouseDown:r=>{r.target.closest("button, input, select, textarea")||(e.onMouseDown?.(r),!r.defaultPrevented&&r.detail>1&&r.preventDefault())}}));u2.displayName=IR;var LR=u2;function kt({className:e,...n}){return o.jsx(LR,{"data-slot":"label",className:We("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",e),...n})}var Td="Switch",[$R,e$]=Kn(Td),[PR,HR]=$R(Td),d2=w.forwardRef((e,n)=>{const{__scopeSwitch:r,name:a,checked:l,defaultChecked:c,required:d,disabled:f,value:m="on",onCheckedChange:h,form:g,...x}=e,[y,b]=w.useState(null),j=rt(n,E=>b(E)),N=w.useRef(!1),S=y?g||!!y.closest("form"):!0,[_,A]=Ar({prop:l,defaultProp:c??!1,onChange:h,caller:Td});return o.jsxs(PR,{scope:r,checked:_,disabled:f,children:[o.jsx(Ye.button,{type:"button",role:"switch","aria-checked":_,"aria-required":d,"data-state":p2(_),"data-disabled":f?"":void 0,disabled:f,value:m,...x,ref:j,onClick:ke(e.onClick,E=>{A(M=>!M),S&&(N.current=E.isPropagationStopped(),N.current||E.stopPropagation())})}),S&&o.jsx(h2,{control:y,bubbles:!N.current,name:a,value:m,checked:_,required:d,disabled:f,form:g,style:{transform:"translateX(-100%)"}})]})});d2.displayName=Td;var f2="SwitchThumb",m2=w.forwardRef((e,n)=>{const{__scopeSwitch:r,...a}=e,l=HR(f2,r);return o.jsx(Ye.span,{"data-state":p2(l.checked),"data-disabled":l.disabled?"":void 0,...a,ref:n})});m2.displayName=f2;var UR="SwitchBubbleInput",h2=w.forwardRef(({__scopeSwitch:e,control:n,checked:r,bubbles:a=!0,...l},c)=>{const d=w.useRef(null),f=rt(d,c),m=fg(r),h=Lp(n);return w.useEffect(()=>{const g=d.current;if(!g)return;const x=window.HTMLInputElement.prototype,b=Object.getOwnPropertyDescriptor(x,"checked").set;if(m!==r&&b){const j=new Event("click",{bubbles:a});b.call(g,r),g.dispatchEvent(j)}},[m,r,a]),o.jsx("input",{type:"checkbox","aria-hidden":!0,defaultChecked:r,...l,tabIndex:-1,ref:f,style:{...l.style,...h,position:"absolute",pointerEvents:"none",opacity:0,margin:0}})});h2.displayName=UR;function p2(e){return e?"checked":"unchecked"}var g2=d2,BR=m2;const Wi=w.forwardRef(({className:e,...n},r)=>o.jsx(g2,{className:We("peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",e),...n,ref:r,children:o.jsx(BR,{className:We("pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0")})}));Wi.displayName=g2.displayName;const VR=["gpt-4.1","gpt-4.1-mini","o1","o1-mini","o3-mini"];function cb({open:e,onOpenChange:n,onBackendUrlChange:r}){const[a,l]=w.useState("general"),{oaiMode:c,setOAIMode:d,azureDeploymentEnabled:f,setAzureDeploymentEnabled:m,authRequired:h,serverCapabilities:g,serverVersion:x,runtime:y,uiMode:b,streamingEnabled:j,setStreamingEnabled:N}=le(),S="",[_,A]=w.useState(()=>localStorage.getItem("devui_backend_url")||S),[E,M]=w.useState(_),[T,D]=w.useState(!!localStorage.getItem("devui_auth_token")),[z,H]=w.useState(""),q=()=>{try{new URL(E),localStorage.setItem("devui_backend_url",E),A(E),r?.(E),n(!1),window.location.reload()}catch{alert("Please enter a valid URL (e.g., http://localhost:8080)")}},X=()=>{localStorage.removeItem("devui_backend_url"),M(S),A(S),r?.(S),window.location.reload()},W=()=>{z.trim()&&(localStorage.setItem("devui_auth_token",z.trim()),D(!0),H(""),window.location.reload())},G=()=>{localStorage.removeItem("devui_auth_token"),D(!1),H(""),window.location.reload()},ne=E!==_,B=!localStorage.getItem("devui_backend_url");return o.jsx(Ir,{open:e,onOpenChange:n,children:o.jsxs(Lr,{className:"w-[600px] max-w-[90vw] flex flex-col max-h-[85vh]",children:[o.jsx($r,{className:"p-6 pb-2 flex-shrink-0",children:o.jsx(Pr,{children:"Settings"})}),o.jsx(So,{onClose:()=>n(!1)}),o.jsxs("div",{className:"flex border-b px-6 flex-shrink-0",children:[o.jsxs("button",{onClick:()=>l("general"),className:`px-4 py-2 text-sm font-medium transition-colors relative ${a==="general"?"text-foreground":"text-muted-foreground hover:text-foreground"}`,children:["General",a==="general"&&o.jsx("div",{className:"absolute bottom-0 left-0 right-0 h-0.5 bg-primary"})]}),g.openai_proxy&&o.jsxs("button",{onClick:()=>l("proxy"),className:`px-4 py-2 text-sm font-medium transition-colors relative ${a==="proxy"?"text-foreground":"text-muted-foreground hover:text-foreground"}`,children:["OpenAI Proxy",a==="proxy"&&o.jsx("div",{className:"absolute bottom-0 left-0 right-0 h-0.5 bg-primary"})]}),o.jsxs("button",{onClick:()=>l("about"),className:`px-4 py-2 text-sm font-medium transition-colors relative ${a==="about"?"text-foreground":"text-muted-foreground hover:text-foreground"}`,children:["About",a==="about"&&o.jsx("div",{className:"absolute bottom-0 left-0 right-0 h-0.5 bg-primary"})]})]}),o.jsxs("div",{className:"px-6 pb-6 overflow-y-auto flex-1 min-h-[400px]",children:[a==="general"&&o.jsxs("div",{className:"space-y-6 pt-4",children:[o.jsxs("div",{className:"space-y-3",children:[o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsx(kt,{htmlFor:"backend-url",className:"text-sm font-medium",children:"Backend URL"}),!B&&o.jsxs(Le,{variant:"ghost",size:"sm",onClick:X,className:"h-7 text-xs",title:"Reset to default",children:[o.jsx(sg,{className:"h-3 w-3 mr-1"}),"Reset"]})]}),o.jsx(as,{id:"backend-url",type:"url",value:E,onChange:U=>M(U.target.value),placeholder:"http://localhost:8080",className:"font-mono text-sm"}),o.jsxs("p",{className:"text-xs text-muted-foreground",children:["Default: ",o.jsx("span",{className:"font-mono",children:S})]}),o.jsx("div",{className:"flex gap-2 pt-2 min-h-[36px]",children:ne&&o.jsxs(o.Fragment,{children:[o.jsx(Le,{onClick:q,size:"sm",className:"flex-1",children:"Apply & Reload"}),o.jsx(Le,{onClick:()=>M(_),variant:"outline",size:"sm",className:"flex-1",children:"Cancel"})]})})]}),(h||T)&&o.jsxs("div",{className:"space-y-3 border-t pt-6",children:[o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsx(kt,{className:"text-sm font-medium",children:"Authentication Token"}),!h&&T&&o.jsx("span",{className:"text-xs text-muted-foreground",children:"(Not required by current backend)"})]}),T?o.jsxs("div",{className:"space-y-3",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(as,{type:"password",value:"••••••••••••••••••••",disabled:!0,className:"font-mono text-sm flex-1"}),o.jsx(Le,{variant:"destructive",size:"sm",onClick:G,className:"flex-shrink-0",children:"Clear"})]}),o.jsx("p",{className:"text-xs text-green-600 dark:text-green-400",children:"✓ Token configured and stored locally"})]}):o.jsxs("div",{className:"space-y-3",children:[o.jsx(as,{type:"password",value:z,onChange:U=>H(U.target.value),placeholder:"Enter bearer token",className:"font-mono text-sm",onKeyDown:U=>{U.key==="Enter"&&z.trim()&&W()}}),o.jsx(Le,{onClick:W,size:"sm",disabled:!z.trim(),className:"w-full",children:"Save & Reload"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:h?"Required by backend (started with --auth flag)":"Not required by current backend"})]})]}),g.deployment&&o.jsxs("div",{className:"space-y-3 border-t pt-6",children:[o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{className:"space-y-0.5",children:[o.jsx(kt,{className:"text-sm font-medium",children:"Azure Deployment"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Enable one-click deployment to Azure Container Apps"})]}),o.jsx(Wi,{checked:f,onCheckedChange:m})]}),o.jsxs("details",{className:"group",children:[o.jsxs("summary",{className:"cursor-pointer text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1",children:[o.jsx(en,{className:"h-3 w-3 transition-transform group-open:rotate-90"}),"Learn more about Azure deployment"]}),o.jsxs("div",{className:"mt-3 space-y-3 pl-4",children:[o.jsx("p",{className:"text-xs text-muted-foreground leading-relaxed",children:'When enabled, agents that support deployment will show a "Deploy to Azure" button. This allows you to deploy your agent to Azure Container Apps directly from DevUI.'}),o.jsxs("div",{className:"space-y-1.5",children:[o.jsx("p",{className:"text-xs font-medium",children:"When enabled:"}),o.jsxs("ul",{className:"text-xs text-muted-foreground space-y-0.5 list-disc list-inside",children:[o.jsx("li",{children:'Shows "Deploy to Azure" for supported agents'}),o.jsx("li",{children:"Requires Azure CLI and proper authentication"}),o.jsx("li",{children:"Backend must have deployment capabilities enabled"})]})]}),o.jsxs("div",{className:"space-y-1.5",children:[o.jsx("p",{className:"text-xs font-medium",children:"When disabled:"}),o.jsxs("ul",{className:"text-xs text-muted-foreground space-y-0.5 list-disc list-inside",children:[o.jsx("li",{children:'Shows "Deployment Guide" for all agents'}),o.jsx("li",{children:"Provides Docker templates and manual deployment instructions"}),o.jsx("li",{children:"No backend deployment capabilities required"})]})]})]})]})]}),o.jsx("div",{className:"space-y-3 border-t pt-6",children:o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{className:"space-y-0.5",children:[o.jsx(kt,{className:"text-sm font-medium",children:"Show Tool Calls"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Display function/tool calls and results in chat messages"})]}),o.jsx(Wi,{checked:le.getState().showToolCalls,onCheckedChange:U=>le.getState().setShowToolCalls(U)})]})}),o.jsxs("div",{className:"space-y-3 border-t pt-6",children:[o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{className:"space-y-0.5",children:[o.jsx(kt,{className:"text-sm font-medium",children:"Streaming Mode"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Stream responses token-by-token as they're generated"})]}),o.jsx(Wi,{checked:j,onCheckedChange:N})]}),!j&&o.jsxs("div",{className:"flex items-start gap-2 text-xs text-amber-600 dark:text-amber-400 bg-amber-500/10 p-3 rounded",children:[o.jsx(Fs,{className:"h-3.5 w-3.5 flex-shrink-0 mt-0.5"}),o.jsxs("div",{children:[o.jsx("p",{className:"font-medium",children:"Non-streaming mode limitations:"}),o.jsxs("ul",{className:"mt-1 space-y-0.5 list-disc list-inside text-amber-600/80 dark:text-amber-400/80",children:[o.jsx("li",{children:"Tool calls won't display in real-time"}),o.jsx("li",{children:"No typing indicator during generation"}),o.jsx("li",{children:"Response appears all at once when complete"})]})]})]})]})]}),a==="proxy"&&g.openai_proxy&&o.jsxs("div",{className:"space-y-6 pt-4",children:[o.jsxs("div",{className:"space-y-4",children:[o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{className:"space-y-0.5",children:[o.jsx(kt,{className:"text-base font-medium",children:"OpenAI Proxy Mode"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Route requests through DevUI backend to OpenAI API"})]}),o.jsx(Wi,{checked:c.enabled,onCheckedChange:U=>d({...c,enabled:U})})]}),!c.enabled&&o.jsx("div",{className:"bordder border-muted bg-muted/30 rounded-lg p-4 space-y-3",children:o.jsxs("div",{className:"flex items-start gap-2",children:[o.jsx(Fs,{className:"h-4 w-4 flex-shrink-0 mt-0.5 text-blue-600 dark:text-blue-400"}),o.jsxs("div",{className:"space-y-2",children:[o.jsx("p",{className:"text-sm font-medium",children:"About OpenAI Proxy Mode"}),o.jsxs("p",{className:"text-xs text-muted-foreground leading-relaxed",children:["When enabled, your chat requests are sent to your DevUI backend"," ",o.jsxs("span",{className:"font-mono font-semibold",children:["(",_,")"]}),", which then forwards them to OpenAI's API. This keeps your"," ",o.jsx("span",{className:"font-mono font-semibold",children:"OPENAI_API_KEY"})," ","secure on the server instead of exposing it in the browser."]}),o.jsxs("div",{className:"space-y-1.5 pt-1",children:[o.jsx("p",{className:"text-xs font-medium",children:"Requirements:"}),o.jsxs("ul",{className:"text-xs text-muted-foreground space-y-0.5 list-disc list-inside",children:[o.jsxs("li",{children:["Backend must have"," ",o.jsx("span",{className:"font-mono",children:"OPENAI_API_KEY"})," ","configured"]}),o.jsx("li",{children:"Backend must support OpenAI Responses API proxying (DevUI does)"})]})]}),o.jsxs("div",{className:"space-y-1.5 pt-1",children:[o.jsx("p",{className:"text-xs font-medium",children:"Why use this?"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Quickly test and compare OpenAI models directly through the DevUI interface without creating custom agents or exposing API keys in the browser."})]})]})]})}),c.enabled&&o.jsxs("div",{className:"space-y-4 pl-4 border-l-2 border-muted",children:[o.jsxs("div",{className:"space-y-2",children:[o.jsx(kt,{className:"text-sm font-medium",children:"Model"}),o.jsx(as,{type:"text",value:c.model,onChange:U=>d({...c,model:U.target.value}),placeholder:"gpt-4.1-mini",className:"font-mono text-sm"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Enter any OpenAI model ID (e.g., gpt-4.1, o1, o3-mini)"})]}),o.jsxs("div",{className:"space-y-2",children:[o.jsx(kt,{className:"text-xs text-muted-foreground",children:"Common presets"}),o.jsx("div",{className:"flex flex-wrap gap-2",children:VR.map(U=>o.jsx(Le,{variant:c.model===U?"default":"outline",size:"sm",onClick:()=>d({...c,model:U}),className:"text-xs h-7",children:U},U))})]}),o.jsxs("details",{className:"group",children:[o.jsxs("summary",{className:"cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1",children:[o.jsx(en,{className:"h-3 w-3 transition-transform group-open:rotate-90"}),"Advanced Parameters (optional)"]}),o.jsxs("div",{className:"space-y-3 mt-3 pl-4",children:[o.jsxs("div",{className:"space-y-1",children:[o.jsx(kt,{className:"text-xs",children:"Temperature"}),o.jsx(as,{type:"number",step:"0.1",min:"0",max:"2",value:c.temperature??"",onChange:U=>d({...c,temperature:U.target.value?parseFloat(U.target.value):void 0}),placeholder:"1.0 (default)",className:"text-sm"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Controls randomness (0-2)"})]}),o.jsxs("div",{className:"space-y-1",children:[o.jsx(kt,{className:"text-xs",children:"Max Output Tokens"}),o.jsx(as,{type:"number",min:"1",value:c.max_output_tokens??"",onChange:U=>d({...c,max_output_tokens:U.target.value?parseInt(U.target.value):void 0}),placeholder:"Auto",className:"text-sm"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Maximum tokens in response"})]}),o.jsxs("div",{className:"space-y-1",children:[o.jsx(kt,{className:"text-xs",children:"Top P"}),o.jsx(as,{type:"number",step:"0.1",min:"0",max:"1",value:c.top_p??"",onChange:U=>d({...c,top_p:U.target.value?parseFloat(U.target.value):void 0}),placeholder:"1.0 (default)",className:"text-sm"}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Nucleus sampling (0-1)"})]}),o.jsxs("div",{className:"space-y-1",children:[o.jsx(kt,{className:"text-xs",children:"Reasoning Effort (o-series models)"}),o.jsxs("select",{value:c.reasoning_effort??"",onChange:U=>d({...c,reasoning_effort:U.target.value?U.target.value:void 0}),className:"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",children:[o.jsx("option",{value:"",children:"Auto (default)"}),o.jsx("option",{value:"minimal",children:"Minimal"}),o.jsx("option",{value:"low",children:"Low"}),o.jsx("option",{value:"medium",children:"Medium"}),o.jsx("option",{value:"high",children:"High"})]}),o.jsx("p",{className:"text-xs text-muted-foreground",children:"Constrains reasoning effort (faster/cheaper vs thorough)"})]})]})]})]})]}),c.enabled&&o.jsxs("div",{className:"flex items-start gap-2 text-xs text-muted-foreground bg-muted/50 p-3 rounded",children:[o.jsx(Fs,{className:"h-3.5 w-3.5 flex-shrink-0 mt-0.5"}),o.jsx("div",{className:"space-y-1",children:o.jsxs("p",{children:["Requests route through"," ",o.jsx("span",{className:"font-mono font-semibold",children:_})," ","to OpenAI API. Server must have"," ",o.jsx("span",{className:"font-mono font-semibold",children:"OPENAI_API_KEY"})," ","configured."]})})]})]}),a==="about"&&o.jsxs("div",{className:"space-y-4 pt-4",children:[o.jsx("p",{className:"text-sm text-muted-foreground",children:"DevUI is a sample app for getting started with Agent Framework."}),o.jsxs("div",{className:"space-y-2 text-sm",children:[o.jsxs("div",{className:"flex justify-between",children:[o.jsx("span",{className:"text-muted-foreground",children:"Version:"}),o.jsx("span",{className:"font-mono",children:x||"Unknown"})]}),o.jsxs("div",{className:"flex justify-between",children:[o.jsx("span",{className:"text-muted-foreground",children:"Runtime:"}),o.jsx("span",{className:"font-mono capitalize",children:y||"Unknown"})]}),o.jsxs("div",{className:"flex justify-between",children:[o.jsx("span",{className:"text-muted-foreground",children:"UI Mode:"}),o.jsx("span",{className:"font-mono capitalize",children:b||"Unknown"})]})]}),(g||h!==void 0)&&o.jsxs("div",{className:"space-y-2 pt-2",children:[o.jsx("p",{className:"text-xs font-medium text-muted-foreground uppercase tracking-wide",children:"Capabilities"}),o.jsxs("div",{className:"space-y-1 text-sm",children:[g?.instrumentation!==void 0&&o.jsxs("div",{className:"flex justify-between items-center",children:[o.jsx("span",{className:"text-muted-foreground",children:"Instrumentation:"}),o.jsx("span",{className:`text-xs px-2 py-0.5 rounded-full ${g.instrumentation?"bg-green-500/10 text-green-600 dark:text-green-400":"bg-muted text-muted-foreground"}`,children:g.instrumentation?"Enabled":"Disabled"})]}),g?.openai_proxy!==void 0&&o.jsxs("div",{className:"flex justify-between items-center",children:[o.jsx("span",{className:"text-muted-foreground",children:"OpenAI Proxy:"}),o.jsx("span",{className:`text-xs px-2 py-0.5 rounded-full ${g.openai_proxy?"bg-green-500/10 text-green-600 dark:text-green-400":"bg-muted text-muted-foreground"}`,children:g.openai_proxy?"Available":"Not Configured"})]}),g?.deployment!==void 0&&o.jsxs("div",{className:"flex justify-between items-center",children:[o.jsx("span",{className:"text-muted-foreground",children:"Deployment:"}),o.jsx("span",{className:`text-xs px-2 py-0.5 rounded-full ${g.deployment?"bg-green-500/10 text-green-600 dark:text-green-400":"bg-muted text-muted-foreground"}`,children:g.deployment?"Available":"Disabled"})]}),h!==void 0&&o.jsxs("div",{className:"flex justify-between items-center",children:[o.jsx("span",{className:"text-muted-foreground",children:"Authentication:"}),o.jsx("span",{className:`text-xs px-2 py-0.5 rounded-full ${h?"bg-blue-500/10 text-blue-600 dark:text-blue-400":"bg-muted text-muted-foreground"}`,children:h?"Required":"Not Required"})]})]})]}),o.jsx("div",{className:"flex justify-center pt-2",children:o.jsxs(Le,{variant:"outline",size:"sm",onClick:()=>window.open("https://github.com/microsoft/agent-framework","_blank"),className:"text-xs",children:[o.jsx(Hu,{className:"h-3 w-3 mr-1"}),"Learn More about Agent Framework"]})})]})]})]})})}const qR="modulepreload",FR=function(e,n){return new URL(e,n).href},ub={},_u=function(n,r,a){let l=Promise.resolve();if(r&&r.length>0){let h=function(g){return Promise.all(g.map(x=>Promise.resolve(x).then(y=>({status:"fulfilled",value:y}),y=>({status:"rejected",reason:y}))))};const d=document.getElementsByTagName("link"),f=document.querySelector("meta[property=csp-nonce]"),m=f?.nonce||f?.getAttribute("nonce");l=h(r.map(g=>{if(g=FR(g,a),g in ub)return;ub[g]=!0;const x=g.endsWith(".css"),y=x?'[rel="stylesheet"]':"";if(a)for(let j=d.length-1;j>=0;j--){const N=d[j];if(N.href===g&&(!x||N.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${g}"]${y}`))return;const b=document.createElement("link");if(b.rel=x?"stylesheet":qR,x||(b.as="script"),b.crossOrigin="",b.href=g,m&&b.setAttribute("nonce",m),document.head.appendChild(b),x)return new Promise((j,N)=>{b.addEventListener("load",j),b.addEventListener("error",()=>N(new Error(`Unable to preload CSS for ${g}`)))})}))}function c(d){const f=new Event("vite:preloadError",{cancelable:!0});if(f.payload=d,window.dispatchEvent(f),!f.defaultPrevented)throw d}return l.then(d=>{for(const f of d||[])f.status==="rejected"&&c(f.reason);return n().catch(c)})},x2="devui_streaming_state_",y2=1440*60*1e3;function Yu(e){return`${x2}${e}`}function YR(e){let n="";for(const r of e)r.type==="response.output_text.delta"&&"delta"in r&&(n+=r.delta);return n}function v2(e){try{const n=Yu(e.conversationId),r=JSON.stringify(e);localStorage.setItem(n,r)}catch(n){console.error("Failed to save streaming state:",n);try{b2();const r=Yu(e.conversationId),a=JSON.stringify(e);localStorage.setItem(r,a)}catch{console.error("Failed to save streaming state even after cleanup")}}}function ba(e){try{const n=Yu(e),r=localStorage.getItem(n);if(!r)return null;const a=JSON.parse(r);return Date.now()-a.timestamp>y2?(Eu(e),null):a.completed?null:a}catch(n){return console.error("Failed to load streaming state:",n),null}}function Nh(e,n,r,a){try{const l=ba(e),c="sequence_number"in n?n.sequence_number:void 0,d=l?[...l.events,n]:[n],f={conversationId:e,responseId:r,lastMessageId:a,lastSequenceNumber:c??l?.lastSequenceNumber??-1,events:d,timestamp:Date.now(),completed:n.type==="response.completed"||n.type==="response.failed",accumulatedText:YR(d)};v2(f)}catch(l){console.error("Failed to update streaming state:",l)}}function jh(e){try{const n=ba(e);n&&(n.completed=!0,n.timestamp=Date.now(),v2(n))}catch(n){console.error("Failed to mark streaming as completed:",n)}}function Eu(e){try{const n=Yu(e);localStorage.removeItem(n)}catch(n){console.error("Failed to clear streaming state:",n)}}function b2(){try{const e=Object.keys(localStorage),n=Date.now();for(const r of e)if(r.startsWith(x2))try{const a=localStorage.getItem(r);if(a){const l=JSON.parse(a);(n-l.timestamp>y2||l.completed)&&localStorage.removeItem(r)}}catch{localStorage.removeItem(r)}}catch(e){console.error("Failed to clear expired streaming states:",e)}}function GR(){b2()}function w2(){const[e,n]=w.useState(!1),r=w.useRef(null),a=w.useCallback(()=>(r.current=new AbortController,n(!1),r.current.signal),[]),l=w.useCallback(()=>{r.current&&(n(!0),r.current.abort(),r.current=null)},[]),c=w.useCallback(()=>{n(!1)},[]),d=w.useCallback(()=>{r.current&&(r.current.abort(),r.current=null)},[]);return{isCancelling:e,createAbortSignal:a,handleCancel:l,resetCancelling:c,cleanup:d}}function Gu(e){return e instanceof DOMException&&e.name==="AbortError"}function XR(e={}){const{onDrop:n,disabled:r=!1}=e,[a,l]=w.useState(!1),[c,d]=w.useState([]),f=w.useRef(0),m=w.useCallback(b=>{b.preventDefault(),b.stopPropagation(),!r&&(f.current++,b.dataTransfer.items&&b.dataTransfer.items.length>0&&l(!0))},[r]),h=w.useCallback(b=>{b.preventDefault(),b.stopPropagation(),!r&&(f.current--,f.current===0&&l(!1))},[r]),g=w.useCallback(b=>{b.preventDefault(),b.stopPropagation()},[]),x=w.useCallback(b=>{if(b.preventDefault(),b.stopPropagation(),l(!1),f.current=0,r)return;const j=Array.from(b.dataTransfer.files);j.length>0&&(d(j),n?.(j))},[r,n]),y=w.useCallback(()=>{d([])},[]);return{isDragOver:a,droppedFiles:c,clearDroppedFiles:y,dragHandlers:{onDragEnter:m,onDragLeave:h,onDragOver:g,onDrop:x}}}const ZR="",WR=1e3,Sh=10;function KR(){const e=localStorage.getItem("devui_backend_url");return e||ZR}function QR(e){return new Promise(n=>setTimeout(n,e))}class JR{baseUrl;authToken=null;constructor(n){this.baseUrl=n||KR(),this.authToken=localStorage.getItem("devui_auth_token")}setBaseUrl(n){this.baseUrl=n}getBaseUrl(){return this.baseUrl}setAuthToken(n){this.authToken=n,n?localStorage.setItem("devui_auth_token",n):localStorage.removeItem("devui_auth_token")}getAuthToken(){return this.authToken}clearAuthToken(){this.setAuthToken(null)}async request(n,r={}){const a=`${this.baseUrl}${n}`,l={"Content-Type":"application/json",...r.headers};this.authToken&&(l.Authorization=`Bearer ${this.authToken}`);const c=await fetch(a,{...r,headers:l});if(!c.ok){if(c.status===401)throw this.clearAuthToken(),new Error("UNAUTHORIZED");let d=`API request failed: ${c.status} ${c.statusText}`;try{const f=await c.json();f.detail?typeof f.detail=="string"?d=f.detail:typeof f.detail=="object"&&f.detail.error?.message&&(d=f.detail.error.message):f.error?.message&&(d=f.error.message)}catch{}throw new Error(d)}return c.json()}async getHealth(){return this.request("/health")}async getMeta(){return this.request("/meta")}async getEntities(){const r=(await this.request("/v1/entities")).entities.map(c=>{if(c.type==="agent")return{id:c.id,name:c.name,description:c.description,type:"agent",source:c.source||"directory",tools:(c.tools||[]).map(d=>typeof d=="string"?d:JSON.stringify(d)),has_env:!!(c.required_env_vars&&c.required_env_vars.length>0),module_path:typeof c.metadata?.module_path=="string"?c.metadata.module_path:void 0,required_env_vars:c.required_env_vars,metadata:c.metadata,deployment_supported:c.deployment_supported,deployment_reason:c.deployment_reason,instructions:c.instructions,model_id:c.model_id,chat_client_type:c.chat_client_type,context_providers:c.context_providers,middleware:c.middleware};{const d=c.executors||c.tools||[];let f=c.start_executor_id||"";if(!f&&d.length>0){const m=d[0];typeof m=="string"&&(f=m)}return{id:c.id,name:c.name,description:c.description,type:"workflow",source:c.source||"directory",executors:d.map(m=>typeof m=="string"?m:JSON.stringify(m)),has_env:!!(c.required_env_vars&&c.required_env_vars.length>0),module_path:typeof c.metadata?.module_path=="string"?c.metadata.module_path:void 0,required_env_vars:c.required_env_vars,metadata:c.metadata,deployment_supported:c.deployment_supported,deployment_reason:c.deployment_reason,input_schema:c.input_schema||{type:"string"},input_type_name:c.input_type_name||"Input",start_executor_id:f,tools:[]}}}),a=r.filter(c=>c.type==="agent"),l=r.filter(c=>c.type==="workflow");return{entities:r,agents:a,workflows:l}}async getAgents(){const{agents:n}=await this.getEntities();return n}async getWorkflows(){const{workflows:n}=await this.getEntities();return n}async getAgentInfo(n){return this.request(`/v1/entities/${n}/info?type=agent`)}async getWorkflowInfo(n){return this.request(`/v1/entities/${n}/info?type=workflow`)}async reloadEntity(n){return this.request(`/v1/entities/${n}/reload`,{method:"POST"})}async createConversation(n){const{oaiMode:r}=await _u(()=>Promise.resolve().then(()=>wu),void 0,import.meta.url).then(c=>({oaiMode:c.useDevUIStore.getState().oaiMode})),a={};r.enabled&&(a["X-Proxy-Backend"]="openai");const l=await this.request("/v1/conversations",{method:"POST",headers:a,body:JSON.stringify({metadata:n})});return{id:l.id,object:"conversation",created_at:l.created_at,metadata:l.metadata}}async listConversations(n){const r=n?`/v1/conversations?agent_id=${encodeURIComponent(n)}`:"/v1/conversations",a=await this.request(r);return{data:a.data.map(l=>({id:l.id,object:"conversation",created_at:l.created_at,metadata:l.metadata})),has_more:a.has_more}}async getConversation(n){const r=await this.request(`/v1/conversations/${n}`);return{id:r.id,object:"conversation",created_at:r.created_at,metadata:r.metadata}}async deleteConversation(n){try{return await this.request(`/v1/conversations/${n}`,{method:"DELETE"}),Eu(n),!0}catch{return!1}}async listConversationItems(n,r){const a=new URLSearchParams;r?.limit&&a.set("limit",r.limit.toString()),r?.after&&a.set("after",r.after),r?.order&&a.set("order",r.order);const l=a.toString(),c=`/v1/conversations/${n}/items${l?`?${l}`:""}`;return this.request(c)}async getConversationItem(n,r){const a=`/v1/conversations/${n}/items/${r}`;return this.request(a)}async deleteConversationItem(n,r){const a=await fetch(`${this.baseUrl}/v1/conversations/${n}/items/${r}`,{method:"DELETE"});if(!a.ok)throw new Error(`Failed to delete item: ${a.statusText}`)}async*streamOpenAIResponse(n,r,a,l){const{oaiMode:c}=await _u(()=>Promise.resolve().then(()=>wu),void 0,import.meta.url).then(x=>({oaiMode:x.useDevUIStore.getState().oaiMode}));c.enabled&&(n.model=c.model,c.temperature!==void 0&&(n.temperature=c.temperature),c.max_output_tokens!==void 0&&(n.max_output_tokens=c.max_output_tokens),c.top_p!==void 0&&(n.top_p=c.top_p),c.instructions!==void 0&&(n.instructions=c.instructions),c.reasoning_effort!==void 0&&(n.reasoning={effort:c.reasoning_effort}));let d=-1,f=0,m=!1,h=l,g;if(r){const x=ba(r);if(x)if(l||(h=x.responseId),d=x.lastSequenceNumber,g=x.lastMessageId,l)m=x.events.length>0;else for(const y of x.events)m=!0,yield y}for(;f<=Sh;)try{let x;if(h){const N=new URLSearchParams;N.set("stream","true"),d>=0&&N.set("starting_after",d.toString());const S=`${this.baseUrl}/v1/responses/${h}?${N.toString()}`,_={Accept:"text/event-stream"};this.authToken&&(_.Authorization=`Bearer ${this.authToken}`),x=await fetch(S,{method:"GET",headers:_,signal:a})}else{const N=`${this.baseUrl}/v1/responses`,S={"Content-Type":"application/json",Accept:"text/event-stream"};c.enabled&&(S["X-Proxy-Backend"]="openai"),this.authToken&&(S.Authorization=`Bearer ${this.authToken}`),x=await fetch(N,{method:"POST",headers:S,body:JSON.stringify(n),signal:a})}if(!x.ok){if(x.status===401)throw this.clearAuthToken(),new Error("UNAUTHORIZED");if(x.status>=400&&x.status<500){let S=`Client error ${x.status}`;try{const _=await x.json();_.error&&_.error.message?S=_.error.message:_.detail&&(S=_.detail)}catch{}throw new Error(`CLIENT_ERROR: ${S}`)}let N=`Request failed with status ${x.status}`;try{const S=await x.json();S.error&&S.error.message?N=S.error.message:S.detail&&(N=S.detail)}catch{}throw new Error(N)}const y=x.body?.getReader();if(!y)throw new Error("Response body is not readable");const b=new TextDecoder;let j="";try{for(;;){if(a?.aborted)throw new DOMException("Request aborted","AbortError");const{done:N,value:S}=await y.read();if(N){r&&jh(r);return}const _=b.decode(S,{stream:!0});j+=_;const A=j.split(` `);j=A.pop()||"";for(const E of A)if(E.startsWith("data: ")){const M=E.slice(6);if(M==="[DONE]"){r&&jh(r);return}try{const T=JSON.parse(M);if("response"in T&&T.response&&typeof T.response=="object"&&"id"in T.response){const z=T.response.id;(!h||h!==z)&&(h=z)}else if("id"in T&&typeof T.id=="string"&&T.id.startsWith("resp_")){const z=T.id;(!h||h!==z)&&(h=z)}"item_id"in T&&T.item_id&&(g=T.item_id);const D="sequence_number"in T?T.sequence_number:void 0;if(D!==void 0)if(m&&D<=1&&d>1)r&&Eu(r),yield{type:"error",message:"Connection lost - previous response failed. Starting new response."},d=D,m=!0,r&&h&&Nh(r,T,h,g),yield T;else{if(D<=d)continue;d=D,m=!0,r&&h&&Nh(r,T,h,g),yield T}else m=!0,r&&h&&Nh(r,T,h,g),yield T}catch(T){console.error("Failed to parse OpenAI SSE event:",T)}}}}finally{y.releaseLock()}}catch(x){const y=x instanceof Error?x.message:String(x);if(Gu(x))throw r&&jh(r),x;if(y==="UNAUTHORIZED"||y.startsWith("CLIENT_ERROR:"))throw x;if(f++,f>Sh)throw new Error(`Connection failed after ${Sh} retry attempts: ${y}`);const b=Math.min(WR*Math.pow(2,f-1),3e4);await QR(b)}}async*streamAgentExecutionOpenAI(n,r,a,l){const c={metadata:{entity_id:n},input:r.input,stream:!0,conversation:r.conversation_id};return yield*this.streamAgentExecutionOpenAIDirect(n,c,r.conversation_id,a,l)}async*streamAgentExecutionOpenAIDirect(n,r,a,l,c){yield*this.streamOpenAIResponse(r,a,l,c)}async*streamWorkflowExecutionOpenAI(n,r,a){const l={metadata:{entity_id:n},input:JSON.stringify(r.input_data||{}),stream:!0,conversation:r.conversation_id,extra_body:r.checkpoint_id?{entity_id:n,checkpoint_id:r.checkpoint_id}:void 0};yield*this.streamOpenAIResponse(l,r.conversation_id,a)}async runAgentSync(n,r){const{oaiMode:a}=await _u(()=>Promise.resolve().then(()=>wu),void 0,import.meta.url).then(d=>({oaiMode:d.useDevUIStore.getState().oaiMode})),l={metadata:{entity_id:n},input:r.input,stream:!1,conversation:r.conversation_id};a.enabled&&(l.model=a.model,a.temperature!==void 0&&(l.temperature=a.temperature),a.max_output_tokens!==void 0&&(l.max_output_tokens=a.max_output_tokens));const c={};return a.enabled&&(c["X-Proxy-Backend"]="openai"),this.request("/v1/responses",{method:"POST",headers:c,body:JSON.stringify(l)})}async runWorkflowSync(n,r){const a={metadata:{entity_id:n},input:JSON.stringify(r.input_data||{}),stream:!1,conversation:r.conversation_id,extra_body:r.checkpoint_id?{entity_id:n,checkpoint_id:r.checkpoint_id}:void 0};return this.request("/v1/responses",{method:"POST",body:JSON.stringify(a)})}clearStreamingState(n){Eu(n)}async*streamDeployment(n){const r=await fetch(`${this.baseUrl}/v1/deployments`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...n,stream:!0})});if(!r.ok)throw new Error(`Deployment failed: ${r.statusText}`);const a=r.body?.getReader();if(!a)throw new Error("No response body");const l=new TextDecoder;let c="";try{for(;;){const{done:d,value:f}=await a.read();if(d)break;c+=l.decode(f,{stream:!0});const m=c.split(` `);c=m.pop()||"";for(const h of m)if(h.startsWith("data: ")){const g=h.slice(6);if(g==="[DONE]")return;try{yield JSON.parse(g)}catch(x){yield{type:"deploy.error",message:`Failed to parse deployment event: ${x instanceof Error?x.message:"Unknown error"}`}}}}}catch(d){throw yield{type:"deploy.failed",message:`Stream interrupted: ${d instanceof Error?d.message:"Unknown error"}`},d}finally{a.releaseLock()}}async listWorkflowSessions(n){const r=`/v1/conversations?entity_id=${encodeURIComponent(n)}&type=workflow_session`;return{data:(await this.request(r)).data.map(c=>({conversation_id:c.id,entity_id:c.metadata?.entity_id||n,created_at:c.created_at,metadata:{name:c.metadata?.name||`Session ${new Date(c.created_at*1e3).toLocaleString()}`,description:c.metadata?.description,type:"workflow_session",checkpoint_summary:c.metadata?.checkpoint_summary}}))}}async createWorkflowSession(n,r){const a={entity_id:n,type:"workflow_session",name:r?.name||`Session ${new Date().toLocaleString()}`,...r?.description&&{description:r.description}},l=await this.createConversation(a);return{conversation_id:l.id,entity_id:n,created_at:l.created_at,metadata:{name:a.name,description:a.description,type:"workflow_session"}}}async deleteWorkflowSession(n,r){if(!await this.deleteConversation(r))throw new Error("Failed to delete workflow session")}}const Ze=new JR;function eD({open:e,onClose:n,agentName:r="Agent",entity:a}){const c=le(C=>C.azureDeploymentEnabled)&&(a?.deployment_supported??!1),[d,f]=w.useState(c?"azure":"docker"),[m,h]=w.useState(null),g=w.useRef(null),x=w.useRef(null),y=le(C=>C.isDeploying),b=le(C=>C.deploymentLogs),j=le(C=>C.lastDeployment),N=le(C=>C.startDeployment),S=le(C=>C.addDeploymentLog),_=le(C=>C.setDeploymentResult),A=le(C=>C.stopDeployment),E=le(C=>C.clearDeploymentState),M=C=>{const $=C.toLowerCase().replace(/[_\s]+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/--+/g,"-").replace(/^[^a-z]+/,"").replace(/-$/,"");return($.match(/^[a-z]/)?$:`app-${$}`).substring(0,31)},T=a?M(a.id):"",[D,z]=w.useState("my-test-rg"),[H,q]=w.useState(T),[X,W]=w.useState("eastus"),[G,ne]=w.useState(null);w.useEffect(()=>{if(a){const C=M(a.id);q(C);const $=B(C);ne($)}},[a?.id]),w.useEffect(()=>{x.current&&b.length>0&&(x.current.scrollTop=x.current.scrollHeight)},[b]);const B=C=>C?C.length>=32?"App name must be less than 32 characters":/^[a-z0-9-]+$/.test(C)?/^[a-z]/.test(C)?/[a-z0-9]$/.test(C)?C.includes("--")?"App name cannot contain consecutive hyphens (--)":null:"App name must end with a letter or number":"App name must start with a lowercase letter":"App name must contain only lowercase letters, numbers, and hyphens (no underscores or uppercase)":null;w.useEffect(()=>()=>{g.current&&clearTimeout(g.current)},[]);const U=async()=>{if(!a?.id||!D||!H)return;const C=D.trim(),$=H.trim(),Y=B($);if(Y){ne(Y);return}try{N();for await(const V of Ze.streamDeployment({entity_id:a.id,resource_group:C,app_name:$,region:X,ui_mode:"user"}))S(V.message),V.type==="deploy.completed"&&V.url&&V.auth_token?_({url:V.url,authToken:V.auth_token}):V.type==="deploy.failed"&&A()}catch(V){S(`Error: ${V instanceof Error?V.message:"Deployment failed"}`),A()}},R=async(C,$)=>{try{await navigator.clipboard.writeText(C),h($),g.current&&clearTimeout(g.current),g.current=setTimeout(()=>{h(null),g.current=null},2e3)}catch{h(null)}},L=`# Dockerfile for ${r} FROM python:3.11-slim diff --git a/python/packages/devui/frontend/src/components/features/workflow/run-workflow-button.tsx b/python/packages/devui/frontend/src/components/features/workflow/run-workflow-button.tsx index d4d1bc4887..9cddab5212 100644 --- a/python/packages/devui/frontend/src/components/features/workflow/run-workflow-button.tsx +++ b/python/packages/devui/frontend/src/components/features/workflow/run-workflow-button.tsx @@ -76,7 +76,7 @@ export function RunWorkflowButton({ // Analyze input requirements const inputAnalysis = useMemo(() => { - // Check if this is a ChatMessage schema (for AgentExecutor workflows) + // Check if this is a Message schema (for AgentExecutor workflows) const isChatMessage = isChatMessageSchema(inputSchema); if (!inputSchema) diff --git a/python/packages/devui/frontend/src/components/features/workflow/schema-form-renderer.tsx b/python/packages/devui/frontend/src/components/features/workflow/schema-form-renderer.tsx index 9abf6a1074..b37761ba5d 100644 --- a/python/packages/devui/frontend/src/components/features/workflow/schema-form-renderer.tsx +++ b/python/packages/devui/frontend/src/components/features/workflow/schema-form-renderer.tsx @@ -115,7 +115,7 @@ export function getFieldColumnSpan( } // ============================================================================ -// ChatMessage Pattern Detection (exported for reuse) +// Message Pattern Detection (exported for reuse) // ============================================================================ export function detectChatMessagePattern( @@ -436,7 +436,7 @@ export function SchemaFormRenderer({ (name) => !hideFields.includes(name) ); - // Detect ChatMessage pattern + // Detect Message pattern const isChatMessageLike = detectChatMessagePattern(schema, requiredFields); // Separate required and optional fields @@ -449,7 +449,7 @@ export function SchemaFormRenderer({ (name) => !requiredFields.includes(name) ); - // For ChatMessage: prioritize text/message/content + // For Message: prioritize text/message/content const sortedOptionalFields = isChatMessageLike ? [...optionalFieldNames].sort((a, b) => { const priority = (name: string) => diff --git a/python/packages/devui/frontend/src/components/features/workflow/workflow-input-form.tsx b/python/packages/devui/frontend/src/components/features/workflow/workflow-input-form.tsx index 367e1d5deb..bcb0a940e9 100644 --- a/python/packages/devui/frontend/src/components/features/workflow/workflow-input-form.tsx +++ b/python/packages/devui/frontend/src/components/features/workflow/workflow-input-form.tsx @@ -48,7 +48,7 @@ export function WorkflowInputForm({ const requiredFields = inputSchema.required || []; const isSimpleInput = inputSchema.type === "string" && !inputSchema.enum; - // Detect ChatMessage-like pattern for auto-filling role + // Detect Message-like pattern for auto-filling role const isChatMessageLike = detectChatMessagePattern(inputSchema, requiredFields); // Validation: check if required fields are filled @@ -82,7 +82,7 @@ export function WorkflowInputForm({ } }); - // Auto-fill role="user" for ChatMessage-like inputs + // Auto-fill role="user" for Message-like inputs if (isChatMessageLike && !initialData["role"]) { initialData["role"] = "user"; } diff --git a/python/packages/devui/frontend/src/components/layout/debug-panel.tsx b/python/packages/devui/frontend/src/components/layout/debug-panel.tsx index 828e03666f..19797ac74b 100644 --- a/python/packages/devui/frontend/src/components/layout/debug-panel.tsx +++ b/python/packages/devui/frontend/src/components/layout/debug-panel.tsx @@ -1340,7 +1340,7 @@ function TraceTreeNode({ node, depth = 0 }: { node: TraceNode; depth?: number }) {/* Operation badge */} - {operationName.replace("ChatAgent.", "").replace("invoke_agent ", "")} + {operationName.replace("Agent.", "").replace("invoke_agent ", "")} {/* Duration */} diff --git a/python/packages/devui/frontend/src/types/agent-framework.ts b/python/packages/devui/frontend/src/types/agent-framework.ts index 5e26580d5f..5a63b8d914 100644 --- a/python/packages/devui/frontend/src/types/agent-framework.ts +++ b/python/packages/devui/frontend/src/types/agent-framework.ts @@ -223,7 +223,7 @@ export interface AgentResponseUpdate { // Agent run response (final) export interface AgentResponse { - messages: ChatMessage[]; + messages: Message[]; response_id?: string; created_at?: CreatedAtT; usage_details?: UsageDetails; @@ -232,7 +232,7 @@ export interface AgentResponse { } // Chat message -export interface ChatMessage { +export interface Message { contents: Content[]; role?: Role; author_name?: string; diff --git a/python/packages/devui/frontend/src/types/index.ts b/python/packages/devui/frontend/src/types/index.ts index 7d6e9a8f73..dc79cc43d4 100644 --- a/python/packages/devui/frontend/src/types/index.ts +++ b/python/packages/devui/frontend/src/types/index.ts @@ -185,7 +185,7 @@ export interface MetaResponse { } // Chat message types matching Agent Framework -export interface ChatMessage { +export interface Message { id: string; role: "user" | "assistant" | "system" | "tool"; contents: import("./agent-framework").Content[]; @@ -212,7 +212,7 @@ export interface AppState { } export interface ChatState { - messages: ChatMessage[]; + messages: Message[]; isStreaming: boolean; // streamEvents removed - use OpenAI events directly instead } diff --git a/python/packages/devui/frontend/src/utils/workflow-utils.ts b/python/packages/devui/frontend/src/utils/workflow-utils.ts index adda06e2d2..6d8c45c019 100644 --- a/python/packages/devui/frontend/src/utils/workflow-utils.ts +++ b/python/packages/devui/frontend/src/utils/workflow-utils.ts @@ -15,8 +15,8 @@ import type { Workflow } from "@/types/workflow"; import { getTypedWorkflow } from "@/types/workflow"; /** - * Detects if a JSON schema represents a ChatMessage input type. - * ChatMessage schemas typically have: + * Detects if a JSON schema represents a Message input type. + * Message schemas typically have: * - type: "object" * - properties with "text" (required string) and "role" (optional string) * @@ -24,7 +24,7 @@ import { getTypedWorkflow } from "@/types/workflow"; * component for workflows that start with an AgentExecutor. * * @param schema - The JSON schema to check - * @returns true if the schema represents a ChatMessage-like input + * @returns true if the schema represents a Message-like input */ export function isChatMessageSchema(schema: JSONSchemaProperty | undefined): boolean { if (!schema) return false; @@ -37,13 +37,13 @@ export function isChatMessageSchema(schema: JSONSchemaProperty | undefined): boo const props = schema.properties; - // ChatMessage has "text" property (the main content) + // Message has "text" property (the main content) const hasText = "text" in props && props.text?.type === "string"; - // ChatMessage has "role" property (user, assistant, system) + // Message has "role" property (user, assistant, system) const hasRole = "role" in props && props.role?.type === "string"; - // If it has both text and role, it's likely a ChatMessage + // If it has both text and role, it's likely a Message if (hasText && hasRole) { return true; } diff --git a/python/packages/devui/pyproject.toml b/python/packages/devui/pyproject.toml index 6dbdd27f3c..aad802d763 100644 --- a/python/packages/devui/pyproject.toml +++ b/python/packages/devui/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://github.com/microsoft/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "fastapi>=0.104.0", "uvicorn[standard]>=0.24.0", "python-dotenv>=1.0.0", diff --git a/python/packages/devui/tests/devui/conftest.py b/python/packages/devui/tests/devui/conftest.py index 4d6f818795..1b1fd0610f 100644 --- a/python/packages/devui/tests/devui/conftest.py +++ b/python/packages/devui/tests/devui/conftest.py @@ -17,16 +17,16 @@ from typing import Any, Generic import pytest import pytest_asyncio from agent_framework import ( + Agent, AgentResponse, AgentResponseUpdate, AgentThread, BaseAgent, BaseChatClient, - ChatAgent, - ChatMessage, ChatResponse, ChatResponseUpdate, Content, + Message, ResponseStream, ) from agent_framework._clients import OptionsCoT @@ -67,17 +67,17 @@ class MockChatClient: async def get_response( self, - messages: str | ChatMessage | list[str] | list[ChatMessage], + messages: str | Message | list[str] | list[Message], **kwargs: Any, ) -> ChatResponse: self.call_count += 1 if self.responses: return self.responses.pop(0) - return ChatResponse(messages=ChatMessage("assistant", ["test response"])) + return ChatResponse(messages=Message("assistant", ["test response"])) async def get_streaming_response( self, - messages: str | ChatMessage | list[str] | list[ChatMessage], + messages: str | Message | list[str] | list[Message], **kwargs: Any, ) -> AsyncIterable[ChatResponseUpdate]: self.call_count += 1 @@ -101,13 +101,13 @@ class MockBaseChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]): self.run_responses: list[ChatResponse] = [] self.streaming_responses: list[list[ChatResponseUpdate]] = [] self.call_count: int = 0 - self.received_messages: list[list[ChatMessage]] = [] + self.received_messages: list[list[Message]] = [] @override def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], stream: bool, options: Mapping[str, Any], **kwargs: Any, @@ -120,11 +120,11 @@ class MockBaseChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]): self.received_messages.append(list(messages)) if self.run_responses: return self.run_responses.pop(0) - return ChatResponse(messages=ChatMessage("assistant", ["Mock response from ChatAgent"])) + return ChatResponse(messages=Message("assistant", ["Mock response from Agent"])) return _get() - async def _stream_impl(self, messages: Sequence[ChatMessage]) -> AsyncIterable[ChatResponseUpdate]: + async def _stream_impl(self, messages: Sequence[Message]) -> AsyncIterable[ChatResponseUpdate]: self.call_count += 1 self.received_messages.append(list(messages)) if self.streaming_responses: @@ -135,7 +135,7 @@ class MockBaseChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]): yield ChatResponseUpdate(contents=[Content.from_text(text="Mock ")], role="assistant") yield ChatResponseUpdate(contents=[Content.from_text(text="streaming ")], role="assistant") yield ChatResponseUpdate(contents=[Content.from_text(text="response ")], role="assistant") - yield ChatResponseUpdate(contents=[Content.from_text(text="from ChatAgent")], role="assistant") + yield ChatResponseUpdate(contents=[Content.from_text(text="from Agent")], role="assistant") # ============================================================================= @@ -159,7 +159,7 @@ class MockAgent(BaseAgent): def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -172,17 +172,17 @@ class MockAgent(BaseAgent): async def _run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, ) -> AgentResponse: self.call_count += 1 - return AgentResponse(messages=[ChatMessage("assistant", [Content.from_text(text=self.response_text)])]) + return AgentResponse(messages=[Message("assistant", [Content.from_text(text=self.response_text)])]) def _run_stream( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -205,7 +205,7 @@ class MockToolCallingAgent(BaseAgent): def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -218,16 +218,16 @@ class MockToolCallingAgent(BaseAgent): async def _run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, ) -> AgentResponse: - return AgentResponse(messages=[ChatMessage("assistant", ["done"])]) + return AgentResponse(messages=[Message("assistant", ["done"])]) def _run_stream( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -275,7 +275,7 @@ class MockToolCallingAgent(BaseAgent): def _create_agent_run_response(text: str = "Test response") -> AgentResponse: """Create an AgentResponse with the given text.""" - return AgentResponse(messages=[ChatMessage("assistant", [Content.from_text(text=text)])]) + return AgentResponse(messages=[Message("assistant", [Content.from_text(text=text)])]) def _create_agent_executor_response( @@ -289,8 +289,8 @@ def _create_agent_executor_response( executor_id=executor_id, agent_response=agent_response, full_conversation=[ - ChatMessage("user", [Content.from_text(text="User input")]), - ChatMessage("assistant", [Content.from_text(text=response_text)]), + Message("user", [Content.from_text(text="User input")]), + Message("assistant", [Content.from_text(text=response_text)]), ], ) @@ -318,7 +318,7 @@ def create_executor_completed_event( This creates the exact data structure that caused the serialization bug: WorkflowEvent.data contains AgentExecutorResponse which contains - AgentResponse and ChatMessage objects (SerializationMixin, not Pydantic). + AgentResponse and Message objects (SerializationMixin, not Pydantic). """ data = _create_agent_executor_response(executor_id) if with_agent_response else {"simple": "dict"} return WorkflowEvent.executor_completed(executor_id=executor_id, data=data) @@ -390,7 +390,7 @@ def executor_completed_event() -> WorkflowEvent[Any]: This creates the exact data structure that caused the serialization bug: executor_completed event (type='executor_completed').data contains AgentExecutorResponse which contains - AgentResponse and ChatMessage objects (SerializationMixin, not Pydantic). + AgentResponse and Message objects (SerializationMixin, not Pydantic). """ data = _create_agent_executor_response("test_executor") return WorkflowEvent.executor_completed(executor_id="test_executor", data=data) @@ -425,10 +425,10 @@ def test_entities_dir() -> str: @pytest_asyncio.fixture async def executor_with_real_agent() -> tuple[AgentFrameworkExecutor, str, MockBaseChatClient]: - """Create an executor with a REAL ChatAgent using mock chat client. + """Create an executor with a REAL Agent using mock chat client. This tests the full execution pipeline: - - Real ChatAgent class + - Real Agent class - Real message handling and normalization - Real middleware pipeline - Only the LLM call is mocked @@ -440,12 +440,12 @@ async def executor_with_real_agent() -> tuple[AgentFrameworkExecutor, str, MockB mapper = MessageMapper() executor = AgentFrameworkExecutor(discovery, mapper) - # Create a REAL ChatAgent with mock client - agent = ChatAgent( + # Create a REAL Agent with mock client + agent = Agent( id="test_chat_agent", name="Test Chat Agent", - description="A real ChatAgent for testing execution flow", - chat_client=mock_client, + description="A real Agent for testing execution flow", + client=mock_client, system_message="You are a helpful test assistant.", ) @@ -469,22 +469,22 @@ async def sequential_workflow() -> tuple[AgentFrameworkExecutor, str, MockBaseCh """ mock_client = MockBaseChatClient() mock_client.run_responses = [ - ChatResponse(messages=ChatMessage("assistant", ["Here's the draft content about the topic."])), - ChatResponse(messages=ChatMessage("assistant", ["Review: Content is clear and well-structured."])), + ChatResponse(messages=Message("assistant", ["Here's the draft content about the topic."])), + ChatResponse(messages=Message("assistant", ["Review: Content is clear and well-structured."])), ] - writer = ChatAgent( + writer = Agent( id="writer", name="Writer", description="Content writer agent", - chat_client=mock_client, + client=mock_client, system_message="You are a content writer. Create clear, engaging content.", ) - reviewer = ChatAgent( + reviewer = Agent( id="reviewer", name="Reviewer", description="Content reviewer agent", - chat_client=mock_client, + client=mock_client, system_message="You are a reviewer. Provide constructive feedback.", ) @@ -513,30 +513,30 @@ async def concurrent_workflow() -> tuple[AgentFrameworkExecutor, str, MockBaseCh """ mock_client = MockBaseChatClient() mock_client.run_responses = [ - ChatResponse(messages=ChatMessage("assistant", ["Research findings: Key data points identified."])), - ChatResponse(messages=ChatMessage("assistant", ["Analysis: Trends indicate positive growth."])), - ChatResponse(messages=ChatMessage("assistant", ["Summary: Overall outlook is favorable."])), + ChatResponse(messages=Message("assistant", ["Research findings: Key data points identified."])), + ChatResponse(messages=Message("assistant", ["Analysis: Trends indicate positive growth."])), + ChatResponse(messages=Message("assistant", ["Summary: Overall outlook is favorable."])), ] - researcher = ChatAgent( + researcher = Agent( id="researcher", name="Researcher", description="Research agent", - chat_client=mock_client, + client=mock_client, system_message="You are a researcher. Find key data and insights.", ) - analyst = ChatAgent( + analyst = Agent( id="analyst", name="Analyst", description="Analysis agent", - chat_client=mock_client, + client=mock_client, system_message="You are an analyst. Identify trends and patterns.", ) - summarizer = ChatAgent( + summarizer = Agent( id="summarizer", name="Summarizer", description="Summary agent", - chat_client=mock_client, + client=mock_client, system_message="You are a summarizer. Provide concise summaries.", ) diff --git a/python/packages/devui/tests/devui/test_cleanup_hooks.py b/python/packages/devui/tests/devui/test_cleanup_hooks.py index f8bdf5c867..8d02bfaf27 100644 --- a/python/packages/devui/tests/devui/test_cleanup_hooks.py +++ b/python/packages/devui/tests/devui/test_cleanup_hooks.py @@ -7,7 +7,7 @@ import tempfile from pathlib import Path import pytest -from agent_framework import AgentResponse, ChatMessage, Content +from agent_framework import AgentResponse, Content, Message from agent_framework_devui import register_cleanup from agent_framework_devui._discovery import EntityDiscovery @@ -39,12 +39,12 @@ class MockAgent: async def _stream(): yield AgentResponse( - messages=[ChatMessage(role="assistant", contents=[Content.from_text(text="Test response")])], + messages=[Message(role="assistant", contents=[Content.from_text(text="Test response")])], ) return _stream() return AgentResponse( - messages=[ChatMessage(role="assistant", contents=[Content.from_text(text="Test response")])], + messages=[Message(role="assistant", contents=[Content.from_text(text="Test response")])], ) @@ -267,7 +267,7 @@ async def test_cleanup_with_file_based_discovery(): # Write agent module with cleanup registration agent_file = agent_dir / "__init__.py" agent_file.write_text(""" -from agent_framework import AgentResponse, ChatMessage, Role, Content +from agent_framework import AgentResponse, Message, Role, Content from agent_framework_devui import register_cleanup class MockCredential: @@ -289,12 +289,12 @@ class TestAgent: if stream: async def _stream(): yield AgentResponse( - messages=[ChatMessage(role="assistant", content=[Content.from_text(text="Test")])], + messages=[Message(role="assistant", content=[Content.from_text(text="Test")])], inner_messages=[], ) return _stream() return AgentResponse( - messages=[ChatMessage(role="assistant", content=[Content.from_text(text="Test")])], + messages=[Message(role="assistant", content=[Content.from_text(text="Test")])], inner_messages=[], ) diff --git a/python/packages/devui/tests/devui/test_conversations.py b/python/packages/devui/tests/devui/test_conversations.py index dbc2e4ddb2..ccaea3524c 100644 --- a/python/packages/devui/tests/devui/test_conversations.py +++ b/python/packages/devui/tests/devui/test_conversations.py @@ -199,7 +199,7 @@ async def test_list_items_pagination(): @pytest.mark.asyncio async def test_list_items_converts_function_calls(): """Test that list_items properly converts function calls to ResponseFunctionToolCallItem.""" - from agent_framework import ChatMessage, ChatMessageStore + from agent_framework import ChatMessageStore, Message store = InMemoryConversationStore() @@ -216,8 +216,8 @@ async def test_list_items_converts_function_calls(): # Simulate messages from agent execution with function calls messages = [ - ChatMessage(role="user", contents=[{"type": "text", "text": "What's the weather in SF?"}]), - ChatMessage( + Message(role="user", contents=[{"type": "text", "text": "What's the weather in SF?"}]), + Message( role="assistant", contents=[ { @@ -228,7 +228,7 @@ async def test_list_items_converts_function_calls(): } ], ), - ChatMessage( + Message( role="tool", contents=[ { @@ -238,7 +238,7 @@ async def test_list_items_converts_function_calls(): } ], ), - ChatMessage(role="assistant", contents=[{"type": "text", "text": "The weather is sunny, 65°F"}]), + Message(role="assistant", contents=[{"type": "text", "text": "The weather is sunny, 65°F"}]), ] # Add messages to thread @@ -284,7 +284,7 @@ async def test_list_items_converts_function_calls(): @pytest.mark.asyncio async def test_list_items_handles_images_and_files(): """Test that list_items properly converts data content (images/files) to OpenAI types.""" - from agent_framework import ChatMessage, ChatMessageStore + from agent_framework import ChatMessageStore, Message store = InMemoryConversationStore() @@ -300,7 +300,7 @@ async def test_list_items_handles_images_and_files(): # Simulate message with image and file messages = [ - ChatMessage( + Message( role="user", contents=[ {"type": "text", "text": "Check this image and PDF"}, diff --git a/python/packages/devui/tests/devui/test_discovery.py b/python/packages/devui/tests/devui/test_discovery.py index c5e92b4645..d1f68c302f 100644 --- a/python/packages/devui/tests/devui/test_discovery.py +++ b/python/packages/devui/tests/devui/test_discovery.py @@ -74,7 +74,7 @@ async def test_discovery_accepts_agents_with_only_run(): init_file = agent_dir / "__init__.py" init_file.write_text(""" -from agent_framework import AgentResponse, AgentThread, ChatMessage, Role, Content +from agent_framework import AgentResponse, AgentThread, Message, Role, Content class NonStreamingAgent: id = "non_streaming" @@ -83,7 +83,7 @@ class NonStreamingAgent: async def run(self, messages=None, *, thread=None, **kwargs): return AgentResponse( - messages=[ChatMessage( + messages=[Message( role="assistant", contents=[Content.from_text(text="response")] )], @@ -188,14 +188,14 @@ workflow = WorkflowBuilder(start_executor=executor).build() agent_dir = temp_path / "my_agent" agent_dir.mkdir() (agent_dir / "agent.py").write_text(""" -from agent_framework import AgentResponse, AgentThread, ChatMessage, Role, TextContent +from agent_framework import AgentResponse, AgentThread, Message, Role, TextContent class TestAgent: name = "Test Agent" async def run(self, messages=None, *, thread=None, **kwargs): return AgentResponse( - messages=[ChatMessage(role="assistant", contents=[Content.from_text(text="test")])], + messages=[Message(role="assistant", contents=[Content.from_text(text="test")])], response_id="test" ) diff --git a/python/packages/devui/tests/devui/test_execution.py b/python/packages/devui/tests/devui/test_execution.py index 3dd417cbf6..3fff11ad79 100644 --- a/python/packages/devui/tests/devui/test_execution.py +++ b/python/packages/devui/tests/devui/test_execution.py @@ -4,7 +4,7 @@ Tests include: - Entity discovery and info retrieval -- Agent execution (sync and streaming) using real ChatAgent with mock LLM +- Agent execution (sync and streaming) using real Agent with mock LLM - Workflow execution using real WorkflowBuilder with FunctionExecutor - Edge cases like non-streaming agents """ @@ -15,7 +15,7 @@ from pathlib import Path from typing import Any import pytest -from agent_framework import AgentExecutor, ChatAgent, FunctionExecutor, WorkflowBuilder +from agent_framework import Agent, AgentExecutor, FunctionExecutor, WorkflowBuilder # Import mock classes from conftest for direct use in some tests from conftest import MockBaseChatClient @@ -77,15 +77,15 @@ async def test_executor_get_entity_info(executor): # ============================================================================= -# Agent Execution Tests (using real ChatAgent with mock LLM) +# Agent Execution Tests (using real Agent with mock LLM) # ============================================================================= async def test_agent_sync_execution(executor_with_real_agent): - """Test synchronous agent execution with REAL ChatAgent (mock LLM). + """Test synchronous agent execution with REAL Agent (mock LLM). This tests the full execution pipeline without needing an API key: - - Real ChatAgent class with middleware + - Real Agent class with middleware - Real message normalization - Mock chat client for LLM calls """ @@ -130,7 +130,7 @@ async def test_agent_sync_execution_respects_model_field(executor_with_real_agen async def test_chat_client_receives_correct_messages(executor_with_real_agent): """Verify the mock chat client receives properly formatted messages. - This tests that the REAL ChatAgent properly: + This tests that the REAL Agent properly: - Normalizes input messages - Formats messages for the chat client """ @@ -297,18 +297,18 @@ async def test_full_pipeline_workflow_events_are_json_serializable(): This is particularly important for workflows with AgentExecutor because: - AgentExecutor produces executor_completed event (type='executor_completed') with AgentExecutorResponse - - AgentExecutorResponse contains AgentResponse and ChatMessage objects + - AgentExecutorResponse contains AgentResponse and Message objects - These are SerializationMixin objects, not Pydantic, which caused the original bug This test ensures the ENTIRE streaming pipeline works end-to-end. """ # Create a workflow with AgentExecutor (the problematic case) mock_client = MockBaseChatClient() - agent = ChatAgent( + agent = Agent( id="serialization_test_agent", name="Serialization Test Agent", description="Agent for testing serialization", - chat_client=mock_client, + client=mock_client, system_message="You are a test assistant.", ) @@ -466,15 +466,15 @@ async def test_executor_parse_raw_string_for_string_workflow(): @pytest.mark.asyncio async def test_executor_parse_converts_to_chat_message_for_sequential_workflow(sequential_workflow): - """Sequential workflows convert string input to ChatMessage.""" - from agent_framework import ChatMessage + """Sequential workflows convert string input to Message.""" + from agent_framework import Message executor, _entity_id, _mock_client, workflow = sequential_workflow - # Sequential workflows expect ChatMessage, so raw string becomes ChatMessage + # Sequential workflows expect Message, so raw string becomes Message parsed = executor._parse_raw_workflow_input(workflow, "hello") - assert isinstance(parsed, ChatMessage) + assert isinstance(parsed, Message) assert parsed.text == "hello" @@ -538,7 +538,7 @@ def test_extract_workflow_hil_responses_handles_stringified_json(): async def test_executor_handles_streaming_agent(): """Test executor handles agents with run(stream=True) method.""" - from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, ChatMessage, Content + from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, Content, Message class StreamingAgent: """Agent with run() method supporting stream parameter.""" @@ -556,7 +556,7 @@ async def test_executor_handles_streaming_agent(): async def _run_impl(self, messages): return AgentResponse( - messages=[ChatMessage(role="assistant", contents=[Content.from_text(text=f"Processed: {messages}")])], + messages=[Message(role="assistant", contents=[Content.from_text(text=f"Processed: {messages}")])], response_id="test_123", ) diff --git a/python/packages/devui/tests/devui/test_mapper.py b/python/packages/devui/tests/devui/test_mapper.py index 3609cd774b..bab2130a99 100644 --- a/python/packages/devui/tests/devui/test_mapper.py +++ b/python/packages/devui/tests/devui/test_mapper.py @@ -304,7 +304,7 @@ async def test_executor_completed_event_with_agent_response( This is a REGRESSION TEST for the serialization bug where WorkflowEvent.data contained AgentExecutorResponse with nested - AgentResponse and ChatMessage objects (SerializationMixin) that + AgentResponse and Message objects (SerializationMixin) that Pydantic couldn't serialize. """ # Create event with realistic nested data - the exact structure that caused the bug @@ -579,13 +579,13 @@ async def test_workflow_output_event(mapper: MessageMapper, test_request: AgentF async def test_workflow_output_event_with_list_data(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None: """Test output event (type='output') with list data (common for sequential/concurrent workflows).""" - from agent_framework import ChatMessage + from agent_framework import Message from agent_framework._workflows._events import WorkflowEvent - # Sequential/Concurrent workflows often output list[ChatMessage] + # Sequential/Concurrent workflows often output list[Message] messages = [ - ChatMessage(role="user", contents=[Content.from_text(text="Hello")]), - ChatMessage(role="assistant", contents=[Content.from_text(text="World")]), + Message(role="user", contents=[Content.from_text(text="Hello")]), + Message(role="assistant", contents=[Content.from_text(text="World")]), ] event = WorkflowEvent.output(executor_id="complete", data=messages) events = await mapper.convert_event(event, test_request) diff --git a/python/packages/devui/tests/devui/test_multimodal_workflow.py b/python/packages/devui/tests/devui/test_multimodal_workflow.py index 1124c9afce..7af7f3f308 100644 --- a/python/packages/devui/tests/devui/test_multimodal_workflow.py +++ b/python/packages/devui/tests/devui/test_multimodal_workflow.py @@ -48,8 +48,8 @@ class TestMultimodalWorkflowInput: assert executor._is_openai_multimodal_format([{"foo": "bar"}]) is False # no type field def test_convert_openai_input_to_chat_message_with_image(self): - """Test that OpenAI format with image is converted to ChatMessage with DataContent.""" - from agent_framework import ChatMessage + """Test that OpenAI format with image is converted to Message with DataContent.""" + from agent_framework import Message discovery = MagicMock(spec=EntityDiscovery) mapper = MagicMock(spec=MessageMapper) @@ -67,11 +67,11 @@ class TestMultimodalWorkflowInput: } ] - # Convert to ChatMessage + # Convert to Message result = executor._convert_input_to_chat_message(openai_input) - # Verify result is ChatMessage - assert isinstance(result, ChatMessage), f"Expected ChatMessage, got {type(result)}" + # Verify result is Message + assert isinstance(result, Message), f"Expected Message, got {type(result)}" assert result.role == "user" # Verify contents @@ -89,7 +89,7 @@ class TestMultimodalWorkflowInput: async def test_parse_workflow_input_handles_json_string_with_multimodal(self): """Test that _parse_workflow_input correctly handles JSON string with multimodal content.""" - from agent_framework import ChatMessage + from agent_framework import Message discovery = MagicMock(spec=EntityDiscovery) mapper = MagicMock(spec=MessageMapper) @@ -114,8 +114,8 @@ class TestMultimodalWorkflowInput: # Parse the input result = await executor._parse_workflow_input(mock_workflow, json_string_input) - # Verify result is ChatMessage with multimodal content - assert isinstance(result, ChatMessage), f"Expected ChatMessage, got {type(result)}" + # Verify result is Message with multimodal content + assert isinstance(result, Message), f"Expected Message, got {type(result)}" assert len(result.contents) == 2 # Verify text content @@ -129,7 +129,7 @@ class TestMultimodalWorkflowInput: async def test_parse_workflow_input_still_handles_simple_dict(self): """Test that simple dict input still works (backward compatibility).""" - from agent_framework import ChatMessage + from agent_framework import Message discovery = MagicMock(spec=EntityDiscovery) mapper = MagicMock(spec=MessageMapper) @@ -139,14 +139,14 @@ class TestMultimodalWorkflowInput: simple_input = {"text": "Hello world", "role": "user"} json_string_input = json.dumps(simple_input) - # Mock workflow with ChatMessage input type + # Mock workflow with Message input type mock_workflow = MagicMock() mock_executor = MagicMock() - mock_executor.input_types = [ChatMessage] + mock_executor.input_types = [Message] mock_workflow.get_start_executor.return_value = mock_executor # Parse the input result = await executor._parse_workflow_input(mock_workflow, json_string_input) - # Result should be ChatMessage (from _parse_structured_workflow_input) - assert isinstance(result, ChatMessage), f"Expected ChatMessage, got {type(result)}" + # Result should be Message (from _parse_structured_workflow_input) + assert isinstance(result, Message), f"Expected Message, got {type(result)}" diff --git a/python/packages/devui/tests/devui/test_schema_generation.py b/python/packages/devui/tests/devui/test_schema_generation.py index ddc8b401a6..a5e6c47ba6 100644 --- a/python/packages/devui/tests/devui/test_schema_generation.py +++ b/python/packages/devui/tests/devui/test_schema_generation.py @@ -67,16 +67,16 @@ def test_dataclass_schema_generation(): def test_chat_message_schema_generation(): - """Test schema generation for ChatMessage (SerializationMixin).""" + """Test schema generation for Message (SerializationMixin).""" try: - from agent_framework import ChatMessage + from agent_framework import Message - schema = generate_input_schema(ChatMessage) + schema = generate_input_schema(Message) assert schema is not None assert isinstance(schema, dict) except ImportError: - pytest.skip("ChatMessage not available - agent_framework not installed") + pytest.skip("Message not available - agent_framework not installed") def test_pydantic_model_schema_generation(): diff --git a/python/packages/devui/tests/devui/test_server.py b/python/packages/devui/tests/devui/test_server.py index 1489142914..b6215ddab5 100644 --- a/python/packages/devui/tests/devui/test_server.py +++ b/python/packages/devui/tests/devui/test_server.py @@ -142,7 +142,7 @@ async def test_credential_cleanup() -> None: """Test that async credentials are properly closed during server cleanup.""" from unittest.mock import AsyncMock, Mock - from agent_framework import ChatAgent + from agent_framework import Agent # Create mock credential with async close mock_credential = AsyncMock() @@ -155,7 +155,7 @@ async def test_credential_cleanup() -> None: mock_client.function_invocation_configuration = None # Create agent with mock client - agent = ChatAgent(name="TestAgent", chat_client=mock_client, instructions="Test agent") + agent = Agent(name="TestAgent", client=mock_client, instructions="Test agent") # Create DevUI server with agent server = DevServer() @@ -175,7 +175,7 @@ async def test_credential_cleanup_error_handling() -> None: """Test that credential cleanup errors are handled gracefully.""" from unittest.mock import AsyncMock, Mock - from agent_framework import ChatAgent + from agent_framework import Agent # Create mock credential that raises error on close mock_credential = AsyncMock() @@ -188,7 +188,7 @@ async def test_credential_cleanup_error_handling() -> None: mock_client.function_invocation_configuration = None # Create agent with mock client - agent = ChatAgent(name="TestAgent", chat_client=mock_client, instructions="Test agent") + agent = Agent(name="TestAgent", client=mock_client, instructions="Test agent") # Create DevUI server with agent server = DevServer() @@ -207,7 +207,7 @@ async def test_multiple_credential_attributes() -> None: """Test that we check all common credential attribute names.""" from unittest.mock import AsyncMock, Mock - from agent_framework import ChatAgent + from agent_framework import Agent # Create mock credentials mock_cred1 = Mock() @@ -223,7 +223,7 @@ async def test_multiple_credential_attributes() -> None: mock_client.function_invocation_configuration = None # Create agent with mock client - agent = ChatAgent(name="TestAgent", chat_client=mock_client, instructions="Test agent") + agent = Agent(name="TestAgent", client=mock_client, instructions="Test agent") # Create DevUI server with agent server = DevServer() diff --git a/python/packages/durabletask/agent_framework_durabletask/_durable_agent_state.py b/python/packages/durabletask/agent_framework_durabletask/_durable_agent_state.py index c6e6eaad08..4fd59df051 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_durable_agent_state.py +++ b/python/packages/durabletask/agent_framework_durabletask/_durable_agent_state.py @@ -37,8 +37,8 @@ from typing import Any, ClassVar, cast from agent_framework import ( AgentResponse, - ChatMessage, Content, + Message, UsageDetails, get_logger, ) @@ -803,11 +803,11 @@ class DurableAgentStateMessage: ) @staticmethod - def from_chat_message(chat_message: ChatMessage) -> DurableAgentStateMessage: + def from_chat_message(chat_message: Message) -> DurableAgentStateMessage: """Converts an Agent Framework chat message to a durable state message. Args: - chat_message: ChatMessage object with role, contents, and metadata to convert + chat_message: Message object with role, contents, and metadata to convert Returns: DurableAgentStateMessage with converted content items and metadata @@ -824,15 +824,15 @@ class DurableAgentStateMessage: ) def to_chat_message(self) -> Any: - """Converts this DurableAgentStateMessage back to an agent framework ChatMessage. + """Converts this DurableAgentStateMessage back to an agent framework Message. Returns: - ChatMessage object with role, contents, and metadata converted back to agent framework types + Message object with role, contents, and metadata converted back to agent framework types """ # Convert DurableAgentStateContent objects back to agent_framework content objects ai_contents = [c.to_ai_content() for c in self.contents] - # Build kwargs for ChatMessage + # Build kwargs for Message kwargs: dict[str, Any] = { "role": self.role, "contents": ai_contents, @@ -844,7 +844,7 @@ class DurableAgentStateMessage: if self.extension_data is not None: kwargs["additional_properties"] = self.extension_data - return ChatMessage(**kwargs) + return Message(**kwargs) class DurableAgentStateDataContent(DurableAgentStateContent): diff --git a/python/packages/durabletask/agent_framework_durabletask/_entities.py b/python/packages/durabletask/agent_framework_durabletask/_entities.py index c39359dc72..186561e3f4 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_entities.py +++ b/python/packages/durabletask/agent_framework_durabletask/_entities.py @@ -11,8 +11,8 @@ from typing import Any, cast from agent_framework import ( AgentResponse, AgentResponseUpdate, - ChatMessage, Content, + Message, ResponseStream, SupportsAgentRun, get_logger, @@ -150,7 +150,7 @@ class AgentEntity: self.state.data.conversation_history.append(state_request) try: - chat_messages: list[ChatMessage] = [ + chat_messages: list[Message] = [ m.to_chat_message() for entry in self.state.data.conversation_history if not self._is_error_response(entry) @@ -175,7 +175,7 @@ class AgentEntity: except Exception as exc: logger.exception("[AgentEntity.run] Agent execution failed.") - error_message = ChatMessage( + error_message = Message( role="assistant", contents=[Content.from_error(message=str(exc), error_code=type(exc).__name__)] ) error_response = AgentResponse( diff --git a/python/packages/durabletask/agent_framework_durabletask/_executors.py b/python/packages/durabletask/agent_framework_durabletask/_executors.py index 226d9dff6c..2193f94e16 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_executors.py +++ b/python/packages/durabletask/agent_framework_durabletask/_executors.py @@ -16,7 +16,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timezone from typing import Any, Generic, TypeVar -from agent_framework import AgentResponse, AgentThread, ChatMessage, Content, get_logger +from agent_framework import AgentResponse, AgentThread, Content, Message, get_logger from durabletask.client import TaskHubGrpcClient from durabletask.entities import EntityInstanceId from durabletask.task import CompletableTask, CompositeTask, OrchestrationContext, Task @@ -179,7 +179,7 @@ class DurableAgentExecutor(ABC, Generic[TaskT]): Returns: AgentResponse: Acceptance response with correlation ID """ - acceptance_message = ChatMessage( + acceptance_message = Message( role="system", contents=[ Content.from_text( @@ -360,7 +360,7 @@ class ClientAgentExecutor(DurableAgentExecutor[AgentResponse]): "[ClientAgentExecutor] Error converting response for correlation: %s", correlation_id, ) - error_message = ChatMessage( + error_message = Message( role="system", contents=[ Content.from_error( @@ -375,7 +375,7 @@ class ClientAgentExecutor(DurableAgentExecutor[AgentResponse]): self.max_poll_retries, correlation_id, ) - error_message = ChatMessage( + error_message = Message( role="system", contents=[ Content.from_error( diff --git a/python/packages/durabletask/agent_framework_durabletask/_shim.py b/python/packages/durabletask/agent_framework_durabletask/_shim.py index 00f606ffe4..19ea8a496f 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_shim.py +++ b/python/packages/durabletask/agent_framework_durabletask/_shim.py @@ -12,7 +12,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from typing import Any, Generic, Literal, TypeVar -from agent_framework import AgentThread, ChatMessage, SupportsAgentRun +from agent_framework import AgentThread, Message, SupportsAgentRun from ._executors import DurableAgentExecutor from ._models import DurableAgentThread @@ -86,7 +86,7 @@ class DurableAIAgent(SupportsAgentRun, Generic[TaskT]): def run( # type: ignore[override] self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: Literal[False] = False, thread: AgentThread | None = None, @@ -136,7 +136,7 @@ class DurableAIAgent(SupportsAgentRun, Generic[TaskT]): """Create a new agent thread via the provider.""" return self._executor.get_new_thread(self.name, **kwargs) - def _normalize_messages(self, messages: str | ChatMessage | list[str] | list[ChatMessage] | None) -> str: + def _normalize_messages(self, messages: str | Message | list[str] | list[Message] | None) -> str: """Convert supported message inputs to a single string. Args: @@ -149,7 +149,7 @@ class DurableAIAgent(SupportsAgentRun, Generic[TaskT]): return "" if isinstance(messages, str): return messages - if isinstance(messages, ChatMessage): + if isinstance(messages, Message): return messages.text or "" if isinstance(messages, list): if not messages: @@ -157,6 +157,6 @@ class DurableAIAgent(SupportsAgentRun, Generic[TaskT]): first_item = messages[0] if isinstance(first_item, str): return "\n".join(messages) # type: ignore[arg-type] - # List of ChatMessage + # List of Message return "\n".join([msg.text or "" for msg in messages]) # type: ignore[union-attr] return "" diff --git a/python/packages/durabletask/agent_framework_durabletask/_worker.py b/python/packages/durabletask/agent_framework_durabletask/_worker.py index ce6dc9d70e..636dadff2a 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_worker.py +++ b/python/packages/durabletask/agent_framework_durabletask/_worker.py @@ -29,7 +29,7 @@ class DurableAIAgentWorker: Example: ```python from durabletask import TaskHubGrpcWorker - from agent_framework import ChatAgent + from agent_framework import Agent from agent_framework.azure import DurableAIAgentWorker # Create the underlying worker @@ -39,7 +39,7 @@ class DurableAIAgentWorker: agent_worker = DurableAIAgentWorker(worker) # Register agents - my_agent = ChatAgent(chat_client=client, name="assistant") + my_agent = Agent(client=client, name="assistant") agent_worker.add_agent(my_agent) # Start the worker diff --git a/python/packages/durabletask/pyproject.toml b/python/packages/durabletask/pyproject.toml index ea989cfd24..ca8876b0e7 100644 --- a/python/packages/durabletask/pyproject.toml +++ b/python/packages/durabletask/pyproject.toml @@ -4,7 +4,7 @@ description = "Durable Task integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,7 +22,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "durabletask>=1.3.0", "durabletask-azuremanaged>=1.3.0", "python-dateutil>=2.8.0", diff --git a/python/packages/durabletask/tests/test_durable_entities.py b/python/packages/durabletask/tests/test_durable_entities.py index 03e26784cc..a11e9718ef 100644 --- a/python/packages/durabletask/tests/test_durable_entities.py +++ b/python/packages/durabletask/tests/test_durable_entities.py @@ -11,7 +11,7 @@ from typing import Any, TypeVar from unittest.mock import AsyncMock, Mock import pytest -from agent_framework import AgentResponse, AgentResponseUpdate, ChatMessage, Content, ResponseStream +from agent_framework import AgentResponse, AgentResponseUpdate, Content, Message, ResponseStream from pydantic import BaseModel from agent_framework_durabletask import ( @@ -71,7 +71,7 @@ def _make_entity(agent: Any, callback: Any = None, *, thread_id: str = "test-thr def _role_value(chat_message: DurableAgentStateMessage) -> str: - """Helper to extract the string role from a ChatMessage.""" + """Helper to extract the string role from a Message.""" role = getattr(chat_message, "role", None) role_value = getattr(role, "value", role) if role_value is None: @@ -81,7 +81,7 @@ def _role_value(chat_message: DurableAgentStateMessage) -> str: def _agent_response(text: str | None) -> AgentResponse: """Create an AgentResponse with a single assistant message.""" - message = ChatMessage(role="assistant", text=text) if text is not None else ChatMessage(role="assistant", text="") + message = Message(role="assistant", text=text) if text is not None else Message(role="assistant", text="") return AgentResponse(messages=[message], created_at="2024-01-01T00:00:00Z") diff --git a/python/packages/durabletask/tests/test_shim.py b/python/packages/durabletask/tests/test_shim.py index 6efb027628..9f2fefc406 100644 --- a/python/packages/durabletask/tests/test_shim.py +++ b/python/packages/durabletask/tests/test_shim.py @@ -10,7 +10,7 @@ from typing import Any from unittest.mock import Mock import pytest -from agent_framework import ChatMessage, SupportsAgentRun +from agent_framework import Message, SupportsAgentRun from pydantic import BaseModel from agent_framework_durabletask import DurableAgentThread @@ -76,8 +76,8 @@ class TestDurableAIAgentMessageNormalization: assert kwargs["run_request"].message == "Hello, world!" def test_run_accepts_chat_message(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None: - """Verify run accepts and normalizes ChatMessage objects.""" - chat_msg = ChatMessage(role="user", text="Test message") + """Verify run accepts and normalizes Message objects.""" + chat_msg = Message(role="user", text="Test message") test_agent.run(chat_msg) mock_executor.run_durable_agent.assert_called_once() @@ -93,10 +93,10 @@ class TestDurableAIAgentMessageNormalization: assert kwargs["run_request"].message == "First message\nSecond message" def test_run_accepts_list_of_chat_messages(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None: - """Verify run accepts and joins list of ChatMessage objects.""" + """Verify run accepts and joins list of Message objects.""" messages = [ - ChatMessage(role="user", text="Message 1"), - ChatMessage(role="assistant", text="Message 2"), + Message(role="user", text="Message 1"), + Message(role="assistant", text="Message 2"), ] test_agent.run(messages) diff --git a/python/packages/foundry_local/pyproject.toml b/python/packages/foundry_local/pyproject.toml index 7e94a0691f..9d6a88fa3a 100644 --- a/python/packages/foundry_local/pyproject.toml +++ b/python/packages/foundry_local/pyproject.toml @@ -4,7 +4,7 @@ description = "Foundry Local integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "foundry-local-sdk>=0.5.1,<1", ] diff --git a/python/packages/foundry_local/samples/foundry_local_agent.py b/python/packages/foundry_local/samples/foundry_local_agent.py index 9e81d2b33d..bca1d469d9 100644 --- a/python/packages/foundry_local/samples/foundry_local_agent.py +++ b/python/packages/foundry_local/samples/foundry_local_agent.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Annotated from agent_framework_foundry_local import FoundryLocalClient if TYPE_CHECKING: - from agent_framework import ChatAgent + from agent_framework import Agent """ This sample demonstrates basic usage of the FoundryLocalClient. @@ -33,7 +33,7 @@ def get_weather( return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." -async def non_streaming_example(agent: ChatAgent) -> None: +async def non_streaming_example(agent: Agent) -> None: """Example of non-streaming response (get the complete result at once).""" print("=== Non-streaming Response Example ===") @@ -43,7 +43,7 @@ async def non_streaming_example(agent: ChatAgent) -> None: print(f"Agent: {result}\n") -async def streaming_example(agent: ChatAgent) -> None: +async def streaming_example(agent: Agent) -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") diff --git a/python/packages/foundry_local/tests/test_foundry_local_client.py b/python/packages/foundry_local/tests/test_foundry_local_client.py index 324c94630e..031461d926 100644 --- a/python/packages/foundry_local/tests/test_foundry_local_client.py +++ b/python/packages/foundry_local/tests/test_foundry_local_client.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import pytest -from agent_framework import ChatClientProtocol +from agent_framework import SupportsChatGetResponse from agent_framework.exceptions import ServiceInitializationError from pydantic import ValidationError @@ -55,7 +55,7 @@ def test_foundry_local_client_init(mock_foundry_local_manager: MagicMock) -> Non assert client.model_id == "test-model-id" assert client.manager is mock_foundry_local_manager - assert isinstance(client, ChatClientProtocol) + assert isinstance(client, SupportsChatGetResponse) def test_foundry_local_client_init_with_bootstrap_false(mock_foundry_local_manager: MagicMock) -> None: diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 06fad5d126..38b31f4e2e 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -15,13 +15,13 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, ContextProvider, + Message, ResponseStream, normalize_messages, ) -from agent_framework._tools import FunctionTool, ToolProtocol +from agent_framework._tools import FunctionTool from agent_framework._types import normalize_tools from agent_framework.exceptions import ServiceException, ServiceInitializationError from copilot import CopilotClient, CopilotSession @@ -151,10 +151,10 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): description: str | None = None, context_provider: ContextProvider | None = None, middleware: Sequence[AgentMiddlewareTypes] | None = None, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, default_options: OptionsT | None = None, env_file_path: str | None = None, @@ -173,7 +173,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): description: Description of the GitHubCopilotAgent. context_provider: Context Provider, to be used by the agent. middleware: Agent middleware used by the agent. - tools: Tools to use for the agent. Can be functions, ToolProtocol instances, + tools: Tools to use for the agent. Can be functions or tool definition dicts. These are converted to Copilot SDK tools internally. default_options: Default options for the agent. Can include cli_path, model, timeout, log_level, etc. @@ -278,7 +278,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[False] = False, thread: AgentThread | None = None, @@ -289,7 +289,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): @overload def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: Literal[True], thread: AgentThread | None = None, @@ -299,7 +299,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -341,7 +341,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): async def _run_impl( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, thread: AgentThread | None = None, options: OptionsT | None = None, @@ -366,7 +366,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): except Exception as ex: raise ServiceException(f"GitHub Copilot request failed: {ex}") from ex - response_messages: list[ChatMessage] = [] + response_messages: list[Message] = [] response_id: str | None = None # send_and_wait returns only the final ASSISTANT_MESSAGE event; @@ -376,7 +376,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): if response_event.data.content: response_messages.append( - ChatMessage( + Message( role="assistant", contents=[Content.from_text(response_event.data.content)], message_id=message_id, @@ -389,7 +389,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): async def _stream_updates( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, thread: AgentThread | None = None, options: OptionsT | None = None, @@ -479,7 +479,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): def _prepare_tools( self, - tools: list[ToolProtocol | MutableMapping[str, Any]], + tools: list[FunctionTool | MutableMapping[str, Any]], ) -> list[CopilotTool]: """Convert Agent Framework tools to Copilot SDK tools. @@ -492,14 +492,11 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): copilot_tools: list[CopilotTool] = [] for tool in tools: - if isinstance(tool, ToolProtocol): - match tool: - case FunctionTool(): - copilot_tools.append(self._tool_to_copilot_tool(tool)) # type: ignore - case _: - logger.debug(f"Unsupported tool type: {type(tool)}") + if isinstance(tool, FunctionTool): + copilot_tools.append(self._tool_to_copilot_tool(tool)) # type: ignore elif isinstance(tool, CopilotTool): copilot_tools.append(tool) + # Note: Other tool types (e.g., dict-based hosted tools) are skipped return copilot_tools diff --git a/python/packages/github_copilot/pyproject.toml b/python/packages/github_copilot/pyproject.toml index 57e3c536f8..5981910b22 100644 --- a/python/packages/github_copilot/pyproject.toml +++ b/python/packages/github_copilot/pyproject.toml @@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "github-copilot-sdk>=0.1.0", ] diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index ed302b5bb6..b2b7b2ebee 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -11,8 +11,8 @@ from agent_framework import ( AgentResponse, AgentResponseUpdate, AgentThread, - ChatMessage, Content, + Message, ) from agent_framework.exceptions import ServiceException from copilot.generated.session_events import Data, SessionEvent, SessionEventType @@ -290,11 +290,11 @@ class TestGitHubCopilotAgentRun: mock_session: MagicMock, assistant_message_event: SessionEvent, ) -> None: - """Test run method with ChatMessage.""" + """Test run method with Message.""" mock_session.send_and_wait.return_value = assistant_message_event agent = GitHubCopilotAgent(client=mock_client) - chat_message = ChatMessage(role="user", contents=[Content.from_text("Hello")]) + chat_message = Message(role="user", contents=[Content.from_text("Hello")]) response = await agent.run(chat_message) assert isinstance(response, AgentResponse) diff --git a/python/packages/lab/gaia/samples/azure_ai_agent.py b/python/packages/lab/gaia/samples/azure_ai_agent.py index 3f64e3a684..f83625b2c4 100644 --- a/python/packages/lab/gaia/samples/azure_ai_agent.py +++ b/python/packages/lab/gaia/samples/azure_ai_agent.py @@ -26,13 +26,13 @@ Example: from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from agent_framework import ChatAgent, HostedCodeInterpreterTool, HostedWebSearchTool +from agent_framework import Agent from agent_framework.azure import AzureAIAgentClient from azure.identity.aio import AzureCliCredential @asynccontextmanager -async def create_gaia_agent() -> AsyncIterator[ChatAgent]: +async def create_gaia_agent() -> AsyncIterator[Agent]: """Create an Azure AI agent configured for GAIA benchmark tasks. The agent is configured with: @@ -40,7 +40,7 @@ async def create_gaia_agent() -> AsyncIterator[ChatAgent]: - Code Interpreter tool for calculations and data analysis Yields: - ChatAgent: A configured agent ready to run GAIA tasks. + Agent: A configured agent ready to run GAIA tasks. Example: async with create_gaia_agent() as agent: @@ -54,11 +54,8 @@ async def create_gaia_agent() -> AsyncIterator[ChatAgent]: instructions="Solve tasks to your best ability. Use Bing Search to find " "information and Code Interpreter to perform calculations and data analysis.", tools=[ - HostedWebSearchTool( - name="Bing Grounding Search", - description="Search the web for current information using Bing", - ), - HostedCodeInterpreterTool(), + AzureAIAgentClient.get_web_search_tool(), + AzureAIAgentClient.get_code_interpreter_tool(), ], ) as agent, ): diff --git a/python/packages/lab/gaia/samples/openai_agent.py b/python/packages/lab/gaia/samples/openai_agent.py index 333c8d0931..a5709ecf2a 100644 --- a/python/packages/lab/gaia/samples/openai_agent.py +++ b/python/packages/lab/gaia/samples/openai_agent.py @@ -25,12 +25,12 @@ Example: from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from agent_framework import ChatAgent, HostedCodeInterpreterTool, HostedWebSearchTool +from agent_framework import Agent from agent_framework.openai import OpenAIResponsesClient @asynccontextmanager -async def create_gaia_agent() -> AsyncIterator[ChatAgent]: +async def create_gaia_agent() -> AsyncIterator[Agent]: """Create an OpenAI agent configured for GAIA benchmark tasks. Uses OpenAI Responses API for enhanced capabilities. @@ -40,25 +40,22 @@ async def create_gaia_agent() -> AsyncIterator[ChatAgent]: - Code Interpreter tool for calculations and data analysis Yields: - ChatAgent: A configured agent ready to run GAIA tasks. + Agent: A configured agent ready to run GAIA tasks. Example: async with create_gaia_agent() as agent: result = await agent.run("What is the capital of France?") print(result.text) """ - chat_client = OpenAIResponsesClient() + client = OpenAIResponsesClient() - async with chat_client.as_agent( + async with client.as_agent( name="GaiaAgent", instructions="Solve tasks to your best ability. Use Web Search to find " "information and Code Interpreter to perform calculations and data analysis.", tools=[ - HostedWebSearchTool( - name="Web Search", - description="Search the web for current information", - ), - HostedCodeInterpreterTool(), + OpenAIResponsesClient.get_web_search_tool(), + OpenAIResponsesClient.get_code_interpreter_tool(), ], ) as agent: yield agent diff --git a/python/packages/lab/lightning/README.md b/python/packages/lab/lightning/README.md index 05d3691d74..e9fd3ec91d 100644 --- a/python/packages/lab/lightning/README.md +++ b/python/packages/lab/lightning/README.md @@ -49,8 +49,8 @@ async def math_agent(task: TaskType, llm: LLM) -> float: """A function that solves a math problem and returns the evaluation score.""" async with ( MCPStdioTool(name="calculator", command="uvx", args=["mcp-server-calculator"]) as mcp_server, - ChatAgent( - chat_client=OpenAIChatClient( + Agent( + client=OpenAIChatClient( model_id=llm.model, api_key="your-api-key", base_url=llm.endpoint, diff --git a/python/packages/lab/lightning/samples/train_math_agent.py b/python/packages/lab/lightning/samples/train_math_agent.py index 0cb771e856..f702b5a631 100644 --- a/python/packages/lab/lightning/samples/train_math_agent.py +++ b/python/packages/lab/lightning/samples/train_math_agent.py @@ -20,7 +20,7 @@ import string from typing import TypedDict, cast import sympy # type: ignore[import-untyped,reportMissingImports] -from agent_framework import AgentResponse, ChatAgent, MCPStdioTool +from agent_framework import Agent, AgentResponse, MCPStdioTool from agent_framework.lab.lightning import AgentFrameworkTracer from agent_framework.openai import OpenAIChatClient from agentlightning import LLM, Dataset, Trainer, rollout @@ -166,8 +166,8 @@ async def math_agent(task: MathProblem, llm: LLM) -> float: # MCPStdioTool provides calculator functionality via MCP protocol async with ( MCPStdioTool(name="calculator", command="uvx", args=["mcp-server-calculator"]) as mcp_server, - ChatAgent( - chat_client=OpenAIChatClient( + Agent( + client=OpenAIChatClient( model_id=llm.model, # This is the model being trained api_key=os.getenv("OPENAI_API_KEY") or "dummy", # Can be dummy when connecting to training LLM base_url=llm.endpoint, # vLLM server endpoint provided by agent-lightning diff --git a/python/packages/lab/lightning/tests/test_lightning.py b/python/packages/lab/lightning/tests/test_lightning.py index c528bd8d78..76e6b98506 100644 --- a/python/packages/lab/lightning/tests/test_lightning.py +++ b/python/packages/lab/lightning/tests/test_lightning.py @@ -9,7 +9,7 @@ import pytest agentlightning = pytest.importorskip("agentlightning") -from agent_framework import AgentExecutor, AgentResponse, ChatAgent, WorkflowBuilder, Workflow +from agent_framework import AgentExecutor, AgentResponse, Agent, WorkflowBuilder, Workflow from agent_framework_lab_lightning import AgentFrameworkTracer from agent_framework.openai import OpenAIChatClient from agentlightning import TracerTraceToTriplet @@ -80,14 +80,14 @@ def workflow_two_agents(): ), ): # Create the two agents - analyzer_agent = ChatAgent( - chat_client=first_chat_client, + analyzer_agent = Agent( + client=first_chat_client, name="DataAnalyzer", instructions="You are a data analyst. Analyze the given data and provide insights.", ) - advisor_agent = ChatAgent( - chat_client=second_chat_client, + advisor_agent = Agent( + client=second_chat_client, name="InvestmentAdvisor", instructions="You are an investment advisor. Based on analysis results, provide recommendations.", ) diff --git a/python/packages/lab/pyproject.toml b/python/packages/lab/pyproject.toml index 9431560d59..daf3e4f3ce 100644 --- a/python/packages/lab/pyproject.toml +++ b/python/packages/lab/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,7 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", ] [project.optional-dependencies] diff --git a/python/packages/lab/tau2/README.md b/python/packages/lab/tau2/README.md index a0b587ea3c..083fd05a9d 100644 --- a/python/packages/lab/tau2/README.md +++ b/python/packages/lab/tau2/README.md @@ -138,21 +138,21 @@ export OPENAI_BASE_URL="https://your-custom-endpoint.com/v1" ```python from agent_framework.lab.tau2 import TaskRunner -from agent_framework import ChatAgent +from agent_framework import Agent class CustomTaskRunner(TaskRunner): def assistant_agent(self, assistant_chat_client): # Override to customize the assistant agent - return ChatAgent( - chat_client=assistant_chat_client, + return Agent( + client=assistant_chat_client, instructions="Your custom system prompt here", # Add custom tools, temperature, etc. ) def user_simulator(self, user_chat_client, task): # Override to customize the user simulator - return ChatAgent( - chat_client=user_chat_client, + return Agent( + client=user_chat_client, instructions="Custom user simulator prompt", ) ``` diff --git a/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py b/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py index dccf6e2882..bd8d521e28 100644 --- a/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py +++ b/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py @@ -2,7 +2,7 @@ from typing import Any -from agent_framework._types import ChatMessage, Content +from agent_framework._types import Content, Message from loguru import logger @@ -11,7 +11,7 @@ def _get_role_value(role: Any) -> str: return role.value if hasattr(role, "value") else str(role) -def flip_messages(messages: list[ChatMessage]) -> list[ChatMessage]: +def flip_messages(messages: list[Message]) -> list[Message]: """Flip message roles between assistant and user for role-playing scenarios. Used in agent simulations where the assistant's messages become user inputs @@ -30,7 +30,7 @@ def flip_messages(messages: list[ChatMessage]) -> list[ChatMessage]: # Flip assistant to user contents = filter_out_function_calls(msg.contents) if contents: - flipped_msg = ChatMessage( + flipped_msg = Message( role="user", # The function calls will cause 400 when role is user contents=contents, @@ -40,7 +40,7 @@ def flip_messages(messages: list[ChatMessage]) -> list[ChatMessage]: flipped_messages.append(flipped_msg) elif role_value == "user": # Flip user to assistant - flipped_msg = ChatMessage( + flipped_msg = Message( role="assistant", contents=msg.contents, author_name=msg.author_name, message_id=msg.message_id ) flipped_messages.append(flipped_msg) @@ -53,7 +53,7 @@ def flip_messages(messages: list[ChatMessage]) -> list[ChatMessage]: return flipped_messages -def log_messages(messages: list[ChatMessage]) -> None: +def log_messages(messages: list[Message]) -> None: """Log messages with colored output based on role and content type. Provides visual debugging by color-coding different message roles and diff --git a/python/packages/lab/tau2/agent_framework_lab_tau2/_sliding_window.py b/python/packages/lab/tau2/agent_framework_lab_tau2/_sliding_window.py index 20a3a2fe27..ad4328ff21 100644 --- a/python/packages/lab/tau2/agent_framework_lab_tau2/_sliding_window.py +++ b/python/packages/lab/tau2/agent_framework_lab_tau2/_sliding_window.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from typing import Any import tiktoken -from agent_framework import ChatMessage, ChatMessageStore +from agent_framework import ChatMessageStore, Message from loguru import logger @@ -19,7 +19,7 @@ class SlidingWindowChatMessageStore(ChatMessageStore): def __init__( self, - messages: Sequence[ChatMessage] | None = None, + messages: Sequence[Message] | None = None, max_tokens: int = 3800, system_message: str | None = None, tool_definitions: Any | None = None, @@ -32,17 +32,17 @@ class SlidingWindowChatMessageStore(ChatMessageStore): # An estimation based on a commonly used vocab table self.encoding = tiktoken.get_encoding("o200k_base") - async def add_messages(self, messages: Sequence[ChatMessage]) -> None: + async def add_messages(self, messages: Sequence[Message]) -> None: await super().add_messages(messages) self.truncated_messages = self.messages.copy() self.truncate_messages() - async def list_messages(self) -> list[ChatMessage]: + async def list_messages(self) -> list[Message]: """Get the current list of messages, which may be truncated.""" return self.truncated_messages - async def list_all_messages(self) -> list[ChatMessage]: + async def list_all_messages(self) -> list[Message]: """Get all messages from the store including the truncated ones.""" return self.messages diff --git a/python/packages/lab/tau2/agent_framework_lab_tau2/_tau2_utils.py b/python/packages/lab/tau2/agent_framework_lab_tau2/_tau2_utils.py index 647dd8884a..b785eae6d7 100644 --- a/python/packages/lab/tau2/agent_framework_lab_tau2/_tau2_utils.py +++ b/python/packages/lab/tau2/agent_framework_lab_tau2/_tau2_utils.py @@ -7,17 +7,19 @@ from typing import Any import numpy as np from agent_framework._tools import FunctionTool -from agent_framework._types import ChatMessage +from agent_framework._types import Message from loguru import logger from pydantic import BaseModel from tau2.data_model.message import ( # type: ignore[import-untyped] AssistantMessage, - Message, SystemMessage, ToolCall, ToolMessage, UserMessage, ) +from tau2.data_model.message import ( + Message as Tau2Message, +) from tau2.data_model.tasks import EnvFunctionCall, InitializationData # type: ignore[import-untyped] from tau2.environment.environment import Environment # type: ignore[import-untyped] from tau2.environment.tool import Tool # type: ignore[import-untyped] @@ -45,7 +47,7 @@ def convert_tau2_tool_to_function_tool(tau2_tool: Tool) -> FunctionTool[Any, Any ) -def convert_agent_framework_messages_to_tau2_messages(messages: list[ChatMessage]) -> list[Message]: +def convert_agent_framework_messages_to_tau2_messages(messages: list[Message]) -> list[Tau2Message]: """Convert agent framework ChatMessages to tau2 Message objects. Handles role mapping, text extraction, function calls, and function results. @@ -119,13 +121,13 @@ def patch_env_set_state() -> None: self: Any, initialization_data: InitializationData | None, initialization_actions: list[EnvFunctionCall] | None, - message_history: list[Message], + message_history: list[Tau2Message], ) -> None: if self.solo_mode and any(isinstance(message, UserMessage) for message in message_history): raise ValueError("User messages are not allowed in solo mode") def get_actions_from_messages( - messages: list[Message], + messages: list[Tau2Message], ) -> list[tuple[ToolCall, ToolMessage]]: """Get the actions from the messages.""" messages = deepcopy(messages)[::-1] diff --git a/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py b/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py index 326aaf0748..68205c880e 100644 --- a/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py +++ b/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py @@ -6,14 +6,14 @@ import uuid from typing import cast from agent_framework import ( + Agent, AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, AgentResponse, - ChatAgent, - ChatClientProtocol, - ChatMessage, FunctionExecutor, + Message, + SupportsChatGetResponse, Workflow, WorkflowBuilder, WorkflowContext, @@ -67,10 +67,10 @@ class TaskRunner: # State tracking step_count: int - full_conversation: list[ChatMessage] + full_conversation: list[Message] termination_reason: TerminationReason | None full_reward_info: RewardInfo | None - _final_user_message: list[ChatMessage] | None + _final_user_message: list[Message] | None _assistant_executor: AgentExecutor | None _user_executor: AgentExecutor | None @@ -159,7 +159,7 @@ class TaskRunner: """Check if user wants to stop the conversation.""" return STOP in text or TRANSFER in text or OUT_OF_SCOPE in text - def assistant_agent(self, assistant_chat_client: ChatClientProtocol) -> ChatAgent: + def assistant_agent(self, assistant_chat_client: SupportsChatGetResponse) -> Agent: """Create an assistant agent. Users can override this method to provide a custom assistant agent. @@ -196,8 +196,8 @@ class TaskRunner: # - Access to all domain tools (booking, cancellation, etc.) # - Sliding window memory to handle long conversations within token limits # - Temperature-controlled response generation - return ChatAgent( - chat_client=assistant_chat_client, + return Agent( + client=assistant_chat_client, instructions=assistant_system_prompt, tools=tools, temperature=self.assistant_sampling_temperature, @@ -208,7 +208,7 @@ class TaskRunner: ), ) - def user_simulator(self, user_simuator_chat_client: ChatClientProtocol, task: Task) -> ChatAgent: + def user_simulator(self, user_simuator_chat_client: SupportsChatGetResponse, task: Task) -> Agent: """Create a user simulator agent. Users can override this method to provide a custom user simulator agent. @@ -230,8 +230,8 @@ class TaskRunner: {task.user_scenario.instructions} """ - return ChatAgent( - chat_client=user_simuator_chat_client, + return Agent( + client=user_simuator_chat_client, instructions=user_sim_system_prompt, temperature=0.0, # No sliding window for user simulator to maintain full conversation context @@ -268,7 +268,7 @@ class TaskRunner: target_id=USER_SIMULATOR_ID if is_from_agent else ASSISTANT_AGENT_ID, ) - def build_conversation_workflow(self, assistant_agent: ChatAgent, user_simulator_agent: ChatAgent) -> Workflow: + def build_conversation_workflow(self, assistant_agent: Agent, user_simulator_agent: Agent) -> Workflow: """Build the conversation workflow. Users can override this method to provide a custom conversation workflow. @@ -304,9 +304,9 @@ class TaskRunner: async def run( self, task: Task, - assistant_chat_client: ChatClientProtocol, - user_simulator_chat_client: ChatClientProtocol, - ) -> list[ChatMessage]: + assistant_chat_client: SupportsChatGetResponse, + user_simulator_chat_client: SupportsChatGetResponse, + ) -> list[Message]: """Run a tau2 task using workflow-based agent orchestration. This method orchestrates a complex multi-agent simulation: @@ -323,7 +323,7 @@ class TaskRunner: user_simulator_chat_client: LLM client for the user simulator Returns: - Complete conversation history as ChatMessage list for evaluation + Complete conversation history as Message list for evaluation """ logger.info(f"Starting workflow agent for task {task.id}: {task.description.purpose}") # type: ignore[unused-ignore] logger.info(f"Assistant chat client: {assistant_chat_client}") @@ -340,11 +340,11 @@ class TaskRunner: # Matches tau2's expected conversation start pattern logger.info(f"Starting workflow with hardcoded greeting: '{DEFAULT_FIRST_AGENT_MESSAGE}'") - first_message = ChatMessage(role="assistant", text=DEFAULT_FIRST_AGENT_MESSAGE) + first_message = Message(role="assistant", text=DEFAULT_FIRST_AGENT_MESSAGE) initial_greeting = AgentExecutorResponse( executor_id=ASSISTANT_AGENT_ID, agent_response=AgentResponse(messages=[first_message]), - full_conversation=[ChatMessage(role="assistant", text=DEFAULT_FIRST_AGENT_MESSAGE)], + full_conversation=[Message(role="assistant", text=DEFAULT_FIRST_AGENT_MESSAGE)], ) # STEP 4: Execute the workflow and collect results @@ -371,7 +371,7 @@ class TaskRunner: return full_conversation def evaluate( - self, task_input: Task, conversation: list[ChatMessage], termination_reason: TerminationReason | None + self, task_input: Task, conversation: list[Message], termination_reason: TerminationReason | None ) -> float: """Evaluate agent performance using tau2's comprehensive evaluation system. diff --git a/python/packages/lab/tau2/tests/test_message_utils.py b/python/packages/lab/tau2/tests/test_message_utils.py index 7bee8bc9be..8908140f94 100644 --- a/python/packages/lab/tau2/tests/test_message_utils.py +++ b/python/packages/lab/tau2/tests/test_message_utils.py @@ -2,14 +2,14 @@ from unittest.mock import patch -from agent_framework._types import ChatMessage, Content +from agent_framework._types import Content, Message from agent_framework_lab_tau2._message_utils import flip_messages, log_messages def test_flip_messages_user_to_assistant(): """Test flipping user message to assistant.""" messages = [ - ChatMessage( + Message( role="user", contents=[Content.from_text(text="Hello assistant")], author_name="User1", @@ -29,7 +29,7 @@ def test_flip_messages_user_to_assistant(): def test_flip_messages_assistant_to_user(): """Test flipping assistant message to user.""" messages = [ - ChatMessage( + Message( role="assistant", contents=[Content.from_text(text="Hello user")], author_name="Assistant1", @@ -51,7 +51,7 @@ def test_flip_messages_assistant_with_function_calls_filtered(): function_call = Content.from_function_call(call_id="call_123", name="test_function", arguments={"param": "value"}) messages = [ - ChatMessage( + Message( role="assistant", contents=[ Content.from_text(text="I'll call a function"), @@ -78,7 +78,7 @@ def test_flip_messages_assistant_with_only_function_calls_skipped(): function_call = Content.from_function_call(call_id="call_456", name="another_function", arguments={"key": "value"}) messages = [ - ChatMessage(role="assistant", contents=[function_call], message_id="msg_004") # Only function call, no text + Message(role="assistant", contents=[function_call], message_id="msg_004") # Only function call, no text ] flipped = flip_messages(messages) @@ -91,7 +91,7 @@ def test_flip_messages_tool_messages_skipped(): """Test that tool messages are skipped.""" function_result = Content.from_function_result(call_id="call_789", result={"success": True}) - messages = [ChatMessage(role="tool", contents=[function_result])] + messages = [Message(role="tool", contents=[function_result])] flipped = flip_messages(messages) @@ -101,9 +101,7 @@ def test_flip_messages_tool_messages_skipped(): def test_flip_messages_system_messages_preserved(): """Test that system messages are preserved as-is.""" - messages = [ - ChatMessage(role="system", contents=[Content.from_text(text="System instruction")], message_id="sys_001") - ] + messages = [Message(role="system", contents=[Content.from_text(text="System instruction")], message_id="sys_001")] flipped = flip_messages(messages) @@ -120,11 +118,11 @@ def test_flip_messages_mixed_conversation(): function_result = Content.from_function_result(call_id="call_mixed", result="function result") messages = [ - ChatMessage(role="system", contents=[Content.from_text(text="System prompt")]), - ChatMessage(role="user", contents=[Content.from_text(text="User question")]), - ChatMessage(role="assistant", contents=[Content.from_text(text="Assistant response"), function_call]), - ChatMessage(role="tool", contents=[function_result]), - ChatMessage(role="assistant", contents=[Content.from_text(text="Final response")]), + Message(role="system", contents=[Content.from_text(text="System prompt")]), + Message(role="user", contents=[Content.from_text(text="User question")]), + Message(role="assistant", contents=[Content.from_text(text="Assistant response"), function_call]), + Message(role="tool", contents=[function_result]), + Message(role="assistant", contents=[Content.from_text(text="Final response")]), ] flipped = flip_messages(messages) @@ -159,7 +157,7 @@ def test_flip_messages_empty_list(): def test_flip_messages_preserves_metadata(): """Test that message metadata is preserved during flipping.""" messages = [ - ChatMessage( + Message( role="user", contents=[Content.from_text(text="Test message")], author_name="TestUser", @@ -178,8 +176,8 @@ def test_flip_messages_preserves_metadata(): def test_log_messages_text_content(mock_logger): """Test logging messages with text content.""" messages = [ - ChatMessage(role="user", contents=[Content.from_text(text="Hello")]), - ChatMessage(role="assistant", contents=[Content.from_text(text="Hi there!")]), + Message(role="user", contents=[Content.from_text(text="Hello")]), + Message(role="assistant", contents=[Content.from_text(text="Hi there!")]), ] log_messages(messages) @@ -193,7 +191,7 @@ def test_log_messages_function_call(mock_logger): """Test logging messages with function calls.""" function_call = Content.from_function_call(call_id="call_log", name="log_function", arguments={"param": "value"}) - messages = [ChatMessage(role="assistant", contents=[function_call])] + messages = [Message(role="assistant", contents=[function_call])] log_messages(messages) @@ -209,7 +207,7 @@ def test_log_messages_function_result(mock_logger): """Test logging messages with function results.""" function_result = Content.from_function_result(call_id="call_result", result="success") - messages = [ChatMessage(role="tool", contents=[function_result])] + messages = [Message(role="tool", contents=[function_result])] log_messages(messages) @@ -223,10 +221,10 @@ def test_log_messages_function_result(mock_logger): def test_log_messages_different_roles(mock_logger): """Test logging messages with different roles get different colors.""" messages = [ - ChatMessage(role="system", contents=[Content.from_text(text="System")]), - ChatMessage(role="user", contents=[Content.from_text(text="User")]), - ChatMessage(role="assistant", contents=[Content.from_text(text="Assistant")]), - ChatMessage(role="tool", contents=[Content.from_text(text="Tool")]), + Message(role="system", contents=[Content.from_text(text="System")]), + Message(role="user", contents=[Content.from_text(text="User")]), + Message(role="assistant", contents=[Content.from_text(text="Assistant")]), + Message(role="tool", contents=[Content.from_text(text="Tool")]), ] log_messages(messages) @@ -250,7 +248,7 @@ def test_log_messages_different_roles(mock_logger): @patch("agent_framework_lab_tau2._message_utils.logger") def test_log_messages_escapes_html(mock_logger): """Test that HTML-like characters are properly escaped in log output.""" - messages = [ChatMessage(role="user", contents=[Content.from_text(text="Message with content")])] + messages = [Message(role="user", contents=[Content.from_text(text="Message with content")])] log_messages(messages) @@ -266,7 +264,7 @@ def test_log_messages_mixed_content_types(mock_logger): function_call = Content.from_function_call(call_id="mixed_call", name="mixed_function", arguments={"key": "value"}) messages = [ - ChatMessage( + Message( role="assistant", contents=[Content.from_text(text="I'll call a function"), function_call, Content.from_text(text="Done!")], ) diff --git a/python/packages/lab/tau2/tests/test_sliding_window.py b/python/packages/lab/tau2/tests/test_sliding_window.py index 706bbf75c9..c991f5b568 100644 --- a/python/packages/lab/tau2/tests/test_sliding_window.py +++ b/python/packages/lab/tau2/tests/test_sliding_window.py @@ -4,7 +4,7 @@ from unittest.mock import patch -from agent_framework._types import ChatMessage, Content +from agent_framework._types import Content, Message from agent_framework_lab_tau2._sliding_window import SlidingWindowChatMessageStore @@ -36,8 +36,8 @@ def test_initialization_with_parameters(): def test_initialization_with_messages(): """Test initializing with existing messages.""" messages = [ - ChatMessage(role="user", contents=[Content.from_text(text="Hello")]), - ChatMessage(role="assistant", contents=[Content.from_text(text="Hi there!")]), + Message(role="user", contents=[Content.from_text(text="Hello")]), + Message(role="assistant", contents=[Content.from_text(text="Hi there!")]), ] sliding_window = SlidingWindowChatMessageStore(messages=messages, max_tokens=1000) @@ -51,8 +51,8 @@ async def test_add_messages_simple(): sliding_window = SlidingWindowChatMessageStore(max_tokens=10000) # Large limit new_messages = [ - ChatMessage(role="user", contents=[Content.from_text(text="What's the weather?")]), - ChatMessage(role="assistant", contents=[Content.from_text(text="I can help with that.")]), + Message(role="user", contents=[Content.from_text(text="What's the weather?")]), + Message(role="assistant", contents=[Content.from_text(text="I can help with that.")]), ] await sliding_window.add_messages(new_messages) @@ -69,7 +69,7 @@ async def test_list_all_messages_vs_list_messages(): # Add many messages to trigger truncation messages = [ - ChatMessage(role="user", contents=[Content.from_text(text=f"Message {i} with some content")]) for i in range(10) + Message(role="user", contents=[Content.from_text(text=f"Message {i} with some content")]) for i in range(10) ] await sliding_window.add_messages(messages) @@ -87,7 +87,7 @@ async def test_list_all_messages_vs_list_messages(): def test_get_token_count_basic(): """Test basic token counting.""" sliding_window = SlidingWindowChatMessageStore(max_tokens=1000) - sliding_window.truncated_messages = [ChatMessage(role="user", contents=[Content.from_text(text="Hello")])] + sliding_window.truncated_messages = [Message(role="user", contents=[Content.from_text(text="Hello")])] token_count = sliding_window.get_token_count() @@ -104,7 +104,7 @@ def test_get_token_count_with_system_message(): token_count_empty = sliding_window.get_token_count() # Add a message - sliding_window.truncated_messages = [ChatMessage(role="user", contents=[Content.from_text(text="Hello")])] + sliding_window.truncated_messages = [Message(role="user", contents=[Content.from_text(text="Hello")])] token_count_with_message = sliding_window.get_token_count() # With message should be more tokens @@ -117,7 +117,7 @@ def test_get_token_count_function_call(): function_call = Content.from_function_call(call_id="call_123", name="test_function", arguments={"param": "value"}) sliding_window = SlidingWindowChatMessageStore(max_tokens=1000) - sliding_window.truncated_messages = [ChatMessage(role="assistant", contents=[function_call])] + sliding_window.truncated_messages = [Message(role="assistant", contents=[function_call])] token_count = sliding_window.get_token_count() assert token_count > 0 @@ -128,7 +128,7 @@ def test_get_token_count_function_result(): function_result = Content.from_function_result(call_id="call_123", result={"success": True, "data": "result"}) sliding_window = SlidingWindowChatMessageStore(max_tokens=1000) - sliding_window.truncated_messages = [ChatMessage(role="tool", contents=[function_result])] + sliding_window.truncated_messages = [Message(role="tool", contents=[function_result])] token_count = sliding_window.get_token_count() assert token_count > 0 @@ -141,17 +141,17 @@ def test_truncate_messages_removes_old_messages(mock_logger): # Create messages that will exceed the limit messages = [ - ChatMessage( + Message( role="user", contents=[Content.from_text(text="This is a very long message that should exceed the token limit")], ), - ChatMessage( + Message( role="assistant", contents=[ Content.from_text(text="This is another very long message that should also exceed the token limit") ], ), - ChatMessage(role="user", contents=[Content.from_text(text="Short msg")]), + Message(role="user", contents=[Content.from_text(text="Short msg")]), ] sliding_window.truncated_messages = messages.copy() @@ -170,10 +170,8 @@ def test_truncate_messages_removes_leading_tool_messages(mock_logger): sliding_window = SlidingWindowChatMessageStore(max_tokens=10000) # Large limit # Create messages starting with tool message - tool_message = ChatMessage( - role="tool", contents=[Content.from_function_result(call_id="call_123", result="result")] - ) - user_message = ChatMessage(role="user", contents=[Content.from_text(text="Hello")]) + tool_message = Message(role="tool", contents=[Content.from_function_result(call_id="call_123", result="result")]) + user_message = Message(role="user", contents=[Content.from_text(text="Hello")]) sliding_window.truncated_messages = [tool_message, user_message] sliding_window.truncate_messages() @@ -231,13 +229,13 @@ async def test_real_world_scenario(): # Simulate a conversation conversation = [ - ChatMessage(role="user", contents=[Content.from_text(text="Hello, how are you?")]), - ChatMessage( + Message(role="user", contents=[Content.from_text(text="Hello, how are you?")]), + Message( role="assistant", contents=[Content.from_text(text="I'm doing well, thank you! How can I help you today?")], ), - ChatMessage(role="user", contents=[Content.from_text(text="Can you tell me about the weather?")]), - ChatMessage( + Message(role="user", contents=[Content.from_text(text="Can you tell me about the weather?")]), + Message( role="assistant", contents=[ Content.from_text( @@ -246,8 +244,8 @@ async def test_real_world_scenario(): ) ], ), - ChatMessage(role="user", contents=[Content.from_text(text="What about telling me a joke instead?")]), - ChatMessage( + Message(role="user", contents=[Content.from_text(text="What about telling me a joke instead?")]), + Message( role="assistant", contents=[ Content.from_text(text="Sure! Why don't scientists trust atoms? Because they make up everything!") diff --git a/python/packages/lab/tau2/tests/test_tau2_utils.py b/python/packages/lab/tau2/tests/test_tau2_utils.py index dff8a56e5c..f463c13ec8 100644 --- a/python/packages/lab/tau2/tests/test_tau2_utils.py +++ b/python/packages/lab/tau2/tests/test_tau2_utils.py @@ -6,7 +6,7 @@ import urllib.request from pathlib import Path import pytest -from agent_framework import ChatMessage, Content, FunctionTool +from agent_framework import Content, FunctionTool, Message from agent_framework_lab_tau2._tau2_utils import ( convert_agent_framework_messages_to_tau2_messages, convert_tau2_tool_to_function_tool, @@ -91,7 +91,7 @@ def test_convert_tau2_tool_to_function_tool_multiple_tools(tau2_airline_environm def test_convert_agent_framework_messages_to_tau2_messages_system(): """Test converting system message.""" - messages = [ChatMessage(role="system", contents=[Content.from_text(text="System instruction")])] + messages = [Message(role="system", contents=[Content.from_text(text="System instruction")])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -103,7 +103,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_system(): def test_convert_agent_framework_messages_to_tau2_messages_user(): """Test converting user message.""" - messages = [ChatMessage(role="user", contents=[Content.from_text(text="Hello assistant")])] + messages = [Message(role="user", contents=[Content.from_text(text="Hello assistant")])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -116,7 +116,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_user(): def test_convert_agent_framework_messages_to_tau2_messages_assistant(): """Test converting assistant message.""" - messages = [ChatMessage(role="assistant", contents=[Content.from_text(text="Hello user")])] + messages = [Message(role="assistant", contents=[Content.from_text(text="Hello user")])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -131,7 +131,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_with_function_call(): """Test converting message with function call.""" function_call = Content.from_function_call(call_id="call_123", name="test_function", arguments={"param": "value"}) - messages = [ChatMessage(role="assistant", contents=[Content.from_text(text="I'll call a function"), function_call])] + messages = [Message(role="assistant", contents=[Content.from_text(text="I'll call a function"), function_call])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -153,7 +153,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_with_function_result( """Test converting message with function result.""" function_result = Content.from_function_result(call_id="call_123", result={"success": True, "data": "result data"}) - messages = [ChatMessage(role="tool", contents=[function_result])] + messages = [Message(role="tool", contents=[function_result])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -173,7 +173,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_with_error(): call_id="call_456", result="Error occurred", exception=Exception("Test error") ) - messages = [ChatMessage(role="tool", contents=[function_result])] + messages = [Message(role="tool", contents=[function_result])] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -185,7 +185,7 @@ def test_convert_agent_framework_messages_to_tau2_messages_with_error(): def test_convert_agent_framework_messages_to_tau2_messages_multiple_text_contents(): """Test converting message with multiple text contents.""" messages = [ - ChatMessage(role="user", contents=[Content.from_text(text="First part"), Content.from_text(text="Second part")]) + Message(role="user", contents=[Content.from_text(text="First part"), Content.from_text(text="Second part")]) ] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) @@ -202,11 +202,11 @@ def test_convert_agent_framework_messages_to_tau2_messages_complex_scenario(): function_result = Content.from_function_result(call_id="call_789", result={"output": "tool result"}) messages = [ - ChatMessage(role="system", contents=[Content.from_text(text="System prompt")]), - ChatMessage(role="user", contents=[Content.from_text(text="User request")]), - ChatMessage(role="assistant", contents=[Content.from_text(text="I'll help you"), function_call]), - ChatMessage(role="tool", contents=[function_result]), - ChatMessage(role="assistant", contents=[Content.from_text(text="Based on the result...")]), + Message(role="system", contents=[Content.from_text(text="System prompt")]), + Message(role="user", contents=[Content.from_text(text="User request")]), + Message(role="assistant", contents=[Content.from_text(text="I'll help you"), function_call]), + Message(role="tool", contents=[function_result]), + Message(role="assistant", contents=[Content.from_text(text="Based on the result...")]), ] tau2_messages = convert_agent_framework_messages_to_tau2_messages(messages) diff --git a/python/packages/mem0/AGENTS.md b/python/packages/mem0/AGENTS.md index 7c4ebaba2a..3a17e7b137 100644 --- a/python/packages/mem0/AGENTS.md +++ b/python/packages/mem0/AGENTS.md @@ -12,7 +12,7 @@ Integration with Mem0 for agent memory management. from agent_framework.mem0 import Mem0Provider provider = Mem0Provider(api_key="your-key") -agent = ChatAgent(..., context_provider=provider) +agent = Agent(..., context_provider=provider) ``` ## Import Path diff --git a/python/packages/mem0/agent_framework_mem0/__init__.py b/python/packages/mem0/agent_framework_mem0/__init__.py index 7ff88aaa42..b43f1dba2c 100644 --- a/python/packages/mem0/agent_framework_mem0/__init__.py +++ b/python/packages/mem0/agent_framework_mem0/__init__.py @@ -8,6 +8,7 @@ import os if os.environ.get("MEM0_TELEMETRY") is None: os.environ["MEM0_TELEMETRY"] = "false" +from ._context_provider import _Mem0ContextProvider from ._provider import Mem0Provider try: @@ -17,5 +18,6 @@ except importlib.metadata.PackageNotFoundError: __all__ = [ "Mem0Provider", + "_Mem0ContextProvider", "__version__", ] diff --git a/python/packages/mem0/agent_framework_mem0/_context_provider.py b/python/packages/mem0/agent_framework_mem0/_context_provider.py new file mode 100644 index 0000000000..6a09887b72 --- /dev/null +++ b/python/packages/mem0/agent_framework_mem0/_context_provider.py @@ -0,0 +1,193 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""New-pattern Mem0 context provider using BaseContextProvider. + +This module provides ``_Mem0ContextProvider``, a side-by-side implementation of +:class:`Mem0Provider` built on the new :class:`BaseContextProvider` hooks pattern. +It will be renamed to ``Mem0ContextProvider`` in PR2 when the old class is removed. +""" + +from __future__ import annotations + +import sys +from contextlib import AbstractAsyncContextManager +from typing import TYPE_CHECKING, Any + +from agent_framework import Message +from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext +from agent_framework.exceptions import ServiceInitializationError +from mem0 import AsyncMemory, AsyncMemoryClient + +if sys.version_info >= (3, 11): + from typing import NotRequired, Self, TypedDict # pragma: no cover +else: + from typing_extensions import NotRequired, Self, TypedDict # pragma: no cover + +if TYPE_CHECKING: + from agent_framework._agents import SupportsAgentRun + + +class _MemorySearchResponse_v1_1(TypedDict): + results: list[dict[str, Any]] + relations: NotRequired[list[dict[str, Any]]] + + +_MemorySearchResponse_v2 = list[dict[str, Any]] + + +class _Mem0ContextProvider(BaseContextProvider): + """Mem0 context provider using the new BaseContextProvider hooks pattern. + + Integrates Mem0 for persistent semantic memory, searching and storing + memories via the Mem0 API. This is the new-pattern equivalent of + :class:`Mem0Provider`. + + Note: + This class uses a temporary ``_`` prefix to coexist with the existing + :class:`Mem0Provider`. It will be renamed to ``Mem0ContextProvider`` + in PR2. + """ + + DEFAULT_CONTEXT_PROMPT = "## Memories\nConsider the following memories when answering user questions:" + + def __init__( + self, + source_id: str, + mem0_client: AsyncMemory | AsyncMemoryClient | None = None, + api_key: str | None = None, + application_id: str | None = None, + agent_id: str | None = None, + user_id: str | None = None, + *, + context_prompt: str | None = None, + ) -> None: + """Initialize the Mem0 context provider. + + Args: + source_id: Unique identifier for this provider instance. + mem0_client: A pre-created Mem0 MemoryClient or None to create a default client. + api_key: The API key for authenticating with the Mem0 API. + application_id: The application ID for scoping memories. + agent_id: The agent ID for scoping memories. + user_id: The user ID for scoping memories. + context_prompt: The prompt to prepend to retrieved memories. + """ + super().__init__(source_id) + should_close_client = False + if mem0_client is None: + mem0_client = AsyncMemoryClient(api_key=api_key) + should_close_client = True + + self.api_key = api_key + self.application_id = application_id + self.agent_id = agent_id + self.user_id = user_id + self.context_prompt = context_prompt or self.DEFAULT_CONTEXT_PROMPT + self.mem0_client = mem0_client + self._should_close_client = should_close_client + + async def __aenter__(self) -> Self: + """Async context manager entry.""" + if self.mem0_client and isinstance(self.mem0_client, AbstractAsyncContextManager): + await self.mem0_client.__aenter__() + return self + + async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None: + """Async context manager exit.""" + if self._should_close_client and self.mem0_client and isinstance(self.mem0_client, AbstractAsyncContextManager): + await self.mem0_client.__aexit__(exc_type, exc_val, exc_tb) + + # -- Hooks pattern --------------------------------------------------------- + + async def before_run( + self, + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Search Mem0 for relevant memories and add to the session context.""" + self._validate_filters() + input_text = "\n".join(msg.text for msg in context.input_messages if msg and msg.text and msg.text.strip()) + if not input_text.strip(): + return + + filters = self._build_filters(session_id=context.session_id) + + search_response: _MemorySearchResponse_v1_1 | _MemorySearchResponse_v2 = await self.mem0_client.search( # type: ignore[misc] + query=input_text, + filters=filters, + ) + + if isinstance(search_response, list): + memories = search_response + elif isinstance(search_response, dict) and "results" in search_response: + memories = search_response["results"] + else: + memories = [search_response] + + line_separated_memories = "\n".join(memory.get("memory", "") for memory in memories) + if line_separated_memories: + context.extend_messages( + self.source_id, + [Message(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")], + ) + + async def after_run( + self, + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Store request/response messages to Mem0 for future retrieval.""" + self._validate_filters() + + messages_to_store: list[Message] = list(context.input_messages) + if context.response and context.response.messages: + messages_to_store.extend(context.response.messages) + + def get_role_value(role: Any) -> str: + return role.value if hasattr(role, "value") else str(role) + + messages: list[dict[str, str]] = [ + {"role": get_role_value(message.role), "content": message.text} + for message in messages_to_store + if get_role_value(message.role) in {"user", "assistant", "system"} and message.text and message.text.strip() + ] + + if messages: + await self.mem0_client.add( # type: ignore[misc] + messages=messages, + user_id=self.user_id, + agent_id=self.agent_id, + run_id=context.session_id, + metadata={"application_id": self.application_id}, + ) + + # -- Internal methods ------------------------------------------------------ + + def _validate_filters(self) -> None: + """Validates that at least one filter is provided.""" + if not self.agent_id and not self.user_id and not self.application_id: + raise ServiceInitializationError( + "At least one of the filters: agent_id, user_id, or application_id is required." + ) + + def _build_filters(self, *, session_id: str | None = None) -> dict[str, Any]: + """Build search filters from initialization parameters.""" + filters: dict[str, Any] = {} + if self.user_id: + filters["user_id"] = self.user_id + if self.agent_id: + filters["agent_id"] = self.agent_id + if session_id: + filters["run_id"] = session_id + if self.application_id: + filters["app_id"] = self.application_id + return filters + + +__all__ = ["_Mem0ContextProvider"] diff --git a/python/packages/mem0/agent_framework_mem0/_provider.py b/python/packages/mem0/agent_framework_mem0/_provider.py index 0dbad13134..d2ba0e7832 100644 --- a/python/packages/mem0/agent_framework_mem0/_provider.py +++ b/python/packages/mem0/agent_framework_mem0/_provider.py @@ -7,7 +7,7 @@ from collections.abc import MutableSequence, Sequence from contextlib import AbstractAsyncContextManager from typing import Any -from agent_framework import ChatMessage, Context, ContextProvider +from agent_framework import Context, ContextProvider, Message from agent_framework.exceptions import ServiceInitializationError from mem0 import AsyncMemory, AsyncMemoryClient @@ -103,19 +103,17 @@ class Mem0Provider(ContextProvider): @override async def invoked( self, - request_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage] | None = None, + request_messages: Message | Sequence[Message], + response_messages: Message | Sequence[Message] | None = None, invoke_exception: Exception | None = None, **kwargs: Any, ) -> None: self._validate_filters() - request_messages_list = ( - [request_messages] if isinstance(request_messages, ChatMessage) else list(request_messages) - ) + request_messages_list = [request_messages] if isinstance(request_messages, Message) else list(request_messages) response_messages_list = ( [response_messages] - if isinstance(response_messages, ChatMessage) + if isinstance(response_messages, Message) else list(response_messages) if response_messages else [] @@ -142,7 +140,7 @@ class Mem0Provider(ContextProvider): ) @override - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: """Called before invoking the AI model to provide context. Args: @@ -155,7 +153,7 @@ class Mem0Provider(ContextProvider): Context: Context object containing instructions with memories. """ self._validate_filters() - messages_list = [messages] if isinstance(messages, ChatMessage) else list(messages) + messages_list = [messages] if isinstance(messages, Message) else list(messages) input_text = "\n".join(msg.text for msg in messages_list if msg and msg.text and msg.text.strip()) # Validate input text is not empty before searching (possible for function approval responses) @@ -182,7 +180,7 @@ class Mem0Provider(ContextProvider): line_separated_memories = "\n".join(memory.get("memory", "") for memory in memories) return Context( - messages=[ChatMessage(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")] + messages=[Message(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")] if line_separated_memories else None ) diff --git a/python/packages/mem0/pyproject.toml b/python/packages/mem0/pyproject.toml index 016ad77bd6..b506240822 100644 --- a/python/packages/mem0/pyproject.toml +++ b/python/packages/mem0/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "mem0ai>=1.0.0", ] diff --git a/python/packages/mem0/tests/test_mem0_context_provider.py b/python/packages/mem0/tests/test_mem0_context_provider.py index 432468fe3f..f01d12053a 100644 --- a/python/packages/mem0/tests/test_mem0_context_provider.py +++ b/python/packages/mem0/tests/test_mem0_context_provider.py @@ -7,7 +7,7 @@ import sys from unittest.mock import AsyncMock import pytest -from agent_framework import ChatMessage, Content, Context +from agent_framework import Content, Context, Message from agent_framework.exceptions import ServiceInitializationError from agent_framework.mem0 import Mem0Provider @@ -33,12 +33,12 @@ def mock_mem0_client() -> AsyncMock: @pytest.fixture -def sample_messages() -> list[ChatMessage]: +def sample_messages() -> list[Message]: """Create sample chat messages for testing.""" return [ - ChatMessage(role="user", text="Hello, how are you?"), - ChatMessage(role="assistant", text="I'm doing well, thank you!"), - ChatMessage(role="system", text="You are a helpful assistant"), + Message(role="user", text="Hello, how are you?"), + Message(role="assistant", text="I'm doing well, thank you!"), + Message(role="system", text="You are a helpful assistant"), ] @@ -157,7 +157,7 @@ class TestMem0ProviderMessagesAdding: async def test_messages_adding_fails_without_filters(self, mock_mem0_client: AsyncMock) -> None: """Test that invoked fails when no filters are provided.""" provider = Mem0Provider(mem0_client=mock_mem0_client) - message = ChatMessage(role="user", text="Hello!") + message = Message(role="user", text="Hello!") with pytest.raises(ServiceInitializationError) as exc_info: await provider.invoked(message) @@ -167,7 +167,7 @@ class TestMem0ProviderMessagesAdding: async def test_messages_adding_single_message(self, mock_mem0_client: AsyncMock) -> None: """Test adding a single message.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) - message = ChatMessage(role="user", text="Hello!") + message = Message(role="user", text="Hello!") await provider.invoked(message) @@ -177,7 +177,7 @@ class TestMem0ProviderMessagesAdding: assert call_args.kwargs["user_id"] == "user123" async def test_messages_adding_multiple_messages( - self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] + self, mock_mem0_client: AsyncMock, sample_messages: list[Message] ) -> None: """Test adding multiple messages.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) @@ -194,7 +194,7 @@ class TestMem0ProviderMessagesAdding: assert call_args.kwargs["messages"] == expected_messages async def test_messages_adding_with_agent_id( - self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] + self, mock_mem0_client: AsyncMock, sample_messages: list[Message] ) -> None: """Test adding messages with agent_id.""" provider = Mem0Provider(agent_id="agent123", mem0_client=mock_mem0_client) @@ -206,7 +206,7 @@ class TestMem0ProviderMessagesAdding: assert call_args.kwargs["user_id"] is None async def test_messages_adding_with_application_id( - self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] + self, mock_mem0_client: AsyncMock, sample_messages: list[Message] ) -> None: """Test adding messages with application_id in metadata.""" provider = Mem0Provider(user_id="user123", application_id="app123", mem0_client=mock_mem0_client) @@ -217,7 +217,7 @@ class TestMem0ProviderMessagesAdding: assert call_args.kwargs["metadata"] == {"application_id": "app123"} async def test_messages_adding_with_scope_to_per_operation_thread_id( - self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] + self, mock_mem0_client: AsyncMock, sample_messages: list[Message] ) -> None: """Test adding messages with scope_to_per_operation_thread_id enabled.""" provider = Mem0Provider( @@ -235,7 +235,7 @@ class TestMem0ProviderMessagesAdding: assert call_args.kwargs["run_id"] == "operation_thread" async def test_messages_adding_without_scope_uses_base_thread_id( - self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] + self, mock_mem0_client: AsyncMock, sample_messages: list[Message] ) -> None: """Test adding messages without scope uses base thread_id.""" provider = Mem0Provider( @@ -254,9 +254,9 @@ class TestMem0ProviderMessagesAdding: """Test that empty or invalid messages are filtered out.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) messages = [ - ChatMessage(role="user", text=""), # Empty text - ChatMessage(role="user", text=" "), # Whitespace only - ChatMessage(role="user", text="Valid message"), + Message(role="user", text=""), # Empty text + Message(role="user", text=" "), # Whitespace only + Message(role="user", text="Valid message"), ] await provider.invoked(messages) @@ -269,8 +269,8 @@ class TestMem0ProviderMessagesAdding: """Test that mem0 client is not called when no valid messages exist.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) messages = [ - ChatMessage(role="user", text=""), - ChatMessage(role="user", text=" "), + Message(role="user", text=""), + Message(role="user", text=" "), ] await provider.invoked(messages) @@ -284,7 +284,7 @@ class TestMem0ProviderModelInvoking: async def test_model_invoking_fails_without_filters(self, mock_mem0_client: AsyncMock) -> None: """Test that invoking fails when no filters are provided.""" provider = Mem0Provider(mem0_client=mock_mem0_client) - message = ChatMessage(role="user", text="What's the weather?") + message = Message(role="user", text="What's the weather?") with pytest.raises(ServiceInitializationError) as exc_info: await provider.invoking(message) @@ -294,7 +294,7 @@ class TestMem0ProviderModelInvoking: async def test_model_invoking_single_message(self, mock_mem0_client: AsyncMock) -> None: """Test invoking with a single message.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) - message = ChatMessage(role="user", text="What's the weather?") + message = Message(role="user", text="What's the weather?") # Mock search results mock_mem0_client.search.return_value = [ @@ -319,7 +319,7 @@ class TestMem0ProviderModelInvoking: assert context.messages[0].text == expected_instructions async def test_model_invoking_multiple_messages( - self, mock_mem0_client: AsyncMock, sample_messages: list[ChatMessage] + self, mock_mem0_client: AsyncMock, sample_messages: list[Message] ) -> None: """Test invoking with multiple messages.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) @@ -335,7 +335,7 @@ class TestMem0ProviderModelInvoking: async def test_model_invoking_with_agent_id(self, mock_mem0_client: AsyncMock) -> None: """Test invoking with agent_id.""" provider = Mem0Provider(agent_id="agent123", mem0_client=mock_mem0_client) - message = ChatMessage(role="user", text="Hello") + message = Message(role="user", text="Hello") mock_mem0_client.search.return_value = [] @@ -353,7 +353,7 @@ class TestMem0ProviderModelInvoking: mem0_client=mock_mem0_client, ) provider._per_operation_thread_id = "operation_thread" - message = ChatMessage(role="user", text="Hello") + message = Message(role="user", text="Hello") mock_mem0_client.search.return_value = [] @@ -365,7 +365,7 @@ class TestMem0ProviderModelInvoking: async def test_model_invoking_no_memories_returns_none_instructions(self, mock_mem0_client: AsyncMock) -> None: """Test that no memories returns context with None instructions.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) - message = ChatMessage(role="user", text="Hello") + message = Message(role="user", text="Hello") mock_mem0_client.search.return_value = [] @@ -381,7 +381,7 @@ class TestMem0ProviderModelInvoking: provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) function_call = Content.from_function_call(call_id="1", name="test_func", arguments='{"arg1": "value1"}') - message = ChatMessage( + message = Message( role="user", contents=[ Content.from_function_approval_response( @@ -403,9 +403,9 @@ class TestMem0ProviderModelInvoking: """Test that empty message text is filtered out from query.""" provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) messages = [ - ChatMessage(role="user", text=""), - ChatMessage(role="user", text="Valid message"), - ChatMessage(role="user", text=" "), + Message(role="user", text=""), + Message(role="user", text="Valid message"), + Message(role="user", text=" "), ] mock_mem0_client.search.return_value = [] @@ -423,7 +423,7 @@ class TestMem0ProviderModelInvoking: context_prompt=custom_prompt, mem0_client=mock_mem0_client, ) - message = ChatMessage(role="user", text="Hello") + message = Message(role="user", text="Hello") mock_mem0_client.search.return_value = [{"memory": "Test memory"}] diff --git a/python/packages/mem0/tests/test_mem0_new_context_provider.py b/python/packages/mem0/tests/test_mem0_new_context_provider.py new file mode 100644 index 0000000000..a56e427e68 --- /dev/null +++ b/python/packages/mem0/tests/test_mem0_new_context_provider.py @@ -0,0 +1,352 @@ +# Copyright (c) Microsoft. All rights reserved. +# pyright: reportPrivateUsage=false + +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest +from agent_framework import AgentResponse, Message +from agent_framework._sessions import AgentSession, SessionContext +from agent_framework.exceptions import ServiceInitializationError + +from agent_framework_mem0._context_provider import _Mem0ContextProvider + + +@pytest.fixture +def mock_mem0_client() -> AsyncMock: + """Create a mock Mem0 AsyncMemoryClient.""" + from mem0 import AsyncMemoryClient + + mock_client = AsyncMock(spec=AsyncMemoryClient) + mock_client.add = AsyncMock() + mock_client.search = AsyncMock() + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock() + return mock_client + + +# -- Initialization tests ------------------------------------------------------ + + +class TestInit: + """Test _Mem0ContextProvider initialization.""" + + def test_init_with_all_params(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider( + source_id="mem0", + mem0_client=mock_mem0_client, + api_key="key-123", + application_id="app1", + agent_id="agent1", + user_id="user1", + context_prompt="Custom prompt", + ) + assert provider.source_id == "mem0" + assert provider.api_key == "key-123" + assert provider.application_id == "app1" + assert provider.agent_id == "agent1" + assert provider.user_id == "user1" + assert provider.context_prompt == "Custom prompt" + assert provider.mem0_client is mock_mem0_client + assert provider._should_close_client is False + + def test_init_default_context_prompt(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + assert provider.context_prompt == _Mem0ContextProvider.DEFAULT_CONTEXT_PROMPT + + def test_init_auto_creates_client_when_none(self) -> None: + """When no client is provided, a default AsyncMemoryClient is created and flagged for closing.""" + with ( + patch("mem0.client.main.AsyncMemoryClient.__init__", return_value=None) as mock_init, + patch("mem0.client.main.AsyncMemoryClient._validate_api_key", return_value=None), + ): + provider = _Mem0ContextProvider(source_id="mem0", api_key="test-key", user_id="u1") + mock_init.assert_called_once_with(api_key="test-key") + assert provider._should_close_client is True + + def test_provided_client_not_flagged_for_close(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + assert provider._should_close_client is False + + +# -- before_run tests ---------------------------------------------------------- + + +class TestBeforeRun: + """Test before_run hook.""" + + async def test_memories_added_to_context(self, mock_mem0_client: AsyncMock) -> None: + """Mocked mem0 search returns memories → messages added to context with prompt.""" + mock_mem0_client.search.return_value = [ + {"memory": "User likes Python"}, + {"memory": "User prefers dark mode"}, + ] + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", text="Hello")], session_id="s1") + + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_mem0_client.search.assert_awaited_once() + assert "mem0" in ctx.context_messages + added = ctx.context_messages["mem0"] + assert len(added) == 1 + assert "User likes Python" in added[0].text # type: ignore[operator] + assert "User prefers dark mode" in added[0].text # type: ignore[operator] + assert provider.context_prompt in added[0].text # type: ignore[operator] + + async def test_empty_input_skips_search(self, mock_mem0_client: AsyncMock) -> None: + """Empty input messages → no search performed.""" + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", text="")], session_id="s1") + + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_mem0_client.search.assert_not_awaited() + assert "mem0" not in ctx.context_messages + + async def test_empty_search_results_no_messages(self, mock_mem0_client: AsyncMock) -> None: + """Empty search results → no messages added.""" + mock_mem0_client.search.return_value = [] + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", text="test")], session_id="s1") + + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + assert "mem0" not in ctx.context_messages + + async def test_validates_filters_before_search(self, mock_mem0_client: AsyncMock) -> None: + """Raises ServiceInitializationError when no filters.""" + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client) + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", text="test")], session_id="s1") + + with pytest.raises(ServiceInitializationError, match="At least one of the filters"): + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + async def test_v1_1_response_format(self, mock_mem0_client: AsyncMock) -> None: + """Search response in v1.1 dict format with 'results' key.""" + mock_mem0_client.search.return_value = {"results": [{"memory": "remembered fact"}]} + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", text="test")], session_id="s1") + + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + added = ctx.context_messages["mem0"] + assert "remembered fact" in added[0].text # type: ignore[operator] + + async def test_search_query_combines_input_messages(self, mock_mem0_client: AsyncMock) -> None: + """Multiple input messages are joined for the search query.""" + mock_mem0_client.search.return_value = [] + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext( + input_messages=[ + Message(role="user", text="Hello"), + Message(role="user", text="World"), + ], + session_id="s1", + ) + + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + call_kwargs = mock_mem0_client.search.call_args.kwargs + assert call_kwargs["query"] == "Hello\nWorld" + + +# -- after_run tests ----------------------------------------------------------- + + +class TestAfterRun: + """Test after_run hook.""" + + async def test_stores_input_and_response(self, mock_mem0_client: AsyncMock) -> None: + """Stores input+response messages to mem0 via client.add.""" + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", text="question")], session_id="s1") + ctx._response = AgentResponse(messages=[Message(role="assistant", text="answer")]) + + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_mem0_client.add.assert_awaited_once() + call_kwargs = mock_mem0_client.add.call_args.kwargs + assert call_kwargs["messages"] == [ + {"role": "user", "content": "question"}, + {"role": "assistant", "content": "answer"}, + ] + assert call_kwargs["user_id"] == "u1" + assert call_kwargs["run_id"] == "s1" + + async def test_only_stores_user_assistant_system(self, mock_mem0_client: AsyncMock) -> None: + """Only stores user/assistant/system messages with text.""" + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext( + input_messages=[ + Message(role="user", text="hello"), + Message(role="tool", text="tool output"), + ], + session_id="s1", + ) + ctx._response = AgentResponse(messages=[Message(role="assistant", text="reply")]) + + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + call_kwargs = mock_mem0_client.add.call_args.kwargs + roles = [m["role"] for m in call_kwargs["messages"]] + assert "tool" not in roles + assert roles == ["user", "assistant"] + + async def test_skips_empty_messages(self, mock_mem0_client: AsyncMock) -> None: + """Skips messages with empty text.""" + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext( + input_messages=[ + Message(role="user", text=""), + Message(role="user", text=" "), + ], + session_id="s1", + ) + ctx._response = AgentResponse(messages=[]) + + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_mem0_client.add.assert_not_awaited() + + async def test_uses_session_id_as_run_id(self, mock_mem0_client: AsyncMock) -> None: + """Uses session_id as run_id.""" + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", text="hi")], session_id="my-session") + ctx._response = AgentResponse(messages=[Message(role="assistant", text="hey")]) + + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + assert mock_mem0_client.add.call_args.kwargs["run_id"] == "my-session" + + async def test_validates_filters(self, mock_mem0_client: AsyncMock) -> None: + """Raises ServiceInitializationError when no filters.""" + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client) + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", text="hi")], session_id="s1") + ctx._response = AgentResponse(messages=[Message(role="assistant", text="hey")]) + + with pytest.raises(ServiceInitializationError, match="At least one of the filters"): + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + async def test_stores_with_application_id_metadata(self, mock_mem0_client: AsyncMock) -> None: + """application_id is passed in metadata.""" + provider = _Mem0ContextProvider( + source_id="mem0", mem0_client=mock_mem0_client, user_id="u1", application_id="app1" + ) + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", text="hi")], session_id="s1") + ctx._response = AgentResponse(messages=[]) + + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + assert mock_mem0_client.add.call_args.kwargs["metadata"] == {"application_id": "app1"} + + +# -- _validate_filters tests -------------------------------------------------- + + +class TestValidateFilters: + """Test _validate_filters method.""" + + def test_raises_when_no_filters(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client) + with pytest.raises(ServiceInitializationError, match="At least one of the filters"): + provider._validate_filters() + + def test_passes_with_user_id(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + provider._validate_filters() # should not raise + + def test_passes_with_agent_id(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, agent_id="a1") + provider._validate_filters() + + def test_passes_with_application_id(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, application_id="app1") + provider._validate_filters() + + +# -- _build_filters tests ----------------------------------------------------- + + +class TestBuildFilters: + """Test _build_filters method.""" + + def test_user_id_only(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + assert provider._build_filters() == {"user_id": "u1"} + + def test_all_params(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider( + source_id="mem0", + mem0_client=mock_mem0_client, + user_id="u1", + agent_id="a1", + application_id="app1", + ) + assert provider._build_filters(session_id="sess1") == { + "user_id": "u1", + "agent_id": "a1", + "run_id": "sess1", + "app_id": "app1", + } + + def test_excludes_none_values(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + filters = provider._build_filters() + assert "agent_id" not in filters + assert "run_id" not in filters + assert "app_id" not in filters + + def test_session_id_mapped_to_run_id(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + filters = provider._build_filters(session_id="s99") + assert filters["run_id"] == "s99" + + def test_empty_when_no_params(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client) + assert provider._build_filters() == {} + + +# -- Context manager tests ----------------------------------------------------- + + +class TestContextManager: + """Test __aenter__/__aexit__ delegation.""" + + async def test_aenter_delegates_to_client(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + result = await provider.__aenter__() + assert result is provider + mock_mem0_client.__aenter__.assert_awaited_once() + + async def test_aexit_closes_auto_created_client(self, mock_mem0_client: AsyncMock) -> None: + """Auto-created clients (_should_close_client=True) are closed on exit.""" + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + provider._should_close_client = True + await provider.__aexit__(None, None, None) + mock_mem0_client.__aexit__.assert_awaited_once() + + async def test_aexit_does_not_close_provided_client(self, mock_mem0_client: AsyncMock) -> None: + """Provided clients (_should_close_client=False) are NOT closed on exit.""" + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + assert provider._should_close_client is False + await provider.__aexit__(None, None, None) + mock_mem0_client.__aexit__.assert_not_awaited() + + async def test_async_with_syntax(self, mock_mem0_client: AsyncMock) -> None: + provider = _Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, user_id="u1") + async with provider as p: + assert p is provider diff --git a/python/packages/ollama/agent_framework_ollama/_chat_client.py b/python/packages/ollama/agent_framework_ollama/_chat_client.py index 8ffba3be3e..6ead403c5b 100644 --- a/python/packages/ollama/agent_framework_ollama/_chat_client.py +++ b/python/packages/ollama/agent_framework_ollama/_chat_client.py @@ -9,7 +9,6 @@ from collections.abc import ( Awaitable, Callable, Mapping, - MutableMapping, Sequence, ) from itertools import chain @@ -18,7 +17,6 @@ from typing import Any, ClassVar, Generic, TypedDict from agent_framework import ( BaseChatClient, ChatAndFunctionMiddlewareTypes, - ChatMessage, ChatMiddlewareLayer, ChatOptions, ChatResponse, @@ -27,9 +25,8 @@ from agent_framework import ( FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, - HostedWebSearchTool, + Message, ResponseStream, - ToolProtocol, UsageDetails, get_logger, ) @@ -343,7 +340,7 @@ class OllamaChatClient( self.model_id = ollama_settings.model_id self.client = client or AsyncClient(host=ollama_settings.host) # Save Host URL for serialization with to_dict() - self.host = str(self.client._client.base_url) + self.host = str(self.client._client.base_url) # pyright: ignore[reportUnknownMemberType,reportPrivateUsage,reportUnknownArgumentType] super().__init__( middleware=middleware, @@ -356,7 +353,7 @@ class OllamaChatClient( def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], options: Mapping[str, Any], stream: bool = False, **kwargs: Any, @@ -397,7 +394,7 @@ class OllamaChatClient( return _get_response() - def _prepare_options(self, messages: Sequence[ChatMessage], options: Mapping[str, Any]) -> dict[str, Any]: + def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, Any]) -> dict[str, Any]: # Handle instructions by prepending to messages as system message instructions = options.get("instructions") if instructions: @@ -448,13 +445,13 @@ class OllamaChatClient( return run_options - def _prepare_messages_for_ollama(self, messages: Sequence[ChatMessage]) -> list[OllamaMessage]: + def _prepare_messages_for_ollama(self, messages: Sequence[Message]) -> list[OllamaMessage]: ollama_messages = [self._prepare_message_for_ollama(msg) for msg in messages] # Flatten the list of lists into a single list return list(chain.from_iterable(ollama_messages)) - def _prepare_message_for_ollama(self, message: ChatMessage) -> list[OllamaMessage]: - message_converters: dict[str, Callable[[ChatMessage], list[OllamaMessage]]] = { + def _prepare_message_for_ollama(self, message: Message) -> list[OllamaMessage]: + message_converters: dict[str, Callable[[Message], list[OllamaMessage]]] = { "system": self._format_system_message, "user": self._format_user_message, "assistant": self._format_assistant_message, @@ -462,10 +459,10 @@ class OllamaChatClient( } return message_converters[message.role](message) - def _format_system_message(self, message: ChatMessage) -> list[OllamaMessage]: + def _format_system_message(self, message: Message) -> list[OllamaMessage]: return [OllamaMessage(role="system", content=message.text)] - def _format_user_message(self, message: ChatMessage) -> list[OllamaMessage]: + def _format_user_message(self, message: Message) -> list[OllamaMessage]: if not any(c.type in {"text", "data"} for c in message.contents) and not message.text: raise ServiceInvalidRequestError( "Ollama connector currently only supports user messages with TextContent or DataContent." @@ -483,7 +480,7 @@ class OllamaChatClient( user_message["images"] = [c.uri.split(",")[1] for c in data_contents if c.uri] return [user_message] - def _format_assistant_message(self, message: ChatMessage) -> list[OllamaMessage]: + def _format_assistant_message(self, message: Message) -> list[OllamaMessage]: text_content = message.text # Ollama shouldn't have encrypted reasoning, so we just process text. reasoning_contents = "".join((c.text or "") for c in message.contents if c.type == "text_reasoning") @@ -506,7 +503,7 @@ class OllamaChatClient( ] return [assistant_message] - def _format_tool_message(self, message: ChatMessage) -> list[OllamaMessage]: + def _format_tool_message(self, message: Message) -> list[OllamaMessage]: # Ollama does not support multiple tool results in a single message, so we create a separate return [ OllamaMessage(role="tool", content=str(item.result), tool_name=item.call_id) @@ -538,7 +535,7 @@ class OllamaChatClient( contents = self._parse_contents_from_ollama(response) return ChatResponse( - messages=[ChatMessage(role="assistant", contents=contents)], + messages=[Message(role="assistant", contents=contents)], model_id=response.model, created_at=response.created_at, usage_details=UsageDetails( @@ -559,21 +556,22 @@ class OllamaChatClient( resp.append(fcc) return resp - def _prepare_tools_for_ollama(self, tools: list[ToolProtocol | MutableMapping[str, Any]]) -> list[dict[str, Any]]: - chat_tools: list[dict[str, Any]] = [] + def _prepare_tools_for_ollama(self, tools: list[Any]) -> list[Any]: + """Prepare tools for the Ollama API. + + Converts FunctionTool to JSON schema format. All other tools pass through unchanged. + + Args: + tools: List of tools to prepare. + + Returns: + List of tool definitions ready for the Ollama API. + """ + chat_tools: list[Any] = [] for tool in tools: - if isinstance(tool, ToolProtocol): - match tool: - case FunctionTool(): - chat_tools.append(tool.to_json_schema_spec()) - case HostedWebSearchTool(): - raise ServiceInvalidRequestError("HostedWebSearchTool is not supported by the Ollama client.") - case _: - raise ServiceInvalidRequestError( - "Unsupported tool type '" - f"{type(tool).__name__}" - "' for Ollama client. Supported tool types: FunctionTool." - ) + if isinstance(tool, FunctionTool): + chat_tools.append(tool.to_json_schema_spec()) else: - chat_tools.append(tool if isinstance(tool, dict) else dict(tool)) + # Pass through all other tools unchanged + chat_tools.append(tool) return chat_tools diff --git a/python/packages/ollama/pyproject.toml b/python/packages/ollama/pyproject.toml index 0534b7c37a..b1bc34c158 100644 --- a/python/packages/ollama/pyproject.toml +++ b/python/packages/ollama/pyproject.toml @@ -4,7 +4,7 @@ description = "Ollama integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "ollama >= 0.5.3", ] diff --git a/python/packages/ollama/tests/test_ollama_chat_client.py b/python/packages/ollama/tests/test_ollama_chat_client.py index 3d1f51e4c8..d65836b2bc 100644 --- a/python/packages/ollama/tests/test_ollama_chat_client.py +++ b/python/packages/ollama/tests/test_ollama_chat_client.py @@ -8,10 +8,9 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from agent_framework import ( BaseChatClient, - ChatMessage, ChatResponseUpdate, Content, - HostedWebSearchTool, + Message, chat_middleware, tool, ) @@ -77,7 +76,7 @@ def ollama_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # @fixture -def chat_history() -> list[ChatMessage]: +def chat_history() -> list[Message]: return [] @@ -244,12 +243,12 @@ async def test_empty_messages() -> None: async def test_cmc( mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: AsyncStream[OllamaChatResponse], ) -> None: mock_chat.return_value = mock_chat_completion_response - chat_history.append(ChatMessage(text="hello world", role="system")) - chat_history.append(ChatMessage(text="hello world", role="user")) + chat_history.append(Message(text="hello world", role="system")) + chat_history.append(Message(text="hello world", role="user")) ollama_client = OllamaChatClient() result = await ollama_client.get_response(messages=chat_history) @@ -261,11 +260,11 @@ async def test_cmc( async def test_cmc_reasoning( mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response_reasoning: AsyncStream[OllamaChatResponse], ) -> None: mock_chat.return_value = mock_chat_completion_response_reasoning - chat_history.append(ChatMessage(text="hello world", role="user")) + chat_history.append(Message(text="hello world", role="user")) ollama_client = OllamaChatClient() result = await ollama_client.get_response(messages=chat_history) @@ -278,11 +277,11 @@ async def test_cmc_reasoning( async def test_cmc_chat_failure( mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], ) -> None: # Simulate a failure in the Ollama client mock_chat.side_effect = Exception("Connection error") - chat_history.append(ChatMessage(text="hello world", role="user")) + chat_history.append(Message(text="hello world", role="user")) ollama_client = OllamaChatClient() @@ -297,12 +296,12 @@ async def test_cmc_chat_failure( async def test_cmc_streaming( mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_streaming_chat_completion_response: AsyncStream[OllamaChatResponse], ) -> None: mock_chat.return_value = mock_streaming_chat_completion_response - chat_history.append(ChatMessage(text="hello world", role="system")) - chat_history.append(ChatMessage(text="hello world", role="user")) + chat_history.append(Message(text="hello world", role="system")) + chat_history.append(Message(text="hello world", role="user")) ollama_client = OllamaChatClient() result = ollama_client.get_response(messages=chat_history, stream=True) @@ -315,11 +314,11 @@ async def test_cmc_streaming( async def test_cmc_streaming_reasoning( mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_streaming_chat_completion_response_reasoning: AsyncStream[OllamaChatResponse], ) -> None: mock_chat.return_value = mock_streaming_chat_completion_response_reasoning - chat_history.append(ChatMessage(text="hello world", role="user")) + chat_history.append(Message(text="hello world", role="user")) ollama_client = OllamaChatClient() result = ollama_client.get_response(messages=chat_history, stream=True) @@ -333,11 +332,11 @@ async def test_cmc_streaming_reasoning( async def test_cmc_streaming_chat_failure( mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], ) -> None: # Simulate a failure in the Ollama client for streaming mock_chat.side_effect = Exception("Streaming connection error") - chat_history.append(ChatMessage(text="hello world", role="user")) + chat_history.append(Message(text="hello world", role="user")) ollama_client = OllamaChatClient() @@ -353,7 +352,7 @@ async def test_cmc_streaming_chat_failure( async def test_cmc_streaming_with_tool_call( mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_streaming_chat_completion_response: AsyncStream[OllamaChatResponse], mock_streaming_chat_completion_tool_call: AsyncStream[OllamaChatResponse], ) -> None: @@ -362,7 +361,7 @@ async def test_cmc_streaming_with_tool_call( mock_streaming_chat_completion_response, ] - chat_history.append(ChatMessage(text="hello world", role="user")) + chat_history.append(Message(text="hello world", role="user")) ollama_client = OllamaChatClient() result = ollama_client.get_response(messages=chat_history, stream=True, options={"tools": [hello_world]}) @@ -384,39 +383,42 @@ async def test_cmc_streaming_with_tool_call( assert text_result.text == "test" -async def test_cmc_with_hosted_tool_call( +@patch.object(AsyncClient, "chat", new_callable=AsyncMock) +async def test_cmc_with_dict_tool_passthrough( + mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], + mock_chat_completion_response: OllamaChatResponse, ) -> None: - with pytest.raises(ServiceInvalidRequestError): - additional_properties = { - "user_location": { - "country": "US", - "city": "Seattle", - } - } + """Test that dict-based tools are passed through to Ollama.""" + mock_chat.return_value = mock_chat_completion_response + chat_history.append(Message(text="hello world", role="user")) - chat_history.append(ChatMessage(text="hello world", role="user")) + ollama_client = OllamaChatClient() + await ollama_client.get_response( + messages=chat_history, + options={ + "tools": [{"type": "function", "function": {"name": "custom_tool", "parameters": {}}}], + }, + ) - ollama_client = OllamaChatClient() - await ollama_client.get_response( - messages=chat_history, - options={ - "tools": HostedWebSearchTool(additional_properties=additional_properties), - }, - ) + # Verify the tool was passed through to the Ollama client + mock_chat.assert_called_once() + call_kwargs = mock_chat.call_args.kwargs + assert "tools" in call_kwargs + assert call_kwargs["tools"] == [{"type": "function", "function": {"name": "custom_tool", "parameters": {}}}] @patch.object(AsyncClient, "chat", new_callable=AsyncMock) async def test_cmc_with_data_content_type( mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: OllamaChatResponse, ) -> None: mock_chat.return_value = mock_chat_completion_response chat_history.append( - ChatMessage( + Message( contents=[Content.from_uri(uri="data:image/png;base64,xyz", media_type="image/png")], role="user", ) @@ -432,14 +434,14 @@ async def test_cmc_with_data_content_type( async def test_cmc_with_invalid_data_content_media_type( mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_streaming_chat_completion_response: AsyncStream[OllamaChatResponse], ) -> None: with pytest.raises(ServiceInvalidRequestError): mock_chat.return_value = mock_streaming_chat_completion_response # Remote Uris are not supported by Ollama client chat_history.append( - ChatMessage( + Message( contents=[Content.from_uri(uri="data:audio/mp3;base64,xyz", media_type="audio/mp3")], role="user", ) @@ -455,14 +457,14 @@ async def test_cmc_with_invalid_data_content_media_type( async def test_cmc_with_invalid_content_type( mock_chat: AsyncMock, ollama_unit_test_env: dict[str, str], - chat_history: list[ChatMessage], + chat_history: list[Message], mock_chat_completion_response: AsyncStream[OllamaChatResponse], ) -> None: with pytest.raises(ServiceInvalidRequestError): mock_chat.return_value = mock_chat_completion_response # Remote Uris are not supported by Ollama client chat_history.append( - ChatMessage( + Message( contents=[Content.from_uri(uri="http://example.com/image.png", media_type="image/png")], role="user", ) @@ -475,9 +477,9 @@ async def test_cmc_with_invalid_content_type( @skip_if_azure_integration_tests_disabled async def test_cmc_integration_with_tool_call( - chat_history: list[ChatMessage], + chat_history: list[Message], ) -> None: - chat_history.append(ChatMessage(text="Call the hello world function and repeat what it says", role="user")) + chat_history.append(Message(text="Call the hello world function and repeat what it says", role="user")) ollama_client = OllamaChatClient() result = await ollama_client.get_response(messages=chat_history, options={"tools": [hello_world]}) @@ -490,9 +492,9 @@ async def test_cmc_integration_with_tool_call( @skip_if_azure_integration_tests_disabled async def test_cmc_integration_with_chat_completion( - chat_history: list[ChatMessage], + chat_history: list[Message], ) -> None: - chat_history.append(ChatMessage(text="Say Hello World", role="user")) + chat_history.append(Message(text="Say Hello World", role="user")) ollama_client = OllamaChatClient() result = await ollama_client.get_response(messages=chat_history) @@ -502,9 +504,9 @@ async def test_cmc_integration_with_chat_completion( @skip_if_azure_integration_tests_disabled async def test_cmc_streaming_integration_with_tool_call( - chat_history: list[ChatMessage], + chat_history: list[Message], ) -> None: - chat_history.append(ChatMessage(text="Call the hello world function and repeat what it says", role="user")) + chat_history.append(Message(text="Call the hello world function and repeat what it says", role="user")) ollama_client = OllamaChatClient() result: AsyncIterable[ChatResponseUpdate] = ollama_client.get_response( @@ -527,9 +529,9 @@ async def test_cmc_streaming_integration_with_tool_call( @skip_if_azure_integration_tests_disabled async def test_cmc_streaming_integration_with_chat_completion( - chat_history: list[ChatMessage], + chat_history: list[Message], ) -> None: - chat_history.append(ChatMessage(text="Say Hello World", role="user")) + chat_history.append(Message(text="Say Hello World", role="user")) ollama_client = OllamaChatClient() result: AsyncIterable[ChatResponseUpdate] = ollama_client.get_response(messages=chat_history, stream=True) diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_base_group_chat_orchestrator.py b/python/packages/orchestrations/agent_framework_orchestrations/_base_group_chat_orchestrator.py index 4d93a3e69b..f01f3700f7 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_base_group_chat_orchestrator.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_base_group_chat_orchestrator.py @@ -12,7 +12,7 @@ from collections.abc import Awaitable, Callable, Sequence from dataclasses import dataclass from typing import Any, ClassVar, TypeAlias -from agent_framework._types import ChatMessage +from agent_framework._types import Message from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse from agent_framework._workflows._events import WorkflowEvent from agent_framework._workflows._executor import Executor, handler @@ -46,17 +46,17 @@ class GroupChatParticipantMessage: to other participants in the group chat to keep them synchronized. """ - messages: list[ChatMessage] + messages: list[Message] @dataclass class GroupChatResponseMessage: """Response envelope emitted by participants back to the orchestrator.""" - message: ChatMessage + message: Message -TerminationCondition: TypeAlias = Callable[[list[ChatMessage]], bool | Awaitable[bool]] +TerminationCondition: TypeAlias = Callable[[list[Message]], bool | Awaitable[bool]] GroupChatWorkflowContextOutT: TypeAlias = AgentExecutorRequest | GroupChatRequestMessage | GroupChatParticipantMessage @@ -167,7 +167,7 @@ class BaseGroupChatOrchestrator(Executor, ABC): self._round_index: int = 0 self._participant_registry = participant_registry # Shared conversation state management - self._full_conversation: list[ChatMessage] = [] + self._full_conversation: list[Message] = [] # region Handlers @@ -175,11 +175,11 @@ class BaseGroupChatOrchestrator(Executor, ABC): async def handle_str( self, task: str, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Handler for string input as workflow entry point. - Wraps the string in a USER role ChatMessage and delegates to _handle_task_message. + Wraps the string in a USER role Message and delegates to _handle_task_message. Args: task: Plain text task description from user @@ -188,32 +188,32 @@ class BaseGroupChatOrchestrator(Executor, ABC): Usage: workflow.run("Write a blog post about AI agents") """ - await self._handle_messages([ChatMessage(role="user", text=task)], ctx) + await self._handle_messages([Message(role="user", text=task)], ctx) @handler async def handle_message( self, - task: ChatMessage, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + task: Message, + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: - """Handler for single ChatMessage input as workflow entry point. + """Handler for single Message input as workflow entry point. Wraps the message in a list and delegates to _handle_task_message. Args: - task: ChatMessage from user + task: Message from user ctx: Workflow context Usage: - workflow.run(ChatMessage(role="user", text="Write a blog post about AI agents")) + workflow.run(Message(role="user", text="Write a blog post about AI agents")) """ await self._handle_messages([task], ctx) @handler async def handle_messages( self, - task: list[ChatMessage], - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + task: list[Message], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Handler for list of ChatMessages as workflow entry point. @@ -224,19 +224,19 @@ class BaseGroupChatOrchestrator(Executor, ABC): ctx: Workflow context Usage: workflow.run([ - ChatMessage(role="user", text="Write a blog post about AI agents"), - ChatMessage(role="user", text="Make it engaging and informative.") + Message(role="user", text="Write a blog post about AI agents"), + Message(role="user", text="Make it engaging and informative.") ]) """ if not task: - raise ValueError("At least one ChatMessage is required to start the group chat workflow.") + raise ValueError("At least one Message is required to start the group chat workflow.") await self._handle_messages(task, ctx) @handler async def handle_participant_response( self, response: AgentExecutorResponse | GroupChatResponseMessage, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Handler for participant responses. @@ -263,8 +263,8 @@ class BaseGroupChatOrchestrator(Executor, ABC): async def _handle_messages( self, - messages: list[ChatMessage], - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + messages: list[Message], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Handle task messages from users as workflow entry point. @@ -279,7 +279,7 @@ class BaseGroupChatOrchestrator(Executor, ABC): async def _handle_response( self, response: AgentExecutorResponse | GroupChatResponseMessage, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Handle a participant response. @@ -295,7 +295,7 @@ class BaseGroupChatOrchestrator(Executor, ABC): # Conversation state management (shared across all patterns) - def _append_messages(self, messages: Sequence[ChatMessage]) -> None: + def _append_messages(self, messages: Sequence[Message]) -> None: """Append messages to the conversation history. Args: @@ -303,7 +303,7 @@ class BaseGroupChatOrchestrator(Executor, ABC): """ self._full_conversation.extend(messages) - def _get_conversation(self) -> list[ChatMessage]: + def _get_conversation(self) -> list[Message]: """Get a copy of the current conversation. Returns: @@ -313,8 +313,8 @@ class BaseGroupChatOrchestrator(Executor, ABC): def _process_participant_response( self, response: AgentExecutorResponse | GroupChatResponseMessage - ) -> list[ChatMessage]: - """Extract ChatMessage from participant response. + ) -> list[Message]: + """Extract Message from participant response. Args: response: Response from participant @@ -351,7 +351,7 @@ class BaseGroupChatOrchestrator(Executor, ABC): result = await result return result - async def _check_terminate_and_yield(self, ctx: WorkflowContext[Never, list[ChatMessage]]) -> bool: + async def _check_terminate_and_yield(self, ctx: WorkflowContext[Never, list[Message]]) -> bool: """Check termination conditions and yield completion if met. Args: @@ -368,22 +368,22 @@ class BaseGroupChatOrchestrator(Executor, ABC): return False - def _create_completion_message(self, message: str) -> ChatMessage: + def _create_completion_message(self, message: str) -> Message: """Create a standardized completion message. Args: message: Completion text Returns: - ChatMessage with completion content + Message with completion content """ - return ChatMessage(role="assistant", text=message, author_name=self._name) + return Message(role="assistant", text=message, author_name=self._name) # Participant routing (shared across all patterns) async def _broadcast_messages_to_participants( self, - messages: list[ChatMessage], + messages: list[Message], ctx: WorkflowContext[AgentExecutorRequest | GroupChatParticipantMessage], participants: Sequence[str] | None = None, ) -> None: @@ -439,9 +439,9 @@ class BaseGroupChatOrchestrator(Executor, ABC): """ if self._participant_registry.is_agent(target): # AgentExecutors receive simple message list - messages: list[ChatMessage] = [] + messages: list[Message] = [] if additional_instruction: - messages.append(ChatMessage(role="user", text=additional_instruction)) + messages.append(Message(role="user", text=additional_instruction)) request = AgentExecutorRequest(messages=messages, should_respond=True) await ctx.send_message(request, target_id=target) await ctx.add_event( @@ -490,7 +490,7 @@ class BaseGroupChatOrchestrator(Executor, ABC): return False - async def _check_round_limit_and_yield(self, ctx: WorkflowContext[Never, list[ChatMessage]]) -> bool: + async def _check_round_limit_and_yield(self, ctx: WorkflowContext[Never, list[Message]]) -> bool: """Check round limit and yield completion if reached. Args: diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py b/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py index 9163168859..062e87806c 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py @@ -6,7 +6,7 @@ import logging from collections.abc import Callable, Sequence from typing import Any -from agent_framework import ChatMessage, SupportsAgentRun +from agent_framework import Message, SupportsAgentRun from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse from agent_framework._workflows._agent_utils import resolve_agent_id from agent_framework._workflows._checkpoint import CheckpointStorage @@ -29,8 +29,7 @@ parallel workflow with: - a default aggregator that combines all agent conversations and completes the workflow Notes: -- Participants can be provided as SupportsAgentRun or Executor instances via `participants=[...]`, - or as factories returning SupportsAgentRun or Executor via `participant_factories=[...]`. +- Participants can be provided as SupportsAgentRun or Executor instances via `participants=[...]`. - A custom aggregator can be provided as: - an Executor instance (it should handle list[AgentExecutorResponse], yield output), or @@ -57,14 +56,14 @@ class _DispatchToAllParticipants(Executor): await ctx.send_message(request) @handler - async def from_message(self, message: ChatMessage, ctx: WorkflowContext[AgentExecutorRequest]) -> None: + async def from_message(self, message: Message, ctx: WorkflowContext[AgentExecutorRequest]) -> None: request = AgentExecutorRequest(messages=normalize_messages_input(message), should_respond=True) await ctx.send_message(request) @handler async def from_messages( self, - messages: list[str | ChatMessage], + messages: list[str | Message], ctx: WorkflowContext[AgentExecutorRequest], ) -> None: request = AgentExecutorRequest(messages=normalize_messages_input(messages), should_respond=True) @@ -74,7 +73,7 @@ class _DispatchToAllParticipants(Executor): class _AggregateAgentConversations(Executor): """Aggregates agent responses and completes with combined ChatMessages. - Emits a list[ChatMessage] shaped as: + Emits a list[Message] shaped as: [ single_user_prompt?, agent1_final_assistant, agent2_final_assistant, ... ] - Extracts a single user prompt (first user message seen across results). @@ -83,9 +82,7 @@ class _AggregateAgentConversations(Executor): """ @handler - async def aggregate( - self, results: list[AgentExecutorResponse], ctx: WorkflowContext[Never, list[ChatMessage]] - ) -> None: + async def aggregate(self, results: list[AgentExecutorResponse], ctx: WorkflowContext[Never, list[Message]]) -> None: if not results: logger.error("Concurrent aggregator received empty results list") raise ValueError("Aggregation failed: no results provided") @@ -99,8 +96,8 @@ class _AggregateAgentConversations(Executor): role_str = str(role).lower() return r_str == role_str - prompt_message: ChatMessage | None = None - assistant_replies: list[ChatMessage] = [] + prompt_message: Message | None = None + assistant_replies: list[Message] = [] for r in results: resp_messages = list(getattr(r.agent_response, "messages", []) or []) @@ -133,7 +130,7 @@ class _AggregateAgentConversations(Executor): logger.error(f"Aggregation failed: no assistant replies found across {len(results)} results") raise RuntimeError("Aggregation failed: no assistant replies found") - output: list[ChatMessage] = [] + output: list[Message] = [] if prompt_message is not None: output.append(prompt_message) else: @@ -187,11 +184,8 @@ class ConcurrentBuilder: r"""High-level builder for concurrent agent workflows. - `participants=[...]` accepts a list of SupportsAgentRun (recommended) or Executor. - - `participant_factories=[...]` accepts a list of factories for SupportsAgentRun (recommended) - or Executor factories - `build()` wires: dispatcher -> fan-out -> participants -> fan-in -> aggregator. - `with_aggregator(...)` overrides the default aggregator with an Executor or callback. - - `register_aggregator(...)` accepts a factory for an Executor as custom aggregator. Usage: @@ -199,12 +193,9 @@ class ConcurrentBuilder: from agent_framework_orchestrations import ConcurrentBuilder - # Minimal: use default aggregator (returns list[ChatMessage]) + # Minimal: use default aggregator (returns list[Message]) workflow = ConcurrentBuilder(participants=[agent1, agent2, agent3]).build() - # With agent factories - workflow = ConcurrentBuilder(participant_factories=[create_agent1, create_agent2, create_agent3]).build() - # Custom aggregator via callback (sync or async). The callback receives # list[AgentExecutorResponse] and its return value becomes the workflow's output. @@ -215,20 +206,6 @@ class ConcurrentBuilder: workflow = ConcurrentBuilder(participants=[agent1, agent2, agent3]).with_aggregator(summarize).build() - # Custom aggregator via a factory - class MyAggregator(Executor): - @handler - async def aggregate(self, results: list[AgentExecutorResponse], ctx: WorkflowContext[Never, str]) -> None: - await ctx.yield_output(" | ".join(r.agent_response.messages[-1].text for r in results)) - - - workflow = ( - ConcurrentBuilder(participant_factories=[create_agent1, create_agent2, create_agent3]) - .register_aggregator(lambda: MyAggregator(id="my_aggregator")) - .build() - ) - - # Enable checkpoint persistence so runs can resume workflow = ConcurrentBuilder(participants=[agent1, agent2, agent3], checkpoint_storage=storage).build() @@ -239,58 +216,29 @@ class ConcurrentBuilder: def __init__( self, *, - participants: Sequence[SupportsAgentRun | Executor] | None = None, - participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None, + participants: Sequence[SupportsAgentRun | Executor], checkpoint_storage: CheckpointStorage | None = None, intermediate_outputs: bool = False, ) -> None: """Initialize the ConcurrentBuilder. Args: - participants: Optional sequence of agent or executor instances to run in parallel. - participant_factories: Optional sequence of callables returning agent or executor instances. + participants: Sequence of agent or executor instances to run in parallel. checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence. intermediate_outputs: If True, enables intermediate outputs from agent participants before aggregation. """ self._participants: list[SupportsAgentRun | Executor] = [] - self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = [] self._aggregator: Executor | None = None - self._aggregator_factory: Callable[[], Executor] | None = None self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage self._request_info_enabled: bool = False self._request_info_filter: set[str] | None = None self._intermediate_outputs: bool = intermediate_outputs - if participants is None and participant_factories is None: - raise ValueError("Either participants or participant_factories must be provided.") - - if participant_factories is not None: - self._set_participant_factories(participant_factories) - if participants is not None: - self._set_participants(participants) - - def _set_participant_factories( - self, - participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]], - ) -> None: - """Set participant factories (internal).""" - if self._participants: - raise ValueError("Cannot provide both participants and participant_factories.") - - if self._participant_factories: - raise ValueError("participant_factories already set.") - - if not participant_factories: - raise ValueError("participant_factories cannot be empty") - - self._participant_factories = list(participant_factories) + self._set_participants(participants) def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None: """Set participants (internal).""" - if self._participant_factories: - raise ValueError("Cannot provide both participants and participant_factories.") - if self._participants: raise ValueError("participants already set.") @@ -315,39 +263,6 @@ class ConcurrentBuilder: self._participants = list(participants) - def register_aggregator(self, aggregator_factory: Callable[[], Executor]) -> "ConcurrentBuilder": - r"""Define a custom aggregator for this concurrent workflow. - - Accepts a factory (callable) that returns an Executor instance. The executor - should handle `list[AgentExecutorResponse]` and yield output using `ctx.yield_output(...)`. - - Args: - aggregator_factory: Callable that returns an Executor instance - - Example: - .. code-block:: python - - class MyCustomExecutor(Executor): ... - - - wf = ( - ConcurrentBuilder() - .register_participants([create_researcher, create_marketer, create_legal]) - .register_aggregator(lambda: MyCustomExecutor(id="my_aggregator")) - .build() - ) - """ - if self._aggregator is not None: - raise ValueError( - "Cannot mix .with_aggregator(...) and .register_aggregator(...) in the same builder instance." - ) - - if self._aggregator_factory is not None: - raise ValueError("register_aggregator() has already been called on this builder instance.") - - self._aggregator_factory = aggregator_factory - return self - def with_aggregator( self, aggregator: Executor @@ -393,11 +308,6 @@ class ConcurrentBuilder: wf = ConcurrentBuilder(participants=[a1, a2, a3]).with_aggregator(summarize).build() """ - if self._aggregator_factory is not None: - raise ValueError( - "Cannot mix .with_aggregator(...) and .register_aggregator(...) in the same builder instance." - ) - if self._aggregator is not None: raise ValueError("with_aggregator() has already been called on this builder instance.") @@ -445,19 +355,10 @@ class ConcurrentBuilder: def _resolve_participants(self) -> list[Executor]: """Resolve participant instances into Executor objects.""" - if not self._participants and not self._participant_factories: - raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.") - # We don't need to check if both are set since that is handled in the respective methods + if not self._participants: + raise ValueError("No participants provided. Pass participants to the constructor.") - participants: list[Executor | SupportsAgentRun] = [] - if self._participant_factories: - # Resolve the participant factories now. This doesn't break the factory pattern - # since the Sequential builder still creates new instances per workflow build. - for factory in self._participant_factories: - p = factory() - participants.append(p) - else: - participants = self._participants + participants: list[Executor | SupportsAgentRun] = self._participants executors: list[Executor] = [] for p in participants: @@ -485,7 +386,7 @@ class ConcurrentBuilder: - If request info is enabled, the orchestration emits a request info event with outputs from all participants before sending the outputs to the aggregator - Aggregator yields output and the workflow becomes idle. The output is either: - - list[ChatMessage] (default aggregator: one user + one assistant per agent) + - list[Message] (default aggregator: one user + one assistant per agent) - custom payload from the provided aggregator Returns: @@ -502,15 +403,7 @@ class ConcurrentBuilder: """ # Internal nodes dispatcher = _DispatchToAllParticipants(id="dispatcher") - aggregator = ( - self._aggregator - if self._aggregator is not None - else ( - self._aggregator_factory() - if self._aggregator_factory is not None - else _AggregateAgentConversations(id="aggregator") - ) - ) + aggregator = self._aggregator if self._aggregator is not None else _AggregateAgentConversations(id="aggregator") # Resolve participants and participant factories to executors participants: list[Executor] = self._resolve_participants() diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py b/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py index f4edbbdcb1..d5ead8d9e7 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py @@ -28,9 +28,7 @@ from collections.abc import Awaitable, Callable, Sequence from dataclasses import dataclass from typing import Any, ClassVar, cast -from agent_framework import ChatAgent, SupportsAgentRun -from agent_framework._threads import AgentThread -from agent_framework._types import ChatMessage +from agent_framework import Agent, AgentThread, Message, SupportsAgentRun from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse from agent_framework._workflows._agent_utils import resolve_agent_id from agent_framework._workflows._checkpoint import CheckpointStorage @@ -69,7 +67,7 @@ class GroupChatState: Attributes: current_round: The current round index of the group chat, starting from 0. participants: A mapping of participant names to their descriptions in the group chat. - conversation: The full conversation history up to this point as a list of ChatMessage. + conversation: The full conversation history up to this point as a list of Message. """ # Round index, starting from 0 @@ -77,7 +75,7 @@ class GroupChatState: # participant name to description mapping as a ordered dict participants: OrderedDict[str, str] # Full conversation history up to this point - conversation: list[ChatMessage] + conversation: list[Message] # region Default orchestrator @@ -165,13 +163,13 @@ class GroupChatOrchestrator(BaseGroupChatOrchestrator): @override async def _handle_messages( self, - messages: list[ChatMessage], - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + messages: list[Message], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Initialize orchestrator state and start the conversation loop.""" self._append_messages(messages) # Termination condition will also be applied to the input messages - if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[ChatMessage]], ctx)): + if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[Message]], ctx)): return next_speaker = await self._get_next_speaker() @@ -192,7 +190,7 @@ class GroupChatOrchestrator(BaseGroupChatOrchestrator): async def _handle_response( self, response: AgentExecutorResponse | GroupChatResponseMessage, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Handle a participant response.""" messages = self._process_participant_response(response) @@ -200,9 +198,9 @@ class GroupChatOrchestrator(BaseGroupChatOrchestrator): messages = clean_conversation_for_handoff(messages) self._append_messages(messages) - if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[ChatMessage]], ctx)): + if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[Message]], ctx)): return - if await self._check_round_limit_and_yield(cast(WorkflowContext[Never, list[ChatMessage]], ctx)): + if await self._check_round_limit_and_yield(cast(WorkflowContext[Never, list[Message]], ctx)): return next_speaker = await self._get_next_speaker() @@ -287,7 +285,7 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator): def __init__( self, - agent: ChatAgent, + agent: Agent, participant_registry: ParticipantRegistry, *, max_rounds: int | None = None, @@ -318,29 +316,29 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator): self._thread = thread or agent.get_new_thread() # Cache for messages since last agent invocation # This is different from the full conversation history maintained by the base orchestrator - self._cache: list[ChatMessage] = [] + self._cache: list[Message] = [] @override - def _append_messages(self, messages: Sequence[ChatMessage]) -> None: + def _append_messages(self, messages: Sequence[Message]) -> None: self._cache.extend(messages) return super()._append_messages(messages) @override async def _handle_messages( self, - messages: list[ChatMessage], - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + messages: list[Message], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Initialize orchestrator state and start the conversation loop.""" self._append_messages(messages) # Termination condition will also be applied to the input messages - if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[ChatMessage]], ctx)): + if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[Message]], ctx)): return agent_orchestration_output = await self._invoke_agent() if await self._check_agent_terminate_and_yield( agent_orchestration_output, - cast(WorkflowContext[Never, list[ChatMessage]], ctx), + cast(WorkflowContext[Never, list[Message]], ctx), ): return @@ -361,22 +359,22 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator): async def _handle_response( self, response: AgentExecutorResponse | GroupChatResponseMessage, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Handle a participant response.""" messages = self._process_participant_response(response) # Remove tool-related content to prevent API errors from empty messages messages = clean_conversation_for_handoff(messages) self._append_messages(messages) - if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[ChatMessage]], ctx)): + if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[Message]], ctx)): return - if await self._check_round_limit_and_yield(cast(WorkflowContext[Never, list[ChatMessage]], ctx)): + if await self._check_round_limit_and_yield(cast(WorkflowContext[Never, list[Message]], ctx)): return agent_orchestration_output = await self._invoke_agent() if await self._check_agent_terminate_and_yield( agent_orchestration_output, - cast(WorkflowContext[Never, list[ChatMessage]], ctx), + cast(WorkflowContext[Never, list[Message]], ctx), ): return @@ -399,7 +397,7 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator): async def _invoke_agent(self) -> AgentOrchestrationOutput: """Invoke the orchestrator agent to determine the next speaker and termination.""" - async def _invoke_agent_helper(conversation: list[ChatMessage]) -> AgentOrchestrationOutput: + async def _invoke_agent_helper(conversation: list[Message]) -> AgentOrchestrationOutput: # Run the agent in non-streaming mode for simplicity agent_response = await self._agent.run( messages=conversation, @@ -431,7 +429,7 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator): ]) ) # Prepend instruction as system message - current_conversation.append(ChatMessage(role="user", text=instruction)) + current_conversation.append(Message(role="user", text=instruction)) retry_attempts = self._retry_attempts while True: @@ -445,7 +443,7 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator): logger.debug(f"Retrying agent orchestration invocation, attempts left: {retry_attempts}") # We don't need the full conversation since the thread should maintain history current_conversation = [ - ChatMessage( + Message( role="user", text=f"Your input could not be parsed due to an error: {ex}. Please try again.", ) @@ -454,7 +452,7 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator): async def _check_agent_terminate_and_yield( self, agent_orchestration_output: AgentOrchestrationOutput, - ctx: WorkflowContext[Never, list[ChatMessage]], + ctx: WorkflowContext[Never, list[Message]], ) -> bool: """Check if the agent requested termination and yield completion if so. @@ -518,7 +516,7 @@ class GroupChatBuilder: into a complete workflow graph that can be executed. Outputs: - The final conversation history as a list of ChatMessage once the group chat completes. + The final conversation history as a list of Message once the group chat completes. """ DEFAULT_ORCHESTRATOR_ID: ClassVar[str] = "group_chat_orchestrator" @@ -529,7 +527,7 @@ class GroupChatBuilder: participants: Sequence[SupportsAgentRun | Executor] | None = None, participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None, # Orchestrator config (exactly one required) - orchestrator_agent: ChatAgent | Callable[[], ChatAgent] | None = None, + orchestrator_agent: Agent | Callable[[], Agent] | None = None, orchestrator: BaseGroupChatOrchestrator | Callable[[], BaseGroupChatOrchestrator] | None = None, selection_func: GroupChatSelectionFunction | None = None, orchestrator_name: str | None = None, @@ -544,7 +542,7 @@ class GroupChatBuilder: Args: participants: Optional sequence of agent or executor instances for the group chat. participant_factories: Optional sequence of callables returning agent or executor instances. - orchestrator_agent: An instance of ChatAgent or a callable that produces one to manage the group chat. + orchestrator_agent: An instance of Agent or a callable that produces one to manage the group chat. orchestrator: An instance of BaseGroupChatOrchestrator or a callable that produces one to manage the group chat. selection_func: Callable that receives the current GroupChatState and returns the name of the next @@ -561,9 +559,9 @@ class GroupChatBuilder: # Orchestrator related members self._orchestrator: BaseGroupChatOrchestrator | None = None - self._orchestrator_factory: Callable[[], ChatAgent | BaseGroupChatOrchestrator] | None = None + self._orchestrator_factory: Callable[[], Agent | BaseGroupChatOrchestrator] | None = None self._selection_func: GroupChatSelectionFunction | None = None - self._agent_orchestrator: ChatAgent | None = None + self._agent_orchestrator: Agent | None = None self._termination_condition: TerminationCondition | None = termination_condition self._max_rounds: int | None = max_rounds self._orchestrator_name: str | None = None @@ -598,7 +596,7 @@ class GroupChatBuilder: def _set_orchestrator( self, *, - orchestrator_agent: ChatAgent | Callable[[], ChatAgent] | None = None, + orchestrator_agent: Agent | Callable[[], Agent] | None = None, orchestrator: BaseGroupChatOrchestrator | Callable[[], BaseGroupChatOrchestrator] | None = None, selection_func: GroupChatSelectionFunction | None = None, orchestrator_name: str | None = None, @@ -606,7 +604,7 @@ class GroupChatBuilder: """Set the orchestrator for this group chat workflow (internal). Args: - orchestrator_agent: An instance of ChatAgent or a callable that produces one to manage the group chat. + orchestrator_agent: An instance of Agent or a callable that produces one to manage the group chat. orchestrator: An instance of BaseGroupChatOrchestrator or a callable that produces one to manage the group chat. selection_func: Callable that receives the current GroupChatState and returns @@ -635,7 +633,7 @@ class GroupChatBuilder: if sum(x is not None for x in [orchestrator_agent, orchestrator, selection_func]) != 1: raise ValueError("Exactly one of orchestrator_agent, orchestrator, or selection_func must be provided.") - if orchestrator_agent is not None and isinstance(orchestrator_agent, ChatAgent): + if orchestrator_agent is not None and isinstance(orchestrator_agent, Agent): self._agent_orchestrator = orchestrator_agent elif orchestrator is not None and isinstance(orchestrator, BaseGroupChatOrchestrator): self._orchestrator = orchestrator @@ -707,11 +705,11 @@ class GroupChatBuilder: .. code-block:: python - from agent_framework import ChatMessage + from agent_framework import Message from agent_framework_orchestrations import GroupChatBuilder - def stop_after_two_calls(conversation: list[ChatMessage]) -> bool: + def stop_after_two_calls(conversation: list[Message]) -> bool: calls = sum(1 for msg in conversation if msg.role == "assistant" and msg.author_name == "specialist") return calls >= 2 @@ -852,7 +850,7 @@ class GroupChatBuilder: if self._orchestrator_factory: orchestrator_instance = self._orchestrator_factory() - if isinstance(orchestrator_instance, ChatAgent): + if isinstance(orchestrator_instance, Agent): return AgentBasedGroupChatOrchestrator( agent=orchestrator_instance, participant_registry=ParticipantRegistry(participants), @@ -862,7 +860,7 @@ class GroupChatBuilder: if isinstance(orchestrator_instance, BaseGroupChatOrchestrator): return orchestrator_instance raise TypeError( - f"Orchestrator factory must return ChatAgent or BaseGroupChatOrchestrator instance. " + f"Orchestrator factory must return Agent or BaseGroupChatOrchestrator instance. " f"Got {type(orchestrator_instance).__name__}." ) diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py index a227b6955e..37f499d763 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py @@ -32,15 +32,15 @@ Key properties: import inspect import logging import sys -from collections.abc import Awaitable, Callable, Mapping, Sequence +from collections.abc import Awaitable, Callable, Sequence from dataclasses import dataclass from typing import Any, cast -from agent_framework import ChatAgent, SupportsAgentRun +from agent_framework import Agent, SupportsAgentRun from agent_framework._middleware import FunctionInvocationContext, FunctionMiddleware from agent_framework._threads import AgentThread from agent_framework._tools import FunctionTool, tool -from agent_framework._types import AgentResponse, AgentResponseUpdate, ChatMessage +from agent_framework._types import AgentResponse, AgentResponseUpdate, Message from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse from agent_framework._workflows._agent_utils import resolve_agent_id from agent_framework._workflows._checkpoint import CheckpointStorage @@ -154,28 +154,28 @@ class HandoffAgentUserRequest: agent_response: AgentResponse @staticmethod - def create_response(response: str | list[str] | ChatMessage | list[ChatMessage]) -> list[ChatMessage]: + def create_response(response: str | list[str] | Message | list[Message]) -> list[Message]: """Create a HandoffAgentUserRequest from a simple text response.""" - messages: list[ChatMessage] = [] + messages: list[Message] = [] if isinstance(response, str): - messages.append(ChatMessage(role="user", text=response)) - elif isinstance(response, ChatMessage): + messages.append(Message(role="user", text=response)) + elif isinstance(response, Message): messages.append(response) elif isinstance(response, list): for item in response: - if isinstance(item, ChatMessage): + if isinstance(item, Message): messages.append(item) elif isinstance(item, str): - messages.append(ChatMessage(role="user", text=item)) + messages.append(Message(role="user", text=item)) else: - raise TypeError("List items must be either str or ChatMessage instances") + raise TypeError("List items must be either str or Message instances") else: - raise TypeError("Response must be str, list of str, ChatMessage, or list of ChatMessage") + raise TypeError("Response must be str, list of str, Message, or list of Message") return messages @staticmethod - def terminate() -> list[ChatMessage]: + def terminate() -> list[Message]: """Create a termination response for the handoff workflow.""" return [] @@ -248,10 +248,8 @@ class HandoffAgentExecutor(AgentExecutor): Returns: A new AgentExecutor instance with handoff tools added """ - if not isinstance(agent, ChatAgent): - raise TypeError( - "Handoff can only be applied to ChatAgent. Please ensure the agent is a ChatAgent instance." - ) + if not isinstance(agent, Agent): + raise TypeError("Handoff can only be applied to Agent. Please ensure the agent is a Agent instance.") # Clone the agent to avoid mutating the original cloned_agent = self._clone_chat_agent(agent) # type: ignore @@ -265,13 +263,13 @@ class HandoffAgentExecutor(AgentExecutor): return cloned_agent - def _clone_chat_agent(self, agent: ChatAgent) -> ChatAgent: - """Produce a deep copy of the ChatAgent while preserving runtime configuration.""" + def _clone_chat_agent(self, agent: Agent) -> Agent: + """Produce a deep copy of the Agent while preserving runtime configuration.""" options = agent.default_options middleware = list(agent.middleware or []) # Reconstruct the original tools list by combining regular tools with MCP tools. - # ChatAgent.__init__ separates MCP tools during initialization, + # Agent.__init__ separates MCP tools during initialization, # so we need to recombine them here to pass the complete tools list to the constructor. # This makes sure MCP tools are preserved when cloning agents for handoff workflows. tools_from_options = options.get("tools") @@ -303,8 +301,8 @@ class HandoffAgentExecutor(AgentExecutor): "user": options.get("user"), } - return ChatAgent( - chat_client=agent.chat_client, + return Agent( + client=agent.client, id=agent.id, name=agent.name, description=agent.description, @@ -314,13 +312,13 @@ class HandoffAgentExecutor(AgentExecutor): default_options=cloned_options, # type: ignore[arg-type] ) - def _apply_auto_tools(self, agent: ChatAgent, targets: Sequence[HandoffConfiguration]) -> None: + def _apply_auto_tools(self, agent: Agent, targets: Sequence[HandoffConfiguration]) -> None: """Attach synthetic handoff tools to a chat agent and return the target lookup table. Creates handoff tools for each specialist agent that this agent can route to. Args: - agent: The ChatAgent to add handoff tools to + agent: The Agent to add handoff tools to targets: Sequence of handoff configurations defining target agents """ default_options = agent.default_options @@ -375,7 +373,7 @@ class HandoffAgentExecutor(AgentExecutor): self._full_conversation.extend(self._cache) # Check termination condition before running the agent - if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[ChatMessage]], ctx)): + if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[Message]], ctx)): return # Run the agent @@ -427,19 +425,19 @@ class HandoffAgentExecutor(AgentExecutor): # or a termination condition is met. # This allows the agent to perform long-running tasks without returning control # to the coordinator or user prematurely. - self._cache.extend([ChatMessage(role="user", text=self._autonomous_mode_prompt)]) + self._cache.extend([Message(role="user", text=self._autonomous_mode_prompt)]) self._autonomous_mode_turns += 1 await self._run_agent_and_emit(ctx) else: # The response is handled via `handle_response` self._autonomous_mode_turns = 0 # Reset autonomous mode turn counter on handoff - await ctx.request_info(HandoffAgentUserRequest(response), list[ChatMessage]) + await ctx.request_info(HandoffAgentUserRequest(response), list[Message]) @response_handler async def handle_response( self, original_request: HandoffAgentUserRequest, - response: list[ChatMessage], + response: list[Message], ctx: WorkflowContext[AgentExecutorResponse, AgentResponse], ) -> None: """Handle user response for a request that is issued after agent runs. @@ -458,7 +456,7 @@ class HandoffAgentExecutor(AgentExecutor): If the response is empty, it indicates termination of the handoff workflow. """ if not response: - await cast(WorkflowContext[Never, list[ChatMessage]], ctx).yield_output(self._full_conversation) + await cast(WorkflowContext[Never, list[Message]], ctx).yield_output(self._full_conversation) return # Broadcast the user response to all other agents @@ -472,7 +470,7 @@ class HandoffAgentExecutor(AgentExecutor): async def _broadcast_messages( self, - messages: list[ChatMessage], + messages: list[Message], ctx: WorkflowContext[AgentExecutorRequest], ) -> None: """Broadcast the workflow cache to the agent before running.""" @@ -506,7 +504,7 @@ class HandoffAgentExecutor(AgentExecutor): return None - async def _check_terminate_and_yield(self, ctx: WorkflowContext[Never, list[ChatMessage]]) -> bool: + async def _check_terminate_and_yield(self, ctx: WorkflowContext[Never, list[Message]]) -> bool: """Check termination conditions and yield completion if met. Args: @@ -561,10 +559,10 @@ class HandoffBuilder: Participants must be agents. Support for custom executors is not available in handoff workflows. Outputs: - The final conversation history as a list of ChatMessage once the group chat completes. + The final conversation history as a list of Message once the group chat completes. Note: - 1. Agents in handoff workflows must be ChatAgent instances and support local tool calls. + 1. Agents in handoff workflows must be Agent instances and support local tool calls. 2. Handoff doesn't support intermediate outputs from agents. All outputs are returned as they become available. This is because agents in handoff workflows are not considered sub-agents of a central orchestrator, thus all outputs are directly emitted. @@ -575,7 +573,6 @@ class HandoffBuilder: *, name: str | None = None, participants: Sequence[SupportsAgentRun] | None = None, - participant_factories: Mapping[str, Callable[[], SupportsAgentRun]] | None = None, description: str | None = None, checkpoint_storage: CheckpointStorage | None = None, termination_condition: TerminationCondition | None = None, @@ -584,8 +581,7 @@ class HandoffBuilder: The builder starts in an unconfigured state and requires you to call: 1. `.participants([...])` - Register agents - 2. or `.participant_factories({...})` - Register agent factories - 3. `.build()` - Construct the final Workflow + 2. `.build()` - Construct the final Workflow Optional configuration methods allow you to customize context management, termination logic, and persistence. @@ -596,9 +592,6 @@ class HandoffBuilder: participants: Optional list of agents that will participate in the handoff workflow. You can also call `.participants([...])` later. Each participant must have a unique identifier (`.name` is preferred if set, otherwise `.id` is used). - participant_factories: Optional mapping of factory names to callables that produce agents when invoked. - This allows for lazy instantiation and state isolation per workflow instance - created by this builder. description: Optional human-readable description explaining the workflow's purpose. Useful for documentation and observability. checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence. @@ -610,10 +603,7 @@ class HandoffBuilder: # Participant related members self._participants: dict[str, SupportsAgentRun] = {} - self._participant_factories: dict[str, Callable[[], SupportsAgentRun]] = {} self._start_id: str | None = None - if participant_factories: - self.register_participants(participant_factories) if participants: self.participants(participants) @@ -631,71 +621,7 @@ class HandoffBuilder: self._autonomous_mode_enabled_agents: list[str] = [] # Termination related members - self._termination_condition: Callable[[list[ChatMessage]], bool | Awaitable[bool]] | None = ( - termination_condition - ) - - def register_participants( - self, participant_factories: Mapping[str, Callable[[], SupportsAgentRun]] - ) -> "HandoffBuilder": - """Register factories that produce agents for the handoff workflow. - - Each factory is a callable that returns an SupportsAgentRun instance. - Factories are invoked when building the workflow, allowing for lazy instantiation - and state isolation per workflow instance. - - Args: - participant_factories: Mapping of factory names to callables that return SupportsAgentRun - instances. Each produced participant must have a unique identifier - (`.name` is preferred if set, otherwise `.id` is used). - - Returns: - Self for method chaining. - - Raises: - ValueError: If participant_factories is empty or `.participants(...)` or `.register_participants(...)` - has already been called. - - Example: - .. code-block:: python - - from agent_framework import ChatAgent - from agent_framework_orchestrations import HandoffBuilder - - - def create_triage() -> ChatAgent: - return ... - - - def create_refund_agent() -> ChatAgent: - return ... - - - def create_billing_agent() -> ChatAgent: - return ... - - - factories = { - "triage": create_triage, - "refund": create_refund_agent, - "billing": create_billing_agent, - } - - # Handoff will be created automatically unless specified otherwise - # The default creates a mesh topology where all agents can handoff to all others - builder = HandoffBuilder().register_participants(factories) - builder.with_start_agent("triage") - """ - if self._participants: - raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.") - - if self._participant_factories: - raise ValueError("register_participants() has already been called on this builder instance.") - if not participant_factories: - raise ValueError("participant_factories cannot be empty") - - self._participant_factories = dict(participant_factories) - return self + self._termination_condition: Callable[[list[Message]], bool | Awaitable[bool]] | None = termination_condition def participants(self, participants: Sequence[SupportsAgentRun]) -> "HandoffBuilder": """Register the agents that will participate in the handoff workflow. @@ -708,8 +634,8 @@ class HandoffBuilder: Self for method chaining. Raises: - ValueError: If participants is empty, contains duplicates, or `.participants()` or - `.register_participants()` has already been called. + ValueError: If participants is empty, contains duplicates, or `.participants()` + has already been called. TypeError: If participants are not SupportsAgentRun instances. Example: @@ -727,9 +653,6 @@ class HandoffBuilder: builder = HandoffBuilder().participants([triage, refund, billing]) builder.with_start_agent(triage) """ - if self._participant_factories: - raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.") - if self._participants: raise ValueError("participants have already been assigned") @@ -755,8 +678,8 @@ class HandoffBuilder: def add_handoff( self, - source: str | SupportsAgentRun, - targets: Sequence[str] | Sequence[SupportsAgentRun], + source: SupportsAgentRun, + targets: Sequence[SupportsAgentRun], *, description: str | None = None, ) -> "HandoffBuilder": @@ -768,16 +691,8 @@ class HandoffBuilder: to all others by default (mesh topology). Args: - source: The agent that can initiate the handoff. Can be: - - Factory name (str): If using participant factories - - SupportsAgentRun instance: The actual agent object - - Cannot mix factory names and instances across source and targets - targets: One or more target agents that the source can hand off to. Can be: - - Factory name (str): If using participant factories - - SupportsAgentRun instance: The actual agent object - - Single target: ["billing_agent"] or [agent_instance] - - Multiple targets: ["billing_agent", "support_agent"] or [agent1, agent2] - - Cannot mix factory names and instances across source and targets + source: The agent that can initiate the handoff. + targets: One or more target agents that the source can hand off to. description: Optional custom description for the handoff. If not provided, the description of the target agent(s) will be used. If the target agent has no description, no description will be set for the handoff tool, which is not recommended. @@ -789,25 +704,10 @@ class HandoffBuilder: Self for method chaining. Raises: - ValueError: 1) If source or targets are not in the participants list, or if - participants(...) hasn't been called yet. - 2) If source or targets are factory names (str) but participant_factories(...) - hasn't been called yet, or if they are not in the participant_factories list. - TypeError: If mixing factory names (str) and SupportsAgentRun/Executor instances + ValueError: If source or targets are not in the participants list, or if + participants(...) hasn't been called yet. Examples: - Single target (using factory name): - - .. code-block:: python - - builder.add_handoff("triage_agent", "billing_agent") - - Multiple targets (using factory names): - - .. code-block:: python - - builder.add_handoff("triage_agent", ["billing_agent", "support_agent", "escalation_agent"]) - Multiple targets (using agent instances): .. code-block:: python @@ -830,96 +730,54 @@ class HandoffBuilder: - Handoff tools are automatically registered for each source agent - If a source agent is configured multiple times via add_handoff, targets are merged """ - if isinstance(source, str) and all(isinstance(t, str) for t in targets): - # Both source and targets are factory names - if not self._participant_factories: - raise ValueError("Call participant_factories(...) before add_handoff(...)") + if not self._participants: + raise ValueError("Call participants(...) before add_handoff(...)") - if source not in self._participant_factories: - raise ValueError(f"Source factory name '{source}' is not in the participant_factories list") + # Resolve source agent ID + source_id = self._resolve_to_id(source) + if source_id not in self._participants: + raise ValueError(f"Source agent '{source}' is not in the participants list") - for target in targets: - if target not in self._participant_factories: - raise ValueError(f"Target factory name '{target}' is not in the participant_factories list") + # Resolve all target IDs + target_ids: list[str] = [] + for target in targets: + target_id = self._resolve_to_id(target) + if target_id not in self._participants: + raise ValueError(f"Target agent '{target}' is not in the participants list") + target_ids.append(target_id) - # Merge with existing handoff configuration for this source - if source in self._handoff_config: - # Add new targets to existing list, avoiding duplicates - for t in targets: - if t in self._handoff_config[source]: - logger.warning(f"Handoff from '{source}' to '{t}' is already configured; overwriting.") - self._handoff_config[source].add(HandoffConfiguration(target=t, description=description)) - else: - self._handoff_config[source] = set() - for t in targets: - self._handoff_config[source].add(HandoffConfiguration(target=t, description=description)) - return self + # Merge with existing handoff configuration for this source + if source_id not in self._handoff_config: + self._handoff_config[source_id] = set() - if isinstance(source, (SupportsAgentRun)) and all(isinstance(t, SupportsAgentRun) for t in targets): - # Both source and targets are instances - if not self._participants: - raise ValueError("Call participants(...) before add_handoff(...)") + for t in target_ids: + config = HandoffConfiguration(target=t, description=description) + if config in self._handoff_config[source_id]: + logger.warning(f"Handoff from '{source_id}' to '{t}' is already configured; overwriting.") + # Remove old config so the new one (with updated description) takes effect + self._handoff_config[source_id].discard(config) + self._handoff_config[source_id].add(config) - # Resolve source agent ID - source_id = self._resolve_to_id(source) - if source_id not in self._participants: - raise ValueError(f"Source agent '{source}' is not in the participants list") + return self - # Resolve all target IDs - target_ids: list[str] = [] - for target in targets: - target_id = self._resolve_to_id(target) - if target_id not in self._participants: - raise ValueError(f"Target agent '{target}' is not in the participants list") - target_ids.append(target_id) - - # Merge with existing handoff configuration for this source - if source_id in self._handoff_config: - # Add new targets to existing list, avoiding duplicates - for t in target_ids: - if t in self._handoff_config[source_id]: - logger.warning(f"Handoff from '{source_id}' to '{t}' is already configured; overwriting.") - self._handoff_config[source_id].add(HandoffConfiguration(target=t, description=description)) - else: - self._handoff_config[source_id] = set() - for t in target_ids: - self._handoff_config[source_id].add(HandoffConfiguration(target=t, description=description)) - - return self - - raise TypeError( - "Cannot mix factory names (str) and SupportsAgentRun instances across source and targets in add_handoff()" - ) - - def with_start_agent(self, agent: str | SupportsAgentRun) -> "HandoffBuilder": + def with_start_agent(self, agent: SupportsAgentRun) -> "HandoffBuilder": """Set the agent that will initiate the handoff workflow. If not specified, the first registered participant will be used as the starting agent. Args: - agent: The agent that will start the workflow. Can be: - - Factory name (str): If using participant factories - - SupportsAgentRun instance: The actual agent object + agent: The agent that will start the workflow. + Returns: Self for method chaining. """ - if isinstance(agent, str): - if self._participant_factories: - if agent not in self._participant_factories: - raise ValueError(f"Start agent factory name '{agent}' is not in the participant_factories list") - else: - raise ValueError("Call register_participants(...) before with_start_agent(...)") - self._start_id = agent - elif isinstance(agent, SupportsAgentRun): - resolved_id = self._resolve_to_id(agent) - if self._participants: - if resolved_id not in self._participants: - raise ValueError(f"Start agent '{resolved_id}' is not in the participants list") - else: - raise ValueError("Call participants(...) before with_start_agent(...)") - self._start_id = resolved_id + resolved_id = self._resolve_to_id(agent) + if self._participants: + if resolved_id not in self._participants: + raise ValueError(f"Start agent '{resolved_id}' is not in the participants list") else: - raise TypeError("Start agent must be a factory name (str) or an SupportsAgentRun instance") + raise ValueError("Call participants(...) before with_start_agent(...)") + self._start_id = resolved_id return self @@ -1026,7 +884,7 @@ class HandoffBuilder: # Asynchronous condition - async def check_termination(conv: list[ChatMessage]) -> bool: + async def check_termination(conv: list[Message]) -> bool: # Can perform async operations return len(conv) > 20 @@ -1090,48 +948,21 @@ class HandoffBuilder: # region Internal Helper Methods def _resolve_agents(self) -> dict[str, SupportsAgentRun]: - """Resolve participant factories into agent instances. - - If agent instances were provided directly via participants(...), those are - returned as-is. If participant factories were provided via participant_factories(...), - those are invoked to create the agent instances. + """Resolve participant instances into agent instances. Returns: - Map of executor IDs or factory names to `SupportsAgentRun` instances + Map of executor IDs to `SupportsAgentRun` instances """ - if not self._participants and not self._participant_factories: - raise ValueError("No participants provided. Call .participants() or .register_participants() first.") - # We don't need to check if both are set since that is handled in the respective methods + if not self._participants: + raise ValueError("No participants provided. Call .participants() first.") - if self._participants: - return self._participants + return self._participants - if self._participant_factories: - # Invoke each factory to create participant instances - factory_names_to_agents: dict[str, SupportsAgentRun] = {} - for factory_name, factory in self._participant_factories.items(): - instance = factory() - if isinstance(instance, SupportsAgentRun): - resolved_id = self._resolve_to_id(instance) - else: - raise TypeError(f"Participants must be SupportsAgentRun instances. Got {type(instance).__name__}.") - - if resolved_id in factory_names_to_agents: - raise ValueError(f"Duplicate participant name '{resolved_id}' detected") - - # Map executors by factory name (not executor.id) because handoff configs reference factory names - # This allows users to configure handoffs using the factory names they provided - factory_names_to_agents[factory_name] = instance - - return factory_names_to_agents - - raise ValueError("No executors or participant_factories have been configured") - - def _resolve_handoffs(self, agents: Mapping[str, SupportsAgentRun]) -> dict[str, list[HandoffConfiguration]]: - """Handoffs may be specified using factory names or instances; resolve to executor IDs. + def _resolve_handoffs(self, agents: dict[str, SupportsAgentRun]) -> dict[str, list[HandoffConfiguration]]: + """Resolve handoff configurations to executor IDs. Args: - agents: Map of agent IDs or factory names to `SupportsAgentRun` instances + agents: Map of agent IDs to `SupportsAgentRun` instances Returns: Map of executor IDs to list of HandoffConfiguration instances @@ -1145,14 +976,14 @@ class HandoffBuilder: if not source_agent: raise ValueError( f"Handoff source agent '{source_id}' not found. " - "Please make sure source has been added as either a participant or participant_factory." + "Please make sure source has been added as a participant." ) for handoff_config in handoff_configurations: target_agent = agents.get(handoff_config.target_id) if not target_agent: raise ValueError( f"Handoff target agent '{handoff_config.target_id}' not found for source '{source_id}'. " - "Please make sure target has been added as either a participant or participant_factory." + "Please make sure target has been added as a participant." ) updated_handoff_configurations.setdefault(self._resolve_to_id(source_agent), []).append( @@ -1184,7 +1015,7 @@ class HandoffBuilder: """Resolve agents into HandoffAgentExecutors. Args: - agents: Map of agent IDs or factory names to `SupportsAgentRun` instances + agents: Map of agent IDs to `SupportsAgentRun` instances handoffs: Map of executor IDs to list of HandoffConfiguration instances Returns: diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py b/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py index 779dad2d5a..eec597cdda 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py @@ -14,7 +14,7 @@ from typing import Any, ClassVar, TypeVar, cast from agent_framework import ( AgentResponse, - ChatMessage, + Message, SupportsAgentRun, ) from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse @@ -56,7 +56,7 @@ ORCH_MSG_KIND_INSTRUCTION = "instruction" ORCH_MSG_KIND_NOTICE = "notice" -def _message_to_payload(message: ChatMessage) -> Any: +def _message_to_payload(message: Message) -> Any: if hasattr(message, "to_dict") and callable(getattr(message, "to_dict", None)): with contextlib.suppress(Exception): return message.to_dict() # type: ignore[attr-defined] @@ -72,24 +72,24 @@ def _message_to_payload(message: ChatMessage) -> Any: return message -def _message_from_payload(payload: Any) -> ChatMessage: - if isinstance(payload, ChatMessage): +def _message_from_payload(payload: Any) -> Message: + if isinstance(payload, Message): return payload - if hasattr(ChatMessage, "from_dict") and isinstance(payload, dict): + if hasattr(Message, "from_dict") and isinstance(payload, dict): with contextlib.suppress(Exception): - return ChatMessage.from_dict(payload) # type: ignore[attr-defined,no-any-return] - if hasattr(ChatMessage, "from_json") and isinstance(payload, str): + return Message.from_dict(payload) # type: ignore[attr-defined,no-any-return] + if hasattr(Message, "from_json") and isinstance(payload, str): with contextlib.suppress(Exception): - return ChatMessage.from_json(payload) # type: ignore[attr-defined,no-any-return] + return Message.from_json(payload) # type: ignore[attr-defined,no-any-return] if isinstance(payload, dict): with contextlib.suppress(Exception): - return ChatMessage(**payload) # type: ignore[arg-type] + return Message(**payload) # type: ignore[arg-type] if isinstance(payload, str): with contextlib.suppress(Exception): decoded = json.loads(payload) if isinstance(decoded, dict): return _message_from_payload(decoded) - raise TypeError("Unable to reconstruct ChatMessage from payload") + raise TypeError("Unable to reconstruct Message from payload") # region Magentic One Prompts @@ -247,7 +247,7 @@ The answer should be phrased as if you were speaking to the user. # region Messages and Types -def _new_chat_history() -> list[ChatMessage]: +def _new_chat_history() -> list[Message]: """Typed default factory for chat history list to satisfy type checkers.""" return [] @@ -261,8 +261,8 @@ def _new_participant_descriptions() -> dict[str, str]: class _MagenticTaskLedger(DictConvertible): """Internal: Task ledger for the Standard Magentic manager.""" - facts: ChatMessage - plan: ChatMessage + facts: Message + plan: Message def to_dict(self) -> dict[str, Any]: return {"facts": _message_to_payload(self.facts), "plan": _message_to_payload(self.plan)} @@ -328,7 +328,7 @@ class MagenticContext(DictConvertible): """Context for the Magentic manager.""" task: str - chat_history: list[ChatMessage] = field(default_factory=_new_chat_history) + chat_history: list[Message] = field(default_factory=_new_chat_history) participant_descriptions: dict[str, str] = field(default_factory=_new_participant_descriptions) round_count: int = 0 stall_count: int = 0 @@ -353,7 +353,7 @@ class MagenticContext(DictConvertible): raise ValueError("MagenticContext requires a 'task' string field.") # `chat_history` is required chat_history_payload = data.get("chat_history", []) - history: list[ChatMessage] = [] + history: list[Message] = [] for item in chat_history_payload: history.append(_message_from_payload(item)) # `participant_descriptions` is required @@ -396,7 +396,7 @@ def _team_block(participants: dict[str, str]) -> str: def _extract_json(text: str) -> dict[str, Any]: """Potentially temp helper method. - Note: this method is required right now because the ChatClientProtocol, when calling + Note: this method is required right now because the SupportsChatGetResponse, when calling response.text, returns duplicate JSON payloads - need to figure out why. The `text` method is concatenating multiple text contents from diff msgs into a single string. @@ -472,12 +472,12 @@ class MagenticManagerBase(ABC): self.task_ledger_full_prompt: str = ORCHESTRATOR_TASK_LEDGER_FULL_PROMPT @abstractmethod - async def plan(self, magentic_context: MagenticContext) -> ChatMessage: + async def plan(self, magentic_context: MagenticContext) -> Message: """Create a plan for the task.""" ... @abstractmethod - async def replan(self, magentic_context: MagenticContext) -> ChatMessage: + async def replan(self, magentic_context: MagenticContext) -> Message: """Replan for the task.""" ... @@ -487,7 +487,7 @@ class MagenticManagerBase(ABC): ... @abstractmethod - async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: + async def prepare_final_answer(self, magentic_context: MagenticContext) -> Message: """Prepare the final answer.""" ... @@ -501,7 +501,7 @@ class MagenticManagerBase(ABC): class StandardMagenticManager(MagenticManagerBase): - """Standard Magentic manager that performs real LLM calls via a ChatAgent. + """Standard Magentic manager that performs real LLM calls via a Agent. The manager constructs prompts that mirror the original Magentic One orchestration: - Facts gathering @@ -580,8 +580,8 @@ class StandardMagenticManager(MagenticManagerBase): async def _complete( self, - messages: list[ChatMessage], - ) -> ChatMessage: + messages: list[Message], + ) -> Message: """Call the underlying agent and return the last assistant message. The agent's run method is called which applies the agent's configured options @@ -595,19 +595,19 @@ class StandardMagenticManager(MagenticManagerBase): return response.messages[-1] - async def plan(self, magentic_context: MagenticContext) -> ChatMessage: + async def plan(self, magentic_context: MagenticContext) -> Message: """Create facts and plan using the model, then render a combined task ledger as a single assistant message.""" team_text = _team_block(magentic_context.participant_descriptions) # Gather facts - facts_user = ChatMessage( + facts_user = Message( role="user", text=self.task_ledger_facts_prompt.format(task=magentic_context.task), ) facts_msg = await self._complete([*magentic_context.chat_history, facts_user]) # Create plan - plan_user = ChatMessage( + plan_user = Message( role="user", text=self.task_ledger_plan_prompt.format(team=team_text), ) @@ -626,9 +626,9 @@ class StandardMagenticManager(MagenticManagerBase): facts=facts_msg.text, plan=plan_msg.text, ) - return ChatMessage(role="assistant", text=combined, author_name=MAGENTIC_MANAGER_NAME) + return Message(role="assistant", text=combined, author_name=MAGENTIC_MANAGER_NAME) - async def replan(self, magentic_context: MagenticContext) -> ChatMessage: + async def replan(self, magentic_context: MagenticContext) -> Message: """Update facts and plan when stalling or looping has been detected.""" if self.task_ledger is None: raise RuntimeError("replan() called before plan(); call plan() once before requesting a replan.") @@ -636,7 +636,7 @@ class StandardMagenticManager(MagenticManagerBase): team_text = _team_block(magentic_context.participant_descriptions) # Update facts - facts_update_user = ChatMessage( + facts_update_user = Message( role="user", text=self.task_ledger_facts_update_prompt.format( task=magentic_context.task, old_facts=self.task_ledger.facts.text @@ -645,7 +645,7 @@ class StandardMagenticManager(MagenticManagerBase): updated_facts = await self._complete([*magentic_context.chat_history, facts_update_user]) # Update plan - plan_update_user = ChatMessage( + plan_update_user = Message( role="user", text=self.task_ledger_plan_update_prompt.format(team=team_text), ) @@ -669,7 +669,7 @@ class StandardMagenticManager(MagenticManagerBase): facts=updated_facts.text, plan=updated_plan.text, ) - return ChatMessage(role="assistant", text=combined, author_name=MAGENTIC_MANAGER_NAME) + return Message(role="assistant", text=combined, author_name=MAGENTIC_MANAGER_NAME) async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: """Use the model to produce a JSON progress ledger based on the conversation so far. @@ -689,7 +689,7 @@ class StandardMagenticManager(MagenticManagerBase): team=team_text, names=names_csv, ) - user_message = ChatMessage(role="user", text=prompt) + user_message = Message(role="user", text=prompt) # Include full context to help the model decide current stage, with small retry loop attempts = 0 @@ -713,13 +713,13 @@ class StandardMagenticManager(MagenticManagerBase): f"Progress ledger parse failed after {self.progress_ledger_retry_count} attempt(s): {last_error}" ) - async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: + async def prepare_final_answer(self, magentic_context: MagenticContext) -> Message: """Ask the model to produce the final answer addressed to the user.""" prompt = self.final_answer_prompt.format(task=magentic_context.task) - user_message = ChatMessage(role="user", text=prompt) + user_message = Message(role="user", text=prompt) response = await self._complete([*magentic_context.chat_history, user_message]) # Ensure role is assistant - return ChatMessage( + return Message( role="assistant", text=response.text, author_name=response.author_name or MAGENTIC_MANAGER_NAME, @@ -771,7 +771,7 @@ class MagenticOrchestratorEvent: """Data payload for magentic_orchestrator events.""" event_type: MagenticOrchestratorEventType - content: ChatMessage | MagenticProgressLedger + content: Message | MagenticProgressLedger # region Request info related types @@ -786,7 +786,7 @@ class MagenticPlanReviewResponse: the plan is considered approved. """ - review: list[ChatMessage] + review: list[Message] @staticmethod def approve() -> "MagenticPlanReviewResponse": @@ -794,14 +794,14 @@ class MagenticPlanReviewResponse: return MagenticPlanReviewResponse(review=[]) @staticmethod - def revise(feedback: str | list[str] | ChatMessage | list[ChatMessage]) -> "MagenticPlanReviewResponse": + def revise(feedback: str | list[str] | Message | list[Message]) -> "MagenticPlanReviewResponse": """Create a revision response with feedback.""" if isinstance(feedback, str): - feedback = [ChatMessage(role="user", text=feedback)] - elif isinstance(feedback, ChatMessage): + feedback = [Message(role="user", text=feedback)] + elif isinstance(feedback, Message): feedback = [feedback] elif isinstance(feedback, list): - feedback = [ChatMessage(role="user", text=item) if isinstance(item, str) else item for item in feedback] + feedback = [Message(role="user", text=item) if isinstance(item, str) else item for item in feedback] return MagenticPlanReviewResponse(review=feedback) @@ -820,7 +820,7 @@ class MagenticPlanReviewRequest: is_stalled: Whether the workflow is currently stalled. """ - plan: ChatMessage + plan: Message current_progress: MagenticProgressLedger | None is_stalled: bool @@ -828,7 +828,7 @@ class MagenticPlanReviewRequest: """Create an approval response.""" return MagenticPlanReviewResponse.approve() - def revise(self, feedback: str | list[str] | ChatMessage | list[ChatMessage]) -> MagenticPlanReviewResponse: + def revise(self, feedback: str | list[str] | Message | list[Message]) -> MagenticPlanReviewResponse: """Create a revision response with feedback.""" return MagenticPlanReviewResponse.revise(feedback) @@ -877,7 +877,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): # Task related state self._magentic_context: MagenticContext | None = None - self._task_ledger: ChatMessage | None = None + self._task_ledger: Message | None = None self._progress_ledger: MagenticProgressLedger | None = None # Termination related state @@ -887,8 +887,8 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): @override async def _handle_messages( self, - messages: list[ChatMessage], - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + messages: list[Message], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Handle the initial task messages to start the workflow.""" if self._terminated: @@ -942,7 +942,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): async def _handle_response( self, response: AgentExecutorResponse | GroupChatResponseMessage, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Handle a response message from a participant.""" if self._magentic_context is None or self._task_ledger is None: @@ -968,7 +968,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): self, original_request: MagenticPlanReviewRequest, response: MagenticPlanReviewResponse, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Handle the human response to the plan review request. @@ -1029,7 +1029,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): async def _run_inner_loop( self, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Run the inner orchestration loop. Coordination phase. Serialized with a lock.""" if self._magentic_context is None or self._task_ledger is None: @@ -1039,16 +1039,14 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): async def _run_inner_loop_helper( self, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Run inner loop with exclusive access.""" # Narrow optional context for the remainder of this method if self._magentic_context is None: raise RuntimeError("Context not initialized") # Check limits first - within_limits = await self._check_within_limits_or_complete( - cast(WorkflowContext[Never, list[ChatMessage]], ctx) - ) + within_limits = await self._check_within_limits_or_complete(cast(WorkflowContext[Never, list[Message]], ctx)) if not within_limits: return @@ -1083,7 +1081,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): # Check for task completion if self._progress_ledger.is_request_satisfied.answer: logger.info("Magentic Orchestrator: Task completed") - await self._prepare_final_answer(cast(WorkflowContext[Never, list[ChatMessage]], ctx)) + await self._prepare_final_answer(cast(WorkflowContext[Never, list[Message]], ctx)) return # Check for stalling or looping @@ -1107,11 +1105,11 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): if next_speaker not in self._participant_registry.participants: logger.warning(f"Invalid next speaker: {next_speaker}") - await self._prepare_final_answer(cast(WorkflowContext[Never, list[ChatMessage]], ctx)) + await self._prepare_final_answer(cast(WorkflowContext[Never, list[Message]], ctx)) return # Add instruction to conversation (assistant guidance) - instruction_msg = ChatMessage( + instruction_msg = Message( role="assistant", text=str(instruction), author_name=MAGENTIC_MANAGER_NAME, @@ -1128,7 +1126,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): async def _reset_and_replan( self, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Reset context and replan.""" if self._magentic_context is None: @@ -1166,7 +1164,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): async def _run_outer_loop( self, - ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[ChatMessage]], + ctx: WorkflowContext[GroupChatWorkflowContextOutT, list[Message]], ) -> None: """Run the outer orchestration loop - planning phase.""" if self._magentic_context is None: @@ -1183,7 +1181,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): # Start inner loop await self._run_inner_loop(ctx) - async def _prepare_final_answer(self, ctx: WorkflowContext[Never, list[ChatMessage]]) -> None: + async def _prepare_final_answer(self, ctx: WorkflowContext[Never, list[Message]]) -> None: """Prepare the final answer using the manager.""" if self._magentic_context is None: raise RuntimeError("Context not initialized") @@ -1196,7 +1194,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): self._terminated = True - async def _check_within_limits_or_complete(self, ctx: WorkflowContext[Never, list[ChatMessage]]) -> bool: + async def _check_within_limits_or_complete(self, ctx: WorkflowContext[Never, list[Message]]) -> bool: """Check if orchestrator is within operational limits. If limits are exceeded, yield a termination message and mark the workflow as terminated. @@ -1223,7 +1221,7 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator): # Yield the full conversation with an indication of termination due to limits await ctx.yield_output([ *self._magentic_context.chat_history, - ChatMessage( + Message( role="assistant", text=f"Workflow terminated due to reaching maximum {limit_type} count.", author_name=MAGENTIC_MANAGER_NAME, @@ -1374,8 +1372,7 @@ class MagenticBuilder: def __init__( self, *, - participants: Sequence[SupportsAgentRun | Executor] | None = None, - participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None, + participants: Sequence[SupportsAgentRun | Executor], # Manager config (exactly one required) manager: MagenticManagerBase | None = None, manager_factory: Callable[[], MagenticManagerBase] | None = None, @@ -1401,8 +1398,7 @@ class MagenticBuilder: """Initialize the Magentic workflow builder. Args: - participants: Optional sequence of agent or executor instances for the workflow. - participant_factories: Optional sequence of callables returning agent or executor instances. + participants: Sequence of agent or executor instances for the workflow. manager: Pre-configured manager instance (subclass of MagenticManagerBase). manager_factory: Callable that returns a new MagenticManagerBase instance. manager_agent: Agent instance for creating a StandardMagenticManager. @@ -1423,7 +1419,6 @@ class MagenticBuilder: intermediate_outputs: If True, enables intermediate outputs from agent participants. """ self._participants: dict[str, SupportsAgentRun | Executor] = {} - self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = [] # Manager related members self._manager: MagenticManagerBase | None = None @@ -1437,13 +1432,7 @@ class MagenticBuilder: # Intermediate outputs self._intermediate_outputs = intermediate_outputs - if participants is None and participant_factories is None: - raise ValueError("Either participants or participant_factories must be provided.") - - if participant_factories is not None: - self._set_participant_factories(participant_factories) - if participants is not None: - self._set_participants(participants) + self._set_participants(participants) # Set manager if provided if any(x is not None for x in [manager, manager_factory, manager_agent, manager_agent_factory]): @@ -1465,27 +1454,8 @@ class MagenticBuilder: max_round_count=max_round_count, ) - def _set_participant_factories( - self, - participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]], - ) -> None: - """Set participant factories (internal).""" - if self._participants: - raise ValueError("Cannot provide both participants and participant_factories.") - - if self._participant_factories: - raise ValueError("participant_factories already set.") - - if not participant_factories: - raise ValueError("participant_factories cannot be empty") - - self._participant_factories = list(participant_factories) - def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None: """Set participants (internal).""" - if self._participant_factories: - raise ValueError("Cannot provide both participants and participant_factories.") - if self._participants: raise ValueError("participants already set.") @@ -1750,17 +1720,10 @@ class MagenticBuilder: def _resolve_participants(self) -> list[Executor]: """Resolve participant instances into Executor objects.""" - if not self._participants and not self._participant_factories: - raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.") - # We don't need to check if both are set since that is handled in the respective methods + if not self._participants: + raise ValueError("No participants provided. Pass participants to the constructor.") - participants: list[Executor | SupportsAgentRun] = [] - if self._participant_factories: - for factory in self._participant_factories: - participant = factory() - participants.append(participant) - else: - participants = list(self._participants.values()) + participants: list[Executor | SupportsAgentRun] = list(self._participants.values()) executors: list[Executor] = [] for participant in participants: diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py b/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py index 51f4e27898..5e4a5d6a28 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from agent_framework._agents import SupportsAgentRun -from agent_framework._types import ChatMessage +from agent_framework._types import Message from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse from agent_framework._workflows._agent_utils import resolve_agent_id from agent_framework._workflows._executor import Executor, handler @@ -44,18 +44,18 @@ class AgentRequestInfoResponse: """Response containing additional information requested from users for agents. Attributes: - messages: list[ChatMessage]: Additional messages provided by users. If empty, + messages: list[Message]: Additional messages provided by users. If empty, the agent response is approved as-is. """ - messages: list[ChatMessage] + messages: list[Message] @staticmethod - def from_messages(messages: list[ChatMessage]) -> "AgentRequestInfoResponse": + def from_messages(messages: list[Message]) -> "AgentRequestInfoResponse": """Create an AgentRequestInfoResponse from a list of ChatMessages. Args: - messages: List of ChatMessage instances provided by users. + messages: List of Message instances provided by users. Returns: AgentRequestInfoResponse instance. @@ -72,7 +72,7 @@ class AgentRequestInfoResponse: Returns: AgentRequestInfoResponse instance. """ - return AgentRequestInfoResponse(messages=[ChatMessage(role="user", text=text) for text in texts]) + return AgentRequestInfoResponse(messages=[Message(role="user", text=text) for text in texts]) @staticmethod def approve() -> "AgentRequestInfoResponse": diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_state.py b/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_state.py index fe8ba64126..0f23f96dc0 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_state.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_state.py @@ -11,11 +11,11 @@ from __future__ import annotations from dataclasses import dataclass, field from typing import Any -from agent_framework._types import ChatMessage +from agent_framework._types import Message -def _new_chat_message_list() -> list[ChatMessage]: - """Factory function for typed empty ChatMessage list. +def _new_chat_message_list() -> list[Message]: + """Factory function for typed empty Message list. Satisfies the type checker. """ @@ -47,11 +47,11 @@ class OrchestrationState: task: Optional primary task/question being orchestrated """ - conversation: list[ChatMessage] = field(default_factory=_new_chat_message_list) + conversation: list[Message] = field(default_factory=_new_chat_message_list) round_index: int = 0 orchestrator_name: str = "" metadata: dict[str, Any] = field(default_factory=_new_metadata_dict) - task: ChatMessage | None = None + task: Message | None = None def to_dict(self) -> dict[str, Any]: """Serialize to dict for checkpointing. diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_orchestrator_helpers.py b/python/packages/orchestrations/agent_framework_orchestrations/_orchestrator_helpers.py index c48af3c6de..757e77f095 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_orchestrator_helpers.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_orchestrator_helpers.py @@ -8,12 +8,12 @@ No inheritance required - just import and call. import logging -from agent_framework._types import ChatMessage +from agent_framework._types import Message logger = logging.getLogger(__name__) -def clean_conversation_for_handoff(conversation: list[ChatMessage]) -> list[ChatMessage]: +def clean_conversation_for_handoff(conversation: list[Message]) -> list[Message]: """Remove tool-related content from conversation for clean handoffs. During handoffs, tool calls can cause API errors because: @@ -37,7 +37,7 @@ def clean_conversation_for_handoff(conversation: list[ChatMessage]) -> list[Chat Returns: Cleaned conversation safe for handoff routing """ - cleaned: list[ChatMessage] = [] + cleaned: list[Message] = [] for msg in conversation: # Skip tool response messages entirely if msg.role == "tool": @@ -58,7 +58,7 @@ def clean_conversation_for_handoff(conversation: list[ChatMessage]) -> list[Chat # Has tool content - only keep if it also has text if msg.text and msg.text.strip(): # Create fresh text-only message while preserving additional_properties - msg_copy = ChatMessage( + msg_copy = Message( role=msg.role, text=msg.text, author_name=msg.author_name, @@ -74,7 +74,7 @@ def create_completion_message( text: str | None = None, author_name: str, reason: str = "completed", -) -> ChatMessage: +) -> Message: """Create a standardized completion message. Simple helper to avoid duplicating completion message creation. @@ -85,10 +85,10 @@ def create_completion_message( reason: Reason for completion (for default text generation) Returns: - ChatMessage with assistant role + Message with assistant role """ message_text = text or f"Conversation {reason}." - return ChatMessage( + return Message( role="assistant", text=message_text, author_name=author_name, diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py b/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py index 3ddecd56dc..5ef4f7fe8c 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py @@ -4,9 +4,8 @@ This module provides a high-level, agent-focused API to assemble a sequential workflow where: -- Participants can be provided as SupportsAgentRun or Executor instances via `participants=[...]`, - or as factories returning SupportsAgentRun or Executor via `participant_factories=[...]` -- A shared conversation context (list[ChatMessage]) is passed along the chain +- Participants are provided as SupportsAgentRun or Executor instances via `participants=[...]` +- A shared conversation context (list[Message]) is passed along the chain - Agents append their assistant messages to the context - Custom executors can transform or summarize and return a refined context - The workflow finishes with the final context produced by the last participant @@ -17,16 +16,16 @@ Typical wiring: Notes: - Participants can mix SupportsAgentRun and Executor objects - Agents are auto-wrapped by WorkflowBuilder as AgentExecutor (unless already wrapped) -- AgentExecutor produces AgentExecutorResponse; _ResponseToConversation converts this to list[ChatMessage] -- Non-agent executors must define a handler that consumes `list[ChatMessage]` and sends back - the updated `list[ChatMessage]` via their workflow context +- AgentExecutor produces AgentExecutorResponse; _ResponseToConversation converts this to list[Message] +- Non-agent executors must define a handler that consumes `list[Message]` and sends back + the updated `list[Message]` via their workflow context Why include the small internal adapter executors? - Input normalization ("input-conversation"): ensures the workflow always starts with a - `list[ChatMessage]` regardless of whether callers pass a `str`, a single `ChatMessage`, + `list[Message]` regardless of whether callers pass a `str`, a single `Message`, or a list. This keeps the first hop strongly typed and avoids boilerplate in participants. - Agent response adaptation ("to-conversation:"): agents (via AgentExecutor) - emit `AgentExecutorResponse`. The adapter converts that to a `list[ChatMessage]` + emit `AgentExecutorResponse`. The adapter converts that to a `list[Message]` using `full_conversation` so original prompts aren't lost when chaining. - Result output ("end"): yields the final conversation list and the workflow becomes idle giving a consistent terminal payload shape for both agents and custom executors. @@ -38,10 +37,10 @@ confusion and to mirror how the concurrent builder uses explicit dispatcher/aggr """ # noqa: E501 import logging -from collections.abc import Callable, Sequence +from collections.abc import Sequence from typing import Any -from agent_framework import ChatMessage, SupportsAgentRun +from agent_framework import Message, SupportsAgentRun from agent_framework._workflows._agent_executor import ( AgentExecutor, AgentExecutorResponse, @@ -63,18 +62,18 @@ logger = logging.getLogger(__name__) class _InputToConversation(Executor): - """Normalizes initial input into a list[ChatMessage] conversation.""" + """Normalizes initial input into a list[Message] conversation.""" @handler - async def from_str(self, prompt: str, ctx: WorkflowContext[list[ChatMessage]]) -> None: + async def from_str(self, prompt: str, ctx: WorkflowContext[list[Message]]) -> None: await ctx.send_message(normalize_messages_input(prompt)) @handler - async def from_message(self, message: ChatMessage, ctx: WorkflowContext[list[ChatMessage]]) -> None: + async def from_message(self, message: Message, ctx: WorkflowContext[list[Message]]) -> None: await ctx.send_message(normalize_messages_input(message)) @handler - async def from_messages(self, messages: list[str | ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None: + async def from_messages(self, messages: list[str | Message], ctx: WorkflowContext[list[Message]]) -> None: await ctx.send_message(normalize_messages_input(messages)) @@ -84,10 +83,10 @@ class _EndWithConversation(Executor): @handler async def end_with_messages( self, - conversation: list[ChatMessage], - ctx: WorkflowContext[Any, list[ChatMessage]], + conversation: list[Message], + ctx: WorkflowContext[Any, list[Message]], ) -> None: - """Handler for ending with a list of ChatMessage. + """Handler for ending with a list of Message. This is used when the last participant is a custom executor. """ @@ -97,7 +96,7 @@ class _EndWithConversation(Executor): async def end_with_agent_executor_response( self, response: AgentExecutorResponse, - ctx: WorkflowContext[Any, list[ChatMessage] | None], + ctx: WorkflowContext[Any, list[Message] | None], ) -> None: """Handle case where last participant is an agent. @@ -110,12 +109,10 @@ class SequentialBuilder: r"""High-level builder for sequential agent/executor workflows with shared context. - `participants=[...]` accepts a list of SupportsAgentRun (recommended) or Executor instances - - `participant_factories=[...]` accepts a list of factories for SupportsAgentRun (recommended) - or Executor factories - - Executors must define a handler that consumes list[ChatMessage] and sends out a list[ChatMessage] - - The workflow wires participants in order, passing a list[ChatMessage] down the chain + - Executors must define a handler that consumes list[Message] and sends out a list[Message] + - The workflow wires participants in order, passing a list[Message] down the chain - Agents append their assistant messages to the conversation - - Custom executors can transform/summarize and return a list[ChatMessage] + - Custom executors can transform/summarize and return a list[Message] - The final output is the conversation produced by the last participant Usage: @@ -127,11 +124,6 @@ class SequentialBuilder: # With agent instances workflow = SequentialBuilder(participants=[agent1, agent2, summarizer_exec]).build() - # With agent factories - workflow = SequentialBuilder( - participant_factories=[create_agent1, create_agent2, create_summarizer_exec] - ).build() - # Enable checkpoint persistence workflow = SequentialBuilder(participants=[agent1, agent2], checkpoint_storage=storage).build() @@ -149,55 +141,27 @@ class SequentialBuilder: def __init__( self, *, - participants: Sequence[SupportsAgentRun | Executor] | None = None, - participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None, + participants: Sequence[SupportsAgentRun | Executor], checkpoint_storage: CheckpointStorage | None = None, intermediate_outputs: bool = False, ) -> None: """Initialize the SequentialBuilder. Args: - participants: Optional sequence of agent or executor instances to run sequentially. - participant_factories: Optional sequence of callables returning agent or executor instances. + participants: Sequence of agent or executor instances to run sequentially. checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence. intermediate_outputs: If True, enables intermediate outputs from agent participants. """ self._participants: list[SupportsAgentRun | Executor] = [] - self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = [] self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage self._request_info_enabled: bool = False self._request_info_filter: set[str] | None = None self._intermediate_outputs: bool = intermediate_outputs - if participants is None and participant_factories is None: - raise ValueError("Either participants or participant_factories must be provided.") - - if participant_factories is not None: - self._set_participant_factories(participant_factories) - if participants is not None: - self._set_participants(participants) - - def _set_participant_factories( - self, - participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]], - ) -> None: - """Set participant factories (internal).""" - if self._participants: - raise ValueError("Cannot provide both participants and participant_factories.") - - if self._participant_factories: - raise ValueError("participant_factories already set.") - - if not participant_factories: - raise ValueError("participant_factories cannot be empty") - - self._participant_factories = list(participant_factories) + self._set_participants(participants) def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None: """Set participants (internal).""" - if self._participant_factories: - raise ValueError("Cannot provide both participants and participant_factories.") - if self._participants: raise ValueError("participants already set.") @@ -256,19 +220,10 @@ class SequentialBuilder: def _resolve_participants(self) -> list[Executor]: """Resolve participant instances into Executor objects.""" - if not self._participants and not self._participant_factories: - raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.") - # We don't need to check if both are set since that is handled in the respective methods + if not self._participants: + raise ValueError("No participants provided. Pass participants to the constructor.") - participants: list[Executor | SupportsAgentRun] = [] - if self._participant_factories: - # Resolve the participant factories now. This doesn't break the factory pattern - # since the Sequential builder still creates new instances per workflow build. - for factory in self._participant_factories: - p = factory() - participants.append(p) - else: - participants = self._participants + participants: list[Executor | SupportsAgentRun] = self._participants executors: list[Executor] = [] for p in participants: @@ -291,7 +246,7 @@ class SequentialBuilder: """Build and validate the sequential workflow. Wiring pattern: - - _InputToConversation normalizes the initial input into list[ChatMessage] + - _InputToConversation normalizes the initial input into list[Message] - For each participant in order: - If Agent (or AgentExecutor): pass conversation to the agent, then optionally route through a request info interceptor, then convert response to conversation diff --git a/python/packages/orchestrations/pyproject.toml b/python/packages/orchestrations/pyproject.toml index 6e3ab8f46e..b8ca3b4eb7 100644 --- a/python/packages/orchestrations/pyproject.toml +++ b/python/packages/orchestrations/pyproject.toml @@ -4,7 +4,7 @@ description = "Orchestration patterns for Microsoft Agent Framework. Includes Se authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", ] [tool.uv] diff --git a/python/packages/orchestrations/tests/test_concurrent.py b/python/packages/orchestrations/tests/test_concurrent.py index cecc8500c8..55100af4c3 100644 --- a/python/packages/orchestrations/tests/test_concurrent.py +++ b/python/packages/orchestrations/tests/test_concurrent.py @@ -7,8 +7,8 @@ from agent_framework import ( AgentExecutorRequest, AgentExecutorResponse, AgentResponse, - ChatMessage, Executor, + Message, WorkflowContext, WorkflowRunState, handler, @@ -32,7 +32,7 @@ class _FakeAgentExec(Executor): @handler async def run(self, request: AgentExecutorRequest, ctx: WorkflowContext[AgentExecutorResponse]) -> None: - response = AgentResponse(messages=ChatMessage(role="assistant", text=self._reply_text)) + response = AgentResponse(messages=Message(role="assistant", text=self._reply_text)) full_conversation = list(request.messages) + list(response.messages) await ctx.send_message(AgentExecutorResponse(self.id, response, full_conversation=full_conversation)) @@ -49,47 +49,6 @@ def test_concurrent_builder_rejects_duplicate_executors() -> None: ConcurrentBuilder(participants=[a, b]) -def test_concurrent_builder_rejects_duplicate_executors_from_factories() -> None: - """Test that duplicate executor IDs from factories are detected at build time.""" - - def create_dup1() -> Executor: - return _FakeAgentExec("dup", "A") - - def create_dup2() -> Executor: - return _FakeAgentExec("dup", "B") # same executor id - - builder = ConcurrentBuilder(participant_factories=[create_dup1, create_dup2]) - with pytest.raises(ValueError, match="Duplicate executor ID 'dup' detected in workflow."): - builder.build() - - -def test_concurrent_builder_rejects_mixed_participants_and_factories() -> None: - """Test that passing both participants and participant_factories to the constructor raises an error.""" - with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"): - ConcurrentBuilder( - participants=[_FakeAgentExec("a", "A")], - participant_factories=[lambda: _FakeAgentExec("b", "B")], - ) - - -def test_concurrent_builder_rejects_both_participants_and_factories() -> None: - """Test that passing both participants and participant_factories raises an error.""" - with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"): - ConcurrentBuilder( - participants=[_FakeAgentExec("a", "A")], - participant_factories=[lambda: _FakeAgentExec("b", "B")], - ) - - -def test_concurrent_builder_rejects_both_factories_and_participants() -> None: - """Test that passing both participant_factories and participants raises an error.""" - with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"): - ConcurrentBuilder( - participant_factories=[lambda: _FakeAgentExec("a", "A")], - participants=[_FakeAgentExec("b", "B")], - ) - - async def test_concurrent_default_aggregator_emits_single_user_and_assistants() -> None: # Three synthetic agent executors e1 = _FakeAgentExec("agentA", "Alpha") @@ -99,18 +58,18 @@ async def test_concurrent_default_aggregator_emits_single_user_and_assistants() wf = ConcurrentBuilder(participants=[e1, e2, e3]).build() completed = False - output: list[ChatMessage] | None = None + output: list[Message] | None = None async for ev in wf.run("prompt: hello world", stream=True): if ev.type == "status" and ev.state == WorkflowRunState.IDLE: completed = True elif ev.type == "output": - output = cast(list[ChatMessage], ev.data) + output = cast(list[Message], ev.data) if completed and output is not None: break assert completed assert output is not None - messages: list[ChatMessage] = output + messages: list[Message] = output # Expect one user message + one assistant message per participant assert len(messages) == 1 + 3 @@ -130,7 +89,7 @@ async def test_concurrent_custom_aggregator_callback_is_used() -> None: async def summarize(results: list[AgentExecutorResponse]) -> str: texts: list[str] = [] for r in results: - msgs: list[ChatMessage] = r.agent_response.messages + msgs: list[Message] = r.agent_response.messages texts.append(msgs[-1].text if msgs else "") return " | ".join(sorted(texts)) @@ -161,7 +120,7 @@ async def test_concurrent_custom_aggregator_sync_callback_is_used() -> None: def summarize_sync(results: list[AgentExecutorResponse], _ctx: WorkflowContext[Any]) -> str: # type: ignore[unused-argument] texts: list[str] = [] for r in results: - msgs: list[ChatMessage] = r.agent_response.messages + msgs: list[Message] = r.agent_response.messages texts.append(msgs[-1].text if msgs else "") return " | ".join(sorted(texts)) @@ -205,7 +164,7 @@ async def test_concurrent_with_aggregator_executor_instance() -> None: async def aggregate(self, results: list[AgentExecutorResponse], ctx: WorkflowContext[Never, str]) -> None: texts: list[str] = [] for r in results: - msgs: list[ChatMessage] = r.agent_response.messages + msgs: list[Message] = r.agent_response.messages texts.append(msgs[-1].text if msgs else "") await ctx.yield_output(" & ".join(sorted(texts))) @@ -231,79 +190,6 @@ async def test_concurrent_with_aggregator_executor_instance() -> None: assert output == "One & Two" -async def test_concurrent_with_aggregator_executor_factory() -> None: - """Test with_aggregator using an Executor factory.""" - - class CustomAggregator(Executor): - @handler - async def aggregate(self, results: list[AgentExecutorResponse], ctx: WorkflowContext[Never, str]) -> None: - texts: list[str] = [] - for r in results: - msgs: list[ChatMessage] = r.agent_response.messages - texts.append(msgs[-1].text if msgs else "") - await ctx.yield_output(" | ".join(sorted(texts))) - - e1 = _FakeAgentExec("agentA", "One") - e2 = _FakeAgentExec("agentB", "Two") - - wf = ( - ConcurrentBuilder(participants=[e1, e2]) - .register_aggregator(lambda: CustomAggregator(id="custom_aggregator")) - .build() - ) - - completed = False - output: str | None = None - async for ev in wf.run("prompt: factory test", stream=True): - if ev.type == "status" and ev.state == WorkflowRunState.IDLE: - completed = True - elif ev.type == "output": - output = cast(str, ev.data) - if completed and output is not None: - break - - assert completed - assert output is not None - assert isinstance(output, str) - assert output == "One | Two" - - -async def test_concurrent_with_aggregator_executor_factory_with_default_id() -> None: - """Test with_aggregator using an Executor class directly as factory (with default __init__ parameters).""" - - class CustomAggregator(Executor): - def __init__(self, id: str = "default_aggregator") -> None: - super().__init__(id) - - @handler - async def aggregate(self, results: list[AgentExecutorResponse], ctx: WorkflowContext[Never, str]) -> None: - texts: list[str] = [] - for r in results: - msgs: list[ChatMessage] = r.agent_response.messages - texts.append(msgs[-1].text if msgs else "") - await ctx.yield_output(" | ".join(sorted(texts))) - - e1 = _FakeAgentExec("agentA", "One") - e2 = _FakeAgentExec("agentB", "Two") - - wf = ConcurrentBuilder(participants=[e1, e2]).register_aggregator(CustomAggregator).build() - - completed = False - output: str | None = None - async for ev in wf.run("prompt: factory test", stream=True): - if ev.type == "status" and ev.state == WorkflowRunState.IDLE: - completed = True - elif ev.type == "output": - output = cast(str, ev.data) - if completed and output is not None: - break - - assert completed - assert output is not None - assert isinstance(output, str) - assert output == "One | Two" - - def test_concurrent_builder_rejects_multiple_calls_to_with_aggregator() -> None: """Test that multiple calls to .with_aggregator() raises an error.""" @@ -318,20 +204,6 @@ def test_concurrent_builder_rejects_multiple_calls_to_with_aggregator() -> None: ) -def test_concurrent_builder_rejects_multiple_calls_to_register_aggregator() -> None: - """Test that multiple calls to .register_aggregator() raises an error.""" - - class CustomAggregator(Executor): - pass - - with pytest.raises(ValueError, match=r"register_aggregator\(\) has already been called"): - ( - ConcurrentBuilder(participants=[_FakeAgentExec("a", "A")]) - .register_aggregator(lambda: CustomAggregator(id="agg1")) - .register_aggregator(lambda: CustomAggregator(id="agg2")) - ) - - async def test_concurrent_checkpoint_resume_round_trip() -> None: storage = InMemoryCheckpointStorage() @@ -343,7 +215,7 @@ async def test_concurrent_checkpoint_resume_round_trip() -> None: wf = ConcurrentBuilder(participants=list(participants), checkpoint_storage=storage).build() - baseline_output: list[ChatMessage] | None = None + baseline_output: list[Message] | None = None async for ev in wf.run("checkpoint concurrent", stream=True): if ev.type == "output": baseline_output = ev.data # type: ignore[assignment] @@ -367,7 +239,7 @@ async def test_concurrent_checkpoint_resume_round_trip() -> None: ) wf_resume = ConcurrentBuilder(participants=list(resumed_participants), checkpoint_storage=storage).build() - resumed_output: list[ChatMessage] | None = None + resumed_output: list[Message] | None = None async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True): if ev.type == "output": resumed_output = ev.data # type: ignore[assignment] @@ -389,7 +261,7 @@ async def test_concurrent_checkpoint_runtime_only() -> None: agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")] wf = ConcurrentBuilder(participants=agents).build() - baseline_output: list[ChatMessage] | None = None + baseline_output: list[Message] | None = None async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True): if ev.type == "output": baseline_output = ev.data # type: ignore[assignment] @@ -410,7 +282,7 @@ async def test_concurrent_checkpoint_runtime_only() -> None: resumed_agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")] wf_resume = ConcurrentBuilder(participants=resumed_agents).build() - resumed_output: list[ChatMessage] | None = None + resumed_output: list[Message] | None = None async for ev in wf_resume.run( checkpoint_id=resume_checkpoint.checkpoint_id, checkpoint_storage=storage, stream=True ): @@ -439,7 +311,7 @@ async def test_concurrent_checkpoint_runtime_overrides_buildtime() -> None: agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")] wf = ConcurrentBuilder(participants=agents, checkpoint_storage=buildtime_storage).build() - baseline_output: list[ChatMessage] | None = None + baseline_output: list[Message] | None = None async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True): if ev.type == "output": baseline_output = ev.data # type: ignore[assignment] @@ -455,11 +327,6 @@ async def test_concurrent_checkpoint_runtime_overrides_buildtime() -> None: assert len(buildtime_checkpoints) == 0, "Build-time storage should have no checkpoints when overridden" -def test_concurrent_builder_rejects_empty_participant_factories() -> None: - with pytest.raises(ValueError): - ConcurrentBuilder(participant_factories=[]) - - async def test_concurrent_builder_reusable_after_build_with_participants() -> None: """Test that the builder can be reused to build multiple identical workflows with participants().""" e1 = _FakeAgentExec("agentA", "One") @@ -471,74 +338,3 @@ async def test_concurrent_builder_reusable_after_build_with_participants() -> No assert builder._participants[0] is e1 # type: ignore assert builder._participants[1] is e2 # type: ignore - assert builder._participant_factories == [] # type: ignore - - -async def test_concurrent_builder_reusable_after_build_with_factories() -> None: - """Test that the builder can be reused to build multiple workflows with register_participants().""" - call_count = 0 - - def create_agent_executor_a() -> Executor: - nonlocal call_count - call_count += 1 - return _FakeAgentExec("agentA", "One") - - def create_agent_executor_b() -> Executor: - nonlocal call_count - call_count += 1 - return _FakeAgentExec("agentB", "Two") - - builder = ConcurrentBuilder(participant_factories=[create_agent_executor_a, create_agent_executor_b]) - - # Build the first workflow - wf1 = builder.build() - - assert builder._participants == [] # type: ignore - assert len(builder._participant_factories) == 2 # type: ignore - assert call_count == 2 - - # Build the second workflow - wf2 = builder.build() - assert call_count == 4 - - # Verify that the two workflows have different executor instances - assert wf1.executors["agentA"] is not wf2.executors["agentA"] - assert wf1.executors["agentB"] is not wf2.executors["agentB"] - - -async def test_concurrent_with_register_participants() -> None: - """Test workflow creation using register_participants with factories.""" - - def create_agent1() -> Executor: - return _FakeAgentExec("agentA", "Alpha") - - def create_agent2() -> Executor: - return _FakeAgentExec("agentB", "Beta") - - def create_agent3() -> Executor: - return _FakeAgentExec("agentC", "Gamma") - - wf = ConcurrentBuilder(participant_factories=[create_agent1, create_agent2, create_agent3]).build() - - completed = False - output: list[ChatMessage] | None = None - async for ev in wf.run("test prompt", stream=True): - if ev.type == "status" and ev.state == WorkflowRunState.IDLE: - completed = True - elif ev.type == "output": - output = cast(list[ChatMessage], ev.data) - if completed and output is not None: - break - - assert completed - assert output is not None - messages: list[ChatMessage] = output - - # Expect one user message + one assistant message per participant - assert len(messages) == 1 + 3 - assert messages[0].role == "user" - assert "test prompt" in messages[0].text - - assistant_texts = {m.text for m in messages[1:]} - assert assistant_texts == {"Alpha", "Beta", "Gamma"} - assert all(m.role == "assistant" for m in messages[1:]) diff --git a/python/packages/orchestrations/tests/test_group_chat.py b/python/packages/orchestrations/tests/test_group_chat.py index 718b8eb3a7..9eb94b19d4 100644 --- a/python/packages/orchestrations/tests/test_group_chat.py +++ b/python/packages/orchestrations/tests/test_group_chat.py @@ -5,16 +5,16 @@ from typing import Any, cast import pytest from agent_framework import ( + Agent, AgentExecutorResponse, AgentResponse, AgentResponseUpdate, AgentThread, BaseAgent, - ChatAgent, - ChatMessage, ChatResponse, ChatResponseUpdate, Content, + Message, WorkflowEvent, WorkflowRunState, ) @@ -38,7 +38,7 @@ class StubAgent(BaseAgent): def run( # type: ignore[override] self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -49,7 +49,7 @@ class StubAgent(BaseAgent): return self._run_impl() async def _run_impl(self) -> AgentResponse: - response = ChatMessage(role="assistant", text=self._reply_text, author_name=self.name) + response = Message(role="assistant", text=self._reply_text, author_name=self.name) return AgentResponse(messages=[response]) async def _run_stream_impl(self) -> AsyncIterable[AgentResponseUpdate]: @@ -69,14 +69,14 @@ class MockChatClient: raise NotImplementedError -class StubManagerAgent(ChatAgent): +class StubManagerAgent(Agent): def __init__(self) -> None: - super().__init__(chat_client=MockChatClient(), name="manager_agent", description="Stub manager") + super().__init__(client=MockChatClient(), name="manager_agent", description="Stub manager") self._call_count = 0 async def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -87,7 +87,7 @@ class StubManagerAgent(ChatAgent): payload = {"terminate": False, "reason": "Selecting agent", "next_speaker": "agent", "final_message": None} return AgentResponse( messages=[ - ChatMessage( + Message( role="assistant", text=( '{"terminate": false, "reason": "Selecting agent", ' @@ -108,7 +108,7 @@ class StubManagerAgent(ChatAgent): } return AgentResponse( messages=[ - ChatMessage( + Message( role="assistant", text=( '{"terminate": true, "reason": "Task complete", ' @@ -143,10 +143,10 @@ class StubMagenticManager(MagenticManagerBase): super().__init__(max_stall_count=3, max_round_count=5) self._round = 0 - async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role="assistant", text="plan", author_name="magentic_manager") + async def plan(self, magentic_context: MagenticContext) -> Message: + return Message(role="assistant", text="plan", author_name="magentic_manager") - async def replan(self, magentic_context: MagenticContext) -> ChatMessage: + async def replan(self, magentic_context: MagenticContext) -> Message: return await self.plan(magentic_context) async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: @@ -169,8 +169,8 @@ class StubMagenticManager(MagenticManagerBase): instruction_or_question=MagenticProgressLedgerItem(reason="", answer=""), ) - async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage(role="assistant", text="final", author_name="magentic_manager") + async def prepare_final_answer(self, magentic_context: MagenticContext) -> Message: + return Message(role="assistant", text="final", author_name="magentic_manager") async def test_group_chat_builder_basic_flow() -> None: @@ -185,12 +185,12 @@ async def test_group_chat_builder_basic_flow() -> None: orchestrator_name="manager", ).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run("coordinate task", stream=True): if event.type == "output": data = event.data if isinstance(data, list): - outputs.append(cast(list[ChatMessage], data)) + outputs.append(cast(list[Message], data)) assert len(outputs) == 1 assert len(outputs[0]) >= 1 @@ -213,8 +213,8 @@ async def test_group_chat_as_agent_accepts_conversation() -> None: agent = workflow.as_agent(name="group-chat-agent") conversation = [ - ChatMessage(role="user", text="kickoff", author_name="user"), - ChatMessage(role="assistant", text="noted", author_name="alpha"), + Message(role="user", text="kickoff", author_name="user"), + Message(role="assistant", text="noted", author_name="alpha"), ] response = await agent.run(conversation) @@ -240,12 +240,9 @@ class TestGroupChatBuilder: builder.build() def test_build_without_participants_raises_error(self) -> None: - """Test that constructing without participants raises ValueError.""" - with pytest.raises( - ValueError, - match=r"Either participants or participant_factories must be provided\.", - ): - GroupChatBuilder() + """Test that constructing with empty participants raises ValueError.""" + with pytest.raises(ValueError): + GroupChatBuilder(participants=[]) def test_duplicate_manager_configuration_raises_error(self) -> None: """Test that configuring multiple orchestrator options raises ValueError.""" @@ -327,12 +324,12 @@ class TestGroupChatWorkflow: selection_func=selector, ).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run("test task", stream=True): if event.type == "output": data = event.data if isinstance(data, list): - outputs.append(cast(list[ChatMessage], data)) + outputs.append(cast(list[Message], data)) # Should have terminated due to max_rounds, expect at least one output assert len(outputs) >= 1 @@ -348,7 +345,7 @@ class TestGroupChatWorkflow: def selector(state: GroupChatState) -> str: return "agent" - def termination_condition(conversation: list[ChatMessage]) -> bool: + def termination_condition(conversation: list[Message]) -> bool: replies = [msg for msg in conversation if msg.role == "assistant" and msg.author_name == "agent"] return len(replies) >= 2 @@ -360,12 +357,12 @@ class TestGroupChatWorkflow: selection_func=selector, ).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run("test task", stream=True): if event.type == "output": data = event.data if isinstance(data, list): - outputs.append(cast(list[ChatMessage], data)) + outputs.append(cast(list[Message], data)) assert outputs, "Expected termination to yield output" conversation = outputs[-1] @@ -386,12 +383,12 @@ class TestGroupChatWorkflow: orchestrator_agent=manager, ).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run("test task", stream=True): if event.type == "output": data = event.data if isinstance(data, list): - outputs.append(cast(list[ChatMessage], data)) + outputs.append(cast(list[Message], data)) assert outputs, "Expected termination to yield output" conversation = outputs[-1] @@ -432,12 +429,12 @@ class TestCheckpointing: selection_func=selector, ).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run("test task", stream=True): if event.type == "output": data = event.data if isinstance(data, list): - outputs.append(cast(list[ChatMessage], data)) + outputs.append(cast(list[Message], data)) assert len(outputs) == 1 # Should complete normally @@ -455,12 +452,12 @@ class TestConversationHandling: workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build() - with pytest.raises(ValueError, match="At least one ChatMessage is required to start the group chat workflow."): + with pytest.raises(ValueError, match="At least one Message is required to start the group chat workflow."): async for _ in workflow.run([], stream=True): pass async def test_handle_string_input(self) -> None: - """Test handling string input creates proper ChatMessage.""" + """Test handling string input creates proper Message.""" def selector(state: GroupChatState) -> str: # Verify the conversation has the user message @@ -473,18 +470,18 @@ class TestConversationHandling: workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run("test string", stream=True): if event.type == "output": data = event.data if isinstance(data, list): - outputs.append(cast(list[ChatMessage], data)) + outputs.append(cast(list[Message], data)) assert len(outputs) == 1 async def test_handle_chat_message_input(self) -> None: - """Test handling ChatMessage input directly.""" - task_message = ChatMessage(role="user", text="test message") + """Test handling Message input directly.""" + task_message = Message(role="user", text="test message") def selector(state: GroupChatState) -> str: # Verify the task message was preserved in conversation @@ -496,20 +493,20 @@ class TestConversationHandling: workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run(task_message, stream=True): if event.type == "output": data = event.data if isinstance(data, list): - outputs.append(cast(list[ChatMessage], data)) + outputs.append(cast(list[Message], data)) assert len(outputs) == 1 async def test_handle_conversation_list_input(self) -> None: """Test handling conversation list preserves context.""" conversation = [ - ChatMessage(role="system", text="system message"), - ChatMessage(role="user", text="user message"), + Message(role="system", text="system message"), + Message(role="user", text="user message"), ] def selector(state: GroupChatState) -> str: @@ -522,12 +519,12 @@ class TestConversationHandling: workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run(conversation, stream=True): if event.type == "output": data = event.data if isinstance(data, list): - outputs.append(cast(list[ChatMessage], data)) + outputs.append(cast(list[Message], data)) assert len(outputs) == 1 @@ -552,12 +549,12 @@ class TestRoundLimitEnforcement: selection_func=selector, ).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run("test", stream=True): if event.type == "output": data = event.data if isinstance(data, list): - outputs.append(cast(list[ChatMessage], data)) + outputs.append(cast(list[Message], data)) # Should have at least one output (the round limit message) assert len(outputs) >= 1 @@ -585,12 +582,12 @@ class TestRoundLimitEnforcement: selection_func=selector, ).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run("test", stream=True): if event.type == "output": data = event.data if isinstance(data, list): - outputs.append(cast(list[ChatMessage], data)) + outputs.append(cast(list[Message], data)) # Should have at least one output (the round limit message) assert len(outputs) >= 1 @@ -611,10 +608,10 @@ async def test_group_chat_checkpoint_runtime_only() -> None: wf = GroupChatBuilder(participants=[agent_a, agent_b], max_rounds=2, selection_func=selector).build() - baseline_output: list[ChatMessage] | None = None + baseline_output: list[Message] | None = None async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True): if ev.type == "output": - baseline_output = cast(list[ChatMessage], ev.data) if isinstance(ev.data, list) else None # type: ignore + baseline_output = cast(list[Message], ev.data) if isinstance(ev.data, list) else None # type: ignore if ev.type == "status" and ev.state in ( WorkflowRunState.IDLE, WorkflowRunState.IDLE_WITH_PENDING_REQUESTS, @@ -647,10 +644,10 @@ async def test_group_chat_checkpoint_runtime_overrides_buildtime() -> None: checkpoint_storage=buildtime_storage, selection_func=selector, ).build() - baseline_output: list[ChatMessage] | None = None + baseline_output: list[Message] | None = None async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True): if ev.type == "output": - baseline_output = cast(list[ChatMessage], ev.data) if isinstance(ev.data, list) else None # type: ignore + baseline_output = cast(list[Message], ev.data) if isinstance(ev.data, list) else None # type: ignore if ev.type == "status" and ev.state in ( WorkflowRunState.IDLE, WorkflowRunState.IDLE_WITH_PENDING_REQUESTS, @@ -775,150 +772,6 @@ def test_group_chat_builder_with_request_info_returns_self(): assert result2 is builder2 -# region Participant Factory Tests - - -def test_group_chat_builder_rejects_empty_participant_factories(): - """Test that GroupChatBuilder rejects empty participant_factories list.""" - - def selector(state: GroupChatState) -> str: - return list(state.participants.keys())[0] - - with pytest.raises(ValueError, match=r"participant_factories cannot be empty"): - GroupChatBuilder(participant_factories=[]) - - with pytest.raises( - ValueError, - match=r"Either participants or participant_factories must be provided\.", - ): - GroupChatBuilder() - - -def test_group_chat_builder_rejects_mixing_participants_and_factories(): - """Test that passing both participants and participant_factories to the constructor raises an error.""" - alpha = StubAgent("alpha", "reply from alpha") - - with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"): - GroupChatBuilder( - participants=[alpha], - participant_factories=[lambda: StubAgent("beta", "reply from beta")], - ) - - -def test_group_chat_builder_rejects_both_factories_and_participants(): - """Test that passing both participant_factories and participants raises an error.""" - with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"): - GroupChatBuilder( - participant_factories=[lambda: StubAgent("alpha", "reply from alpha")], - participants=[StubAgent("beta", "reply from beta")], - ) - - -def test_group_chat_builder_rejects_both_participants_and_factories(): - """Test that passing both participants and participant_factories raises an error.""" - with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"): - GroupChatBuilder( - participants=[StubAgent("alpha", "reply from alpha")], - participant_factories=[lambda: StubAgent("beta", "reply from beta")], - ) - - -async def test_group_chat_with_participant_factories(): - """Test workflow creation using participant_factories.""" - call_count = 0 - - def create_alpha() -> StubAgent: - nonlocal call_count - call_count += 1 - return StubAgent("alpha", "reply from alpha") - - def create_beta() -> StubAgent: - nonlocal call_count - call_count += 1 - return StubAgent("beta", "reply from beta") - - selector = make_sequence_selector() - - workflow = GroupChatBuilder( - participant_factories=[create_alpha, create_beta], - max_rounds=2, - selection_func=selector, - ).build() - - # Factories should be called during build - assert call_count == 2 - - outputs: list[WorkflowEvent] = [] - async for event in workflow.run("coordinate task", stream=True): - if event.type == "output": - outputs.append(event) - - assert len(outputs) == 1 - - -async def test_group_chat_participant_factories_reusable_builder(): - """Test that the builder can be reused to build multiple workflows with factories.""" - call_count = 0 - - def create_alpha() -> StubAgent: - nonlocal call_count - call_count += 1 - return StubAgent("alpha", "reply from alpha") - - def create_beta() -> StubAgent: - nonlocal call_count - call_count += 1 - return StubAgent("beta", "reply from beta") - - selector = make_sequence_selector() - - builder = GroupChatBuilder(participant_factories=[create_alpha, create_beta], max_rounds=2, selection_func=selector) - - # Build first workflow - wf1 = builder.build() - assert call_count == 2 - - # Build second workflow - wf2 = builder.build() - assert call_count == 4 - - # Verify that the two workflows have different agent instances - assert wf1.executors["alpha"] is not wf2.executors["alpha"] - assert wf1.executors["beta"] is not wf2.executors["beta"] - - -async def test_group_chat_participant_factories_with_checkpointing(): - """Test checkpointing with participant_factories.""" - storage = InMemoryCheckpointStorage() - - def create_alpha() -> StubAgent: - return StubAgent("alpha", "reply from alpha") - - def create_beta() -> StubAgent: - return StubAgent("beta", "reply from beta") - - selector = make_sequence_selector() - - workflow = GroupChatBuilder( - participant_factories=[create_alpha, create_beta], - checkpoint_storage=storage, - max_rounds=2, - selection_func=selector, - ).build() - - outputs: list[WorkflowEvent] = [] - async for event in workflow.run("checkpoint test", stream=True): - if event.type == "output": - outputs.append(event) - - assert outputs, "Should have workflow output" - - checkpoints = await storage.list_checkpoints() - assert checkpoints, "Checkpoints should be created during workflow execution" - - -# endregion - # region Orchestrator Factory Tests @@ -928,8 +781,8 @@ def test_group_chat_builder_rejects_multiple_orchestrator_configurations(): def selector(state: GroupChatState) -> str: return list(state.participants.keys())[0] - def agent_factory() -> ChatAgent: - return cast(ChatAgent, StubManagerAgent()) + def agent_factory() -> Agent: + return cast(Agent, StubManagerAgent()) agent = StubAgent("test", "response") @@ -948,8 +801,8 @@ def test_group_chat_builder_requires_exactly_one_orchestrator_option(): def selector(state: GroupChatState) -> str: return list(state.participants.keys())[0] - def agent_factory() -> ChatAgent: - return cast(ChatAgent, StubManagerAgent()) + def agent_factory() -> Agent: + return cast(Agent, StubManagerAgent()) agent = StubAgent("test", "response") @@ -963,19 +816,19 @@ def test_group_chat_builder_requires_exactly_one_orchestrator_option(): async def test_group_chat_with_orchestrator_factory_returning_chat_agent(): - """Test workflow creation using orchestrator_factory that returns ChatAgent.""" + """Test workflow creation using orchestrator_factory that returns Agent.""" factory_call_count = 0 - class DynamicManagerAgent(ChatAgent): + class DynamicManagerAgent(Agent): """Manager agent that dynamically selects from available participants.""" def __init__(self) -> None: - super().__init__(chat_client=MockChatClient(), name="dynamic_manager", description="Dynamic manager") + super().__init__(client=MockChatClient(), name="dynamic_manager", description="Dynamic manager") self._call_count = 0 async def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -990,7 +843,7 @@ async def test_group_chat_with_orchestrator_factory_returning_chat_agent(): } return AgentResponse( messages=[ - ChatMessage( + Message( role="assistant", text=( '{"terminate": false, "reason": "Selecting alpha", ' @@ -1010,7 +863,7 @@ async def test_group_chat_with_orchestrator_factory_returning_chat_agent(): } return AgentResponse( messages=[ - ChatMessage( + Message( role="assistant", text=( '{"terminate": true, "reason": "Task complete", ' @@ -1022,10 +875,10 @@ async def test_group_chat_with_orchestrator_factory_returning_chat_agent(): value=payload, ) - def agent_factory() -> ChatAgent: + def agent_factory() -> Agent: nonlocal factory_call_count factory_call_count += 1 - return cast(ChatAgent, DynamicManagerAgent()) + return cast(Agent, DynamicManagerAgent()) alpha = StubAgent("alpha", "reply from alpha") beta = StubAgent("beta", "reply from beta") @@ -1046,7 +899,7 @@ async def test_group_chat_with_orchestrator_factory_returning_chat_agent(): assert isinstance(final_messages, list) assert any( msg.text == "dynamic manager final" - for msg in cast(list[ChatMessage], final_messages) + for msg in cast(list[Message], final_messages) if msg.author_name == "dynamic_manager" ) @@ -1086,10 +939,10 @@ async def test_group_chat_orchestrator_factory_reusable_builder(): """Test that the builder can be reused to build multiple workflows with orchestrator factory.""" factory_call_count = 0 - def agent_factory() -> ChatAgent: + def agent_factory() -> Agent: nonlocal factory_call_count factory_call_count += 1 - return cast(ChatAgent, StubManagerAgent()) + return cast(Agent, StubManagerAgent()) alpha = StubAgent("alpha", "reply from alpha") beta = StubAgent("beta", "reply from beta") @@ -1118,88 +971,15 @@ def test_group_chat_orchestrator_factory_invalid_return_type(): with pytest.raises( TypeError, - match=r"Orchestrator factory must return ChatAgent or BaseGroupChatOrchestrator instance", + match=r"Orchestrator factory must return Agent or BaseGroupChatOrchestrator instance", ): GroupChatBuilder(participants=[alpha], orchestrator=invalid_factory).build() with pytest.raises( TypeError, - match=r"Orchestrator factory must return ChatAgent or BaseGroupChatOrchestrator instance", + match=r"Orchestrator factory must return Agent or BaseGroupChatOrchestrator instance", ): GroupChatBuilder(participants=[alpha], orchestrator_agent=invalid_factory).build() -def test_group_chat_with_both_participant_and_orchestrator_factories(): - """Test workflow creation using both participant_factories and orchestrator_factory.""" - participant_factory_call_count = 0 - agent_factory_call_count = 0 - - def create_alpha() -> StubAgent: - nonlocal participant_factory_call_count - participant_factory_call_count += 1 - return StubAgent("alpha", "reply from alpha") - - def create_beta() -> StubAgent: - nonlocal participant_factory_call_count - participant_factory_call_count += 1 - return StubAgent("beta", "reply from beta") - - def agent_factory() -> ChatAgent: - nonlocal agent_factory_call_count - agent_factory_call_count += 1 - return cast(ChatAgent, StubManagerAgent()) - - workflow = GroupChatBuilder( - participant_factories=[create_alpha, create_beta], - orchestrator_agent=agent_factory, - ).build() - - # All factories should be called during build - assert participant_factory_call_count == 2 - assert agent_factory_call_count == 1 - - # Verify all executors are present in the workflow - assert "alpha" in workflow.executors - assert "beta" in workflow.executors - assert "manager_agent" in workflow.executors - - -async def test_group_chat_factories_reusable_for_multiple_workflows(): - """Test that both factories are reused correctly for multiple workflow builds.""" - participant_factory_call_count = 0 - agent_factory_call_count = 0 - - def create_alpha() -> StubAgent: - nonlocal participant_factory_call_count - participant_factory_call_count += 1 - return StubAgent("alpha", "reply from alpha") - - def create_beta() -> StubAgent: - nonlocal participant_factory_call_count - participant_factory_call_count += 1 - return StubAgent("beta", "reply from beta") - - def agent_factory() -> ChatAgent: - nonlocal agent_factory_call_count - agent_factory_call_count += 1 - return cast(ChatAgent, StubManagerAgent()) - - builder = GroupChatBuilder(participant_factories=[create_alpha, create_beta], orchestrator_agent=agent_factory) - - # Build first workflow - wf1 = builder.build() - assert participant_factory_call_count == 2 - assert agent_factory_call_count == 1 - - # Build second workflow - wf2 = builder.build() - assert participant_factory_call_count == 4 - assert agent_factory_call_count == 2 - - # Verify that the workflows have different agent and orchestrator instances - assert wf1.executors["alpha"] is not wf2.executors["alpha"] - assert wf1.executors["beta"] is not wf2.executors["beta"] - assert wf1.executors["manager_agent"] is not wf2.executors["manager_agent"] - - # endregion diff --git a/python/packages/orchestrations/tests/test_handoff.py b/python/packages/orchestrations/tests/test_handoff.py index 7b382d3511..38ff6ea49a 100644 --- a/python/packages/orchestrations/tests/test_handoff.py +++ b/python/packages/orchestrations/tests/test_handoff.py @@ -6,13 +6,13 @@ from unittest.mock import AsyncMock, MagicMock import pytest from agent_framework import ( - ChatAgent, - ChatMessage, + Agent, ChatResponse, ChatResponseUpdate, Content, Context, ContextProvider, + Message, ResponseStream, WorkflowEvent, resolve_agent_id, @@ -50,7 +50,7 @@ class MockChatClient(ChatMiddlewareLayer[Any], FunctionInvocationLayer[Any], Bas def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], stream: bool, options: Mapping[str, Any], **kwargs: Any, @@ -60,7 +60,7 @@ class MockChatClient(ChatMiddlewareLayer[Any], FunctionInvocationLayer[Any], Bas async def _get() -> ChatResponse: contents = _build_reply_contents(self._name, self._handoff_to, self._next_call_id()) - reply = ChatMessage( + reply = Message( role="assistant", contents=contents, ) @@ -105,7 +105,7 @@ def _build_reply_contents( return contents -class MockHandoffAgent(ChatAgent): +class MockHandoffAgent(Agent): """Mock agent that can hand off to another agent.""" def __init__( @@ -121,7 +121,7 @@ class MockHandoffAgent(ChatAgent): handoff_to: The name of the agent to hand off to, or None for no handoff. This is hardcoded for testing purposes so that the agent always attempts to hand off. """ - super().__init__(chat_client=MockChatClient(name=name, handoff_to=handoff_to), name=name, id=name) + super().__init__(client=MockChatClient(name=name, handoff_to=handoff_to), name=name, id=name) async def _drain(stream: AsyncIterable[WorkflowEvent]) -> list[WorkflowEvent]: @@ -196,7 +196,7 @@ async def test_autonomous_mode_yields_output_without_user_request(): final_conversation = outputs[-1].data assert isinstance(final_conversation, list) - conversation_list = cast(list[ChatMessage], final_conversation) + conversation_list = cast(list[Message], final_conversation) assert any(msg.role == "assistant" and (msg.text or "").startswith("specialist reply") for msg in conversation_list) @@ -229,17 +229,15 @@ def test_build_fails_without_start_agent(): def test_build_fails_without_participants(): """Verify that build() raises ValueError when no participants are provided.""" - with pytest.raises( - ValueError, match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first." - ): - HandoffBuilder().build() + with pytest.raises(ValueError): + HandoffBuilder(participants=[]).build() async def test_handoff_async_termination_condition() -> None: """Test that async termination conditions work correctly.""" termination_call_count = 0 - async def async_termination(conv: list[ChatMessage]) -> bool: + async def async_termination(conv: list[Message]) -> bool: nonlocal termination_call_count termination_call_count += 1 user_count = sum(1 for msg in conv if msg.role == "user") @@ -260,7 +258,7 @@ async def test_handoff_async_termination_condition() -> None: events = await _drain( workflow.run( - stream=True, responses={requests[-1].request_id: [ChatMessage(role="user", text="Second user message")]} + stream=True, responses={requests[-1].request_id: [Message(role="user", text="Second user message")]} ) ) outputs = [ev for ev in events if ev.type == "output"] @@ -268,7 +266,7 @@ async def test_handoff_async_termination_condition() -> None: final_conversation = outputs[0].data assert isinstance(final_conversation, list) - final_conv_list = cast(list[ChatMessage], final_conversation) + final_conv_list = cast(list[Message], final_conversation) user_messages = [msg for msg in final_conv_list if msg.role == "user"] assert len(user_messages) == 2 assert termination_call_count > 0 @@ -283,7 +281,7 @@ async def test_tool_choice_preserved_from_agent_config(): if options: recorded_tool_choices.append(options.get("tool_choice")) return ChatResponse( - messages=[ChatMessage(role="assistant", text="Response")], + messages=[Message(role="assistant", text="Response")], response_id="test_response", ) @@ -291,8 +289,8 @@ async def test_tool_choice_preserved_from_agent_config(): mock_client.get_response = AsyncMock(side_effect=mock_get_response) # Create agent with specific tool_choice configuration via default_options - agent = ChatAgent( - chat_client=mock_client, + agent = Agent( + client=mock_client, name="test_agent", default_options={"tool_choice": {"mode": "required"}}, # type: ignore ) @@ -315,7 +313,7 @@ async def test_context_provider_preserved_during_handoff(): class TestContextProvider(ContextProvider): """A test context provider that tracks its invocations.""" - async def invoking(self, messages: Sequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Sequence[Message], **kwargs: Any) -> Context: provider_calls.append("invoking") return Context(instructions="Test context from provider.") @@ -326,8 +324,8 @@ async def test_context_provider_preserved_during_handoff(): mock_client = MockChatClient(name="test_agent") # Create agent with context provider using proper constructor - agent = ChatAgent( - chat_client=mock_client, + agent = Agent( + client=mock_client, name="test_agent", id="test_agent", context_provider=context_provider, @@ -349,162 +347,6 @@ async def test_context_provider_preserved_during_handoff(): ) -# region Participant Factory Tests - - -def test_handoff_builder_rejects_empty_participant_factories(): - """Test that HandoffBuilder rejects empty participant_factories dictionary.""" - # Empty factories are rejected immediately when calling participant_factories() - with pytest.raises(ValueError, match=r"participant_factories cannot be empty"): - HandoffBuilder().register_participants({}) - - with pytest.raises( - ValueError, match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\." - ): - HandoffBuilder(participant_factories={}).build() - - -def test_handoff_builder_rejects_mixing_participants_and_factories(): - """Test that mixing participants and participant_factories in __init__ raises an error.""" - triage = MockHandoffAgent(name="triage") - with pytest.raises(ValueError, match="Cannot mix .participants"): - HandoffBuilder(participants=[triage], participant_factories={"triage": lambda: triage}) - - -def test_handoff_builder_rejects_mixing_participants_and_participant_factories_methods(): - """Test that mixing .participants() and .participant_factories() raises an error.""" - triage = MockHandoffAgent(name="triage") - - # Case 1: participants first, then participant_factories - with pytest.raises(ValueError, match="Cannot mix .participants"): - HandoffBuilder(participants=[triage]).register_participants({ - "specialist": lambda: MockHandoffAgent(name="specialist") - }) - - # Case 2: participant_factories first, then participants - with pytest.raises(ValueError, match="Cannot mix .participants"): - HandoffBuilder(participant_factories={"triage": lambda: triage}).participants([ - MockHandoffAgent(name="specialist") - ]) - - # Case 3: participants(), then participant_factories() - with pytest.raises(ValueError, match="Cannot mix .participants"): - HandoffBuilder().participants([triage]).register_participants({ - "specialist": lambda: MockHandoffAgent(name="specialist") - }) - - # Case 4: participant_factories(), then participants() - with pytest.raises(ValueError, match="Cannot mix .participants"): - HandoffBuilder().register_participants({"triage": lambda: triage}).participants([ - MockHandoffAgent(name="specialist") - ]) - - # Case 5: mix during initialization - with pytest.raises(ValueError, match="Cannot mix .participants"): - HandoffBuilder( - participants=[triage], participant_factories={"specialist": lambda: MockHandoffAgent(name="specialist")} - ) - - -def test_handoff_builder_rejects_multiple_calls_to_participant_factories(): - """Test that multiple calls to .participant_factories() raises an error.""" - with pytest.raises( - ValueError, match=r"register_participants\(\) has already been called on this builder instance." - ): - ( - HandoffBuilder() - .register_participants({"agent1": lambda: MockHandoffAgent(name="agent1")}) - .register_participants({"agent2": lambda: MockHandoffAgent(name="agent2")}) - ) - - -def test_handoff_builder_rejects_multiple_calls_to_participants(): - """Test that multiple calls to .participants() raises an error.""" - with pytest.raises(ValueError, match="participants have already been assigned"): - ( - HandoffBuilder() - .participants([MockHandoffAgent(name="agent1")]) - .participants([MockHandoffAgent(name="agent2")]) - ) - - -def test_handoff_builder_rejects_instance_coordinator_with_factories(): - """Test that using an agent instance for set_coordinator when using factories raises an error.""" - - def create_triage() -> MockHandoffAgent: - return MockHandoffAgent(name="triage") - - def create_specialist() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist") - - # Create an agent instance - coordinator_instance = MockHandoffAgent(name="coordinator") - - with pytest.raises(ValueError, match=r"Call participants\(\.\.\.\) before with_start_agent\(\.\.\.\)"): - ( - HandoffBuilder( - participant_factories={"triage": create_triage, "specialist": create_specialist} - ).with_start_agent(coordinator_instance) # Instance, not factory name - ) - - -def test_handoff_builder_rejects_factory_name_coordinator_with_instances(): - """Test that using a factory name for set_coordinator when using instances raises an error.""" - triage = MockHandoffAgent(name="triage") - specialist = MockHandoffAgent(name="specialist") - - with pytest.raises(ValueError, match=r"Call register_participants\(...\) before with_start_agent\(...\)"): - ( - HandoffBuilder(participants=[triage, specialist]).with_start_agent( - "triage" - ) # String factory name, not instance - ) - - -def test_handoff_builder_rejects_mixed_types_in_add_handoff_source(): - """Test that add_handoff rejects factory name source with instance-based participants.""" - triage = MockHandoffAgent(name="triage") - specialist = MockHandoffAgent(name="specialist") - - with pytest.raises(TypeError, match="Cannot mix factory names \\(str\\) and SupportsAgentRun.*instances"): - ( - HandoffBuilder(participants=[triage, specialist]) - .with_start_agent(triage) - .add_handoff("triage", [specialist]) # String source with instance participants - ) - - -def test_handoff_builder_accepts_all_factory_names_in_add_handoff(): - """Test that add_handoff accepts all factory names when using participant_factories.""" - - def create_triage() -> MockHandoffAgent: - return MockHandoffAgent(name="triage") - - def create_specialist_a() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist_a") - - def create_specialist_b() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist_b") - - # This should work - all strings with participant_factories - builder = ( - HandoffBuilder( - participant_factories={ - "triage": create_triage, - "specialist_a": create_specialist_a, - "specialist_b": create_specialist_b, - } - ) - .with_start_agent("triage") - .add_handoff("triage", ["specialist_a", "specialist_b"]) - ) - - workflow = builder.build() - assert "triage" in workflow.executors - assert "specialist_a" in workflow.executors - assert "specialist_b" in workflow.executors - - def test_handoff_builder_accepts_all_instances_in_add_handoff(): """Test that add_handoff accepts all instances when using participants.""" triage = MockHandoffAgent(name="triage", handoff_to="specialist_a") @@ -522,260 +364,3 @@ def test_handoff_builder_accepts_all_instances_in_add_handoff(): assert "triage" in workflow.executors assert "specialist_a" in workflow.executors assert "specialist_b" in workflow.executors - - -async def test_handoff_with_participant_factories(): - """Test workflow creation using participant_factories.""" - call_count = 0 - - def create_triage() -> MockHandoffAgent: - nonlocal call_count - call_count += 1 - return MockHandoffAgent(name="triage", handoff_to="specialist") - - def create_specialist() -> MockHandoffAgent: - nonlocal call_count - call_count += 1 - return MockHandoffAgent(name="specialist") - - workflow = ( - HandoffBuilder( - participant_factories={"triage": create_triage, "specialist": create_specialist}, - termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2, - ) - .with_start_agent("triage") - .build() - ) - - # Factories should be called during build - assert call_count == 2 - - events = await _drain(workflow.run("Need help", stream=True)) - requests = [ev for ev in events if ev.type == "request_info"] - assert requests - - # Follow-up message - events = await _drain( - workflow.run(stream=True, responses={requests[-1].request_id: [ChatMessage(role="user", text="More details")]}) - ) - outputs = [ev for ev in events if ev.type == "output"] - assert outputs - - -async def test_handoff_participant_factories_reusable_builder(): - """Test that the builder can be reused to build multiple workflows with factories.""" - call_count = 0 - - def create_triage() -> MockHandoffAgent: - nonlocal call_count - call_count += 1 - return MockHandoffAgent(name="triage", handoff_to="specialist") - - def create_specialist() -> MockHandoffAgent: - nonlocal call_count - call_count += 1 - return MockHandoffAgent(name="specialist") - - builder = HandoffBuilder( - participant_factories={"triage": create_triage, "specialist": create_specialist} - ).with_start_agent("triage") - - # Build first workflow - wf1 = builder.build() - assert call_count == 2 - - # Build second workflow - wf2 = builder.build() - assert call_count == 4 - - # Verify that the two workflows have different agent instances - assert wf1.executors["triage"] is not wf2.executors["triage"] - assert wf1.executors["specialist"] is not wf2.executors["specialist"] - - -async def test_handoff_with_participant_factories_and_add_handoff(): - """Test that .add_handoff() works correctly with participant_factories.""" - - def create_triage() -> MockHandoffAgent: - return MockHandoffAgent(name="triage", handoff_to="specialist_a") - - def create_specialist_a() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist_a", handoff_to="specialist_b") - - def create_specialist_b() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist_b") - - workflow = ( - HandoffBuilder( - participant_factories={ - "triage": create_triage, - "specialist_a": create_specialist_a, - "specialist_b": create_specialist_b, - }, - termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 3, - ) - .with_start_agent("triage") - .add_handoff("triage", ["specialist_a", "specialist_b"]) - .add_handoff("specialist_a", ["specialist_b"]) - .build() - ) - - # Start conversation - triage hands off to specialist_a - events = await _drain(workflow.run("Initial request", stream=True)) - requests = [ev for ev in events if ev.type == "request_info"] - assert requests - - # Verify specialist_a executor exists and was called - assert "specialist_a" in workflow.executors - - # Second user message - specialist_a hands off to specialist_b - events = await _drain( - workflow.run( - stream=True, responses={requests[-1].request_id: [ChatMessage(role="user", text="Need escalation")]} - ) - ) - requests = [ev for ev in events if ev.type == "request_info"] - assert requests - - # Verify specialist_b executor exists - assert "specialist_b" in workflow.executors - - -async def test_handoff_participant_factories_with_checkpointing(): - """Test checkpointing with participant_factories.""" - from agent_framework._workflows._checkpoint import InMemoryCheckpointStorage - - storage = InMemoryCheckpointStorage() - - def create_triage() -> MockHandoffAgent: - return MockHandoffAgent(name="triage", handoff_to="specialist") - - def create_specialist() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist") - - workflow = ( - HandoffBuilder( - participant_factories={"triage": create_triage, "specialist": create_specialist}, - checkpoint_storage=storage, - termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2, - ) - .with_start_agent("triage") - .build() - ) - - # Run workflow and capture output - events = await _drain(workflow.run("checkpoint test", stream=True)) - requests = [ev for ev in events if ev.type == "request_info"] - assert requests - - events = await _drain( - workflow.run(stream=True, responses={requests[-1].request_id: [ChatMessage(role="user", text="follow up")]}) - ) - outputs = [ev for ev in events if ev.type == "output"] - assert outputs, "Should have workflow output after termination condition is met" - - # List checkpoints - just verify they were created - checkpoints = await storage.list_checkpoints() - assert checkpoints, "Checkpoints should be created during workflow execution" - - -def test_handoff_set_coordinator_with_factory_name(): - """Test that set_coordinator accepts factory name as string.""" - - def create_triage() -> MockHandoffAgent: - return MockHandoffAgent(name="triage") - - def create_specialist() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist") - - builder = HandoffBuilder( - participant_factories={"triage": create_triage, "specialist": create_specialist} - ).with_start_agent("triage") - - workflow = builder.build() - assert "triage" in workflow.executors - - -def test_handoff_add_handoff_with_factory_names(): - """Test that add_handoff accepts factory names as strings.""" - - def create_triage() -> MockHandoffAgent: - return MockHandoffAgent(name="triage", handoff_to="specialist_a") - - def create_specialist_a() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist_a") - - def create_specialist_b() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist_b") - - builder = ( - HandoffBuilder( - participant_factories={ - "triage": create_triage, - "specialist_a": create_specialist_a, - "specialist_b": create_specialist_b, - } - ) - .with_start_agent("triage") - .add_handoff("triage", ["specialist_a", "specialist_b"]) - ) - - workflow = builder.build() - assert "triage" in workflow.executors - assert "specialist_a" in workflow.executors - assert "specialist_b" in workflow.executors - - -async def test_handoff_participant_factories_autonomous_mode(): - """Test autonomous mode with participant_factories.""" - - def create_triage() -> MockHandoffAgent: - return MockHandoffAgent(name="triage", handoff_to="specialist") - - def create_specialist() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist") - - workflow = ( - HandoffBuilder(participant_factories={"triage": create_triage, "specialist": create_specialist}) - .with_start_agent("triage") - .with_autonomous_mode(agents=["specialist"], turn_limits={"specialist": 1}) - .build() - ) - - events = await _drain(workflow.run("Issue", stream=True)) - requests = [ev for ev in events if ev.type == "request_info"] - assert requests and len(requests) == 1 - assert requests[0].source_executor_id == "specialist" - - -def test_handoff_participant_factories_invalid_coordinator_name(): - """Test that set_coordinator raises error for non-existent factory name.""" - - def create_triage() -> MockHandoffAgent: - return MockHandoffAgent(name="triage") - - with pytest.raises( - ValueError, match="Start agent factory name 'nonexistent' is not in the participant_factories list" - ): - (HandoffBuilder(participant_factories={"triage": create_triage}).with_start_agent("nonexistent").build()) - - -def test_handoff_participant_factories_invalid_handoff_target(): - """Test that add_handoff raises error for non-existent target factory name.""" - - def create_triage() -> MockHandoffAgent: - return MockHandoffAgent(name="triage") - - def create_specialist() -> MockHandoffAgent: - return MockHandoffAgent(name="specialist") - - with pytest.raises(ValueError, match="Target factory name 'nonexistent' is not in the participant_factories list"): - ( - HandoffBuilder(participant_factories={"triage": create_triage, "specialist": create_specialist}) - .with_start_agent("triage") - .add_handoff("triage", ["nonexistent"]) - .build() - ) - - -# endregion Participant Factory Tests diff --git a/python/packages/orchestrations/tests/test_magentic.py b/python/packages/orchestrations/tests/test_magentic.py index 5846b56ae4..b24284f9c3 100644 --- a/python/packages/orchestrations/tests/test_magentic.py +++ b/python/packages/orchestrations/tests/test_magentic.py @@ -11,9 +11,9 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, Executor, + Message, SupportsAgentRun, Workflow, WorkflowCheckpoint, @@ -48,7 +48,7 @@ def test_magentic_context_reset_behavior(): participant_descriptions={"Alice": "Researcher"}, ) # seed context state - ctx.chat_history.append(ChatMessage("assistant", ["draft"])) + ctx.chat_history.append(Message("assistant", ["draft"])) ctx.stall_count = 2 prev_reset = ctx.reset_count @@ -61,8 +61,8 @@ def test_magentic_context_reset_behavior(): @dataclass class _SimpleLedger: - facts: ChatMessage - plan: ChatMessage + facts: Message + plan: Message class FakeManager(MagenticManagerBase): @@ -108,25 +108,25 @@ class FakeManager(MagenticManagerBase): plan_payload = cast(dict[str, Any] | None, ledger_dict.get("plan")) if facts_payload is not None and plan_payload is not None: try: - facts = ChatMessage.from_dict(facts_payload) - plan = ChatMessage.from_dict(plan_payload) + facts = Message.from_dict(facts_payload) + plan = Message.from_dict(plan_payload) self.task_ledger = _SimpleLedger(facts=facts, plan=plan) except Exception: # pragma: no cover - defensive pass - async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - facts = ChatMessage("assistant", ["GIVEN OR VERIFIED FACTS\n- A\n"]) - plan = ChatMessage("assistant", ["- Do X\n- Do Y\n"]) + async def plan(self, magentic_context: MagenticContext) -> Message: + facts = Message("assistant", ["GIVEN OR VERIFIED FACTS\n- A\n"]) + plan = Message("assistant", ["- Do X\n- Do Y\n"]) self.task_ledger = _SimpleLedger(facts=facts, plan=plan) combined = f"Task: {magentic_context.task}\n\nFacts:\n{facts.text}\n\nPlan:\n{plan.text}" - return ChatMessage("assistant", [combined], author_name=self.name) + return Message("assistant", [combined], author_name=self.name) - async def replan(self, magentic_context: MagenticContext) -> ChatMessage: - facts = ChatMessage("assistant", ["GIVEN OR VERIFIED FACTS\n- A2\n"]) - plan = ChatMessage("assistant", ["- Do Z\n"]) + async def replan(self, magentic_context: MagenticContext) -> Message: + facts = Message("assistant", ["GIVEN OR VERIFIED FACTS\n- A2\n"]) + plan = Message("assistant", ["- Do Z\n"]) self.task_ledger = _SimpleLedger(facts=facts, plan=plan) combined = f"Task: {magentic_context.task}\n\nFacts:\n{facts.text}\n\nPlan:\n{plan.text}" - return ChatMessage("assistant", [combined], author_name=self.name) + return Message("assistant", [combined], author_name=self.name) async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: # At least two messages in chat history means request is satisfied for testing @@ -139,8 +139,8 @@ class FakeManager(MagenticManagerBase): instruction_or_question=MagenticProgressLedgerItem(reason="test", answer=self.instruction_text), ) - async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage("assistant", [self.FINAL_ANSWER], author_name=self.name) + async def prepare_final_answer(self, magentic_context: MagenticContext) -> Message: + return Message("assistant", [self.FINAL_ANSWER], author_name=self.name) class StubAgent(BaseAgent): @@ -150,7 +150,7 @@ class StubAgent(BaseAgent): def run( # type: ignore[override] self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -160,7 +160,7 @@ class StubAgent(BaseAgent): return self._run_stream() async def _run() -> AgentResponse: - response = ChatMessage("assistant", [self._reply_text], author_name=self.name) + response = Message("assistant", [self._reply_text], author_name=self.name) return AgentResponse(messages=[response]) return _run() @@ -177,7 +177,7 @@ class DummyExec(Executor): @handler async def _noop( - self, message: GroupChatRequestMessage, ctx: WorkflowContext[ChatMessage] + self, message: GroupChatRequestMessage, ctx: WorkflowContext[Message] ) -> None: # pragma: no cover - not called pass @@ -190,13 +190,13 @@ async def test_magentic_builder_returns_workflow_and_runs() -> None: assert isinstance(workflow, Workflow) - outputs: list[ChatMessage] = [] + outputs: list[Message] = [] orchestrator_event_count = 0 async for event in workflow.run("compose summary", stream=True): if event.type == "output": msg = event.data if isinstance(msg, list): - outputs.extend(cast(list[ChatMessage], msg)) + outputs.extend(cast(list[Message], msg)) elif event.type == "magentic_orchestrator": orchestrator_event_count += 1 @@ -216,8 +216,8 @@ async def test_magentic_as_agent_does_not_accept_conversation() -> None: agent = workflow.as_agent(name="magentic-agent") conversation = [ - ChatMessage("system", ["Guidelines"], author_name="system"), - ChatMessage("user", ["Summarize the findings"], author_name="requester"), + Message("system", ["Guidelines"], author_name="system"), + Message("user", ["Summarize the findings"], author_name="requester"), ] with pytest.raises(ValueError, match="Magentic only support a single task message to start the workflow."): await agent.run(conversation) @@ -250,7 +250,7 @@ async def test_magentic_workflow_plan_review_approval_to_completion(): assert isinstance(req_event.data, MagenticPlanReviewRequest) completed = False - output: list[ChatMessage] | None = None + output: list[Message] | None = None async for ev in wf.run(stream=True, responses={req_event.request_id: req_event.data.approve()}): if ev.type == "status" and ev.state == WorkflowRunState.IDLE: completed = True @@ -262,7 +262,7 @@ async def test_magentic_workflow_plan_review_approval_to_completion(): assert completed assert output is not None assert isinstance(output, list) - assert all(isinstance(msg, ChatMessage) for msg in output) + assert all(isinstance(msg, Message) for msg in output) async def test_magentic_plan_review_with_revise(): @@ -273,7 +273,7 @@ async def test_magentic_plan_review_with_revise(): def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] super().__init__(*args, **kwargs) - async def replan(self, magentic_context: MagenticContext) -> ChatMessage: # type: ignore[override] + async def replan(self, magentic_context: MagenticContext) -> Message: # type: ignore[override] self.replan_count += 1 return await super().replan(magentic_context) @@ -340,7 +340,7 @@ async def test_magentic_orchestrator_round_limit_produces_partial_result(): assert isinstance(data, list) assert len(data) > 0 # type: ignore assert data[-1].role == "assistant" # type: ignore - assert all(isinstance(msg, ChatMessage) for msg in data) # type: ignore + assert all(isinstance(msg, Message) for msg in data) # type: ignore async def test_magentic_checkpoint_resume_round_trip(): @@ -406,7 +406,7 @@ class StubManagerAgent(BaseAgent): def run( self, - messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None, + messages: str | Message | Sequence[str | Message] | None = None, *, stream: bool = False, thread: Any = None, @@ -416,22 +416,22 @@ class StubManagerAgent(BaseAgent): return self._run_stream() async def _run() -> AgentResponse: - return AgentResponse(messages=[ChatMessage("assistant", ["ok"])]) + return AgentResponse(messages=[Message("assistant", ["ok"])]) return _run() async def _run_stream(self) -> AsyncIterable[AgentResponseUpdate]: - yield AgentResponseUpdate(message_deltas=[ChatMessage("assistant", ["ok"])]) + yield AgentResponseUpdate(message_deltas=[Message("assistant", ["ok"])]) async def test_standard_manager_plan_and_replan_via_complete_monkeypatch(): mgr = StandardMagenticManager(StubManagerAgent()) - async def fake_complete_plan(messages: list[ChatMessage], **kwargs: Any) -> ChatMessage: + async def fake_complete_plan(messages: list[Message], **kwargs: Any) -> Message: # Return a different response depending on call order length if any("FACTS" in (m.text or "") for m in messages): - return ChatMessage("assistant", ["- step A\n- step B"]) - return ChatMessage("assistant", ["GIVEN OR VERIFIED FACTS\n- fact1"]) + return Message("assistant", ["- step A\n- step B"]) + return Message("assistant", ["GIVEN OR VERIFIED FACTS\n- fact1"]) # First, patch to produce facts then plan mgr._complete = fake_complete_plan # type: ignore[attr-defined] @@ -444,10 +444,10 @@ async def test_standard_manager_plan_and_replan_via_complete_monkeypatch(): assert any(t in combined.text for t in ("- step A", "- step B", "- step")) # Now replan with new outputs - async def fake_complete_replan(messages: list[ChatMessage], **kwargs: Any) -> ChatMessage: + async def fake_complete_replan(messages: list[Message], **kwargs: Any) -> Message: if any("Please briefly explain" in (m.text or "") for m in messages): - return ChatMessage("assistant", ["- new step"]) - return ChatMessage("assistant", ["GIVEN OR VERIFIED FACTS\n- updated"]) + return Message("assistant", ["- new step"]) + return Message("assistant", ["GIVEN OR VERIFIED FACTS\n- updated"]) mgr._complete = fake_complete_replan # type: ignore[attr-defined] combined2 = await mgr.replan(ctx.clone()) @@ -459,7 +459,7 @@ async def test_standard_manager_progress_ledger_success_and_error(): ctx = MagenticContext(task="task", participant_descriptions={"alice": "desc"}) # Success path: valid JSON - async def fake_complete_ok(messages: list[ChatMessage], **kwargs: Any) -> ChatMessage: + async def fake_complete_ok(messages: list[Message], **kwargs: Any) -> Message: json_text = ( '{"is_request_satisfied": {"reason": "r", "answer": false}, ' '"is_in_loop": {"reason": "r", "answer": false}, ' @@ -467,15 +467,15 @@ async def test_standard_manager_progress_ledger_success_and_error(): '"next_speaker": {"reason": "r", "answer": "alice"}, ' '"instruction_or_question": {"reason": "r", "answer": "do"}}' ) - return ChatMessage("assistant", [json_text]) + return Message("assistant", [json_text]) mgr._complete = fake_complete_ok # type: ignore[attr-defined] ledger = await mgr.create_progress_ledger(ctx.clone()) assert ledger.next_speaker.answer == "alice" # Error path: invalid JSON now raises to avoid emitting planner-oriented instructions to agents - async def fake_complete_bad(messages: list[ChatMessage], **kwargs: Any) -> ChatMessage: - return ChatMessage("assistant", ["not-json"]) + async def fake_complete_bad(messages: list[Message], **kwargs: Any) -> Message: + return Message("assistant", ["not-json"]) mgr._complete = fake_complete_bad # type: ignore[attr-defined] with pytest.raises(RuntimeError): @@ -487,11 +487,11 @@ class InvokeOnceManager(MagenticManagerBase): super().__init__(max_round_count=5, max_stall_count=3, max_reset_count=2) self._invoked = False - async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage("assistant", ["ledger"]) + async def plan(self, magentic_context: MagenticContext) -> Message: + return Message("assistant", ["ledger"]) - async def replan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage("assistant", ["re-ledger"]) + async def replan(self, magentic_context: MagenticContext) -> Message: + return Message("assistant", ["re-ledger"]) async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: if not self._invoked: @@ -513,8 +513,8 @@ class InvokeOnceManager(MagenticManagerBase): instruction_or_question=MagenticProgressLedgerItem(reason="r", answer="done"), ) - async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage("assistant", ["final"]) + async def prepare_final_answer(self, magentic_context: MagenticContext) -> Message: + return Message("assistant", ["final"]) class StubThreadAgent(BaseAgent): @@ -526,7 +526,7 @@ class StubThreadAgent(BaseAgent): return self._run_stream() async def _run(): - return AgentResponse(messages=[ChatMessage("assistant", ["thread-ok"], author_name=self.name)]) + return AgentResponse(messages=[Message("assistant", ["thread-ok"], author_name=self.name)]) return _run() @@ -543,18 +543,18 @@ class StubAssistantsClient: class StubAssistantsAgent(BaseAgent): - chat_client: object | None = None # allow assignment via Pydantic field + client: object | None = None # allow assignment via Pydantic field def __init__(self) -> None: super().__init__(name="agentA") - self.chat_client = StubAssistantsClient() # type name contains 'AssistantsClient' + self.client = StubAssistantsClient() # type name contains 'AssistantsClient' def run(self, messages=None, *, stream: bool = False, thread=None, **kwargs): # type: ignore[override] if stream: return self._run_stream() async def _run(): - return AgentResponse(messages=[ChatMessage("assistant", ["assistants-ok"], author_name=self.name)]) + return AgentResponse(messages=[Message("assistant", ["assistants-ok"], author_name=self.name)]) return _run() @@ -566,8 +566,8 @@ class StubAssistantsAgent(BaseAgent): ) -async def _collect_agent_responses_setup(participant: SupportsAgentRun) -> list[ChatMessage]: - captured: list[ChatMessage] = [] +async def _collect_agent_responses_setup(participant: SupportsAgentRun) -> list[Message]: + captured: list[Message] = [] wf = MagenticBuilder(participants=[participant], intermediate_outputs=True, manager=InvokeOnceManager()).build() @@ -578,7 +578,7 @@ async def _collect_agent_responses_setup(participant: SupportsAgentRun) -> list[ # Capture streaming updates (type="output" with AgentResponseUpdate data) if ev.type == "output" and isinstance(ev.data, AgentResponseUpdate): captured.append( - ChatMessage( + Message( role=ev.data.role or "assistant", text=ev.data.text or "", author_name=ev.data.author_name, @@ -711,11 +711,11 @@ class NotProgressingManager(MagenticManagerBase): A manager that never marks progress being made, to test stall/reset limits. """ - async def plan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage("assistant", ["ledger"]) + async def plan(self, magentic_context: MagenticContext) -> Message: + return Message("assistant", ["ledger"]) - async def replan(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage("assistant", ["re-ledger"]) + async def replan(self, magentic_context: MagenticContext) -> Message: + return Message("assistant", ["re-ledger"]) async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: return MagenticProgressLedger( @@ -726,8 +726,8 @@ class NotProgressingManager(MagenticManagerBase): instruction_or_question=MagenticProgressLedgerItem(reason="r", answer="done"), ) - async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessage: - return ChatMessage("assistant", ["final"]) + async def prepare_final_answer(self, magentic_context: MagenticContext) -> Message: + return Message("assistant", ["final"]) async def test_magentic_stall_and_reset_reach_limits(): @@ -747,7 +747,7 @@ async def test_magentic_stall_and_reset_reach_limits(): output_event = next((e for e in events if e.type == "output"), None) assert output_event is not None assert isinstance(output_event.data, list) - assert all(isinstance(msg, ChatMessage) for msg in output_event.data) # type: ignore + assert all(isinstance(msg, Message) for msg in output_event.data) # type: ignore assert len(output_event.data) > 0 # type: ignore assert output_event.data[-1].text is not None # type: ignore assert output_event.data[-1].text == "Workflow terminated due to reaching maximum reset count." # type: ignore @@ -760,7 +760,7 @@ async def test_magentic_checkpoint_runtime_only() -> None: manager = FakeManager(max_round_count=10) wf = MagenticBuilder(participants=[DummyExec("agentA")], manager=manager).build() - baseline_output: ChatMessage | None = None + baseline_output: Message | None = None async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True): if ev.type == "output": baseline_output = ev.data # type: ignore[assignment] @@ -794,7 +794,7 @@ async def test_magentic_checkpoint_runtime_overrides_buildtime() -> None: participants=[DummyExec("agentA")], checkpoint_storage=buildtime_storage, manager=manager ).build() - baseline_output: ChatMessage | None = None + baseline_output: Message | None = None async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True): if ev.type == "output": baseline_output = ev.data # type: ignore[assignment] @@ -821,8 +821,8 @@ async def test_magentic_context_no_duplicate_on_reset(): ctx = MagenticContext(task="task", participant_descriptions={"Alice": "Researcher"}) # Add some history - ctx.chat_history.append(ChatMessage("assistant", ["response1"])) - ctx.chat_history.append(ChatMessage("assistant", ["response2"])) + ctx.chat_history.append(Message("assistant", ["response1"])) + ctx.chat_history.append(Message("assistant", ["response2"])) assert len(ctx.chat_history) == 2 # Reset @@ -832,7 +832,7 @@ async def test_magentic_context_no_duplicate_on_reset(): assert len(ctx.chat_history) == 0, "chat_history should be empty after reset" # Add new history - ctx.chat_history.append(ChatMessage("assistant", ["new_response"])) + ctx.chat_history.append(Message("assistant", ["new_response"])) assert len(ctx.chat_history) == 1, "Should have exactly 1 message after adding to reset context" @@ -844,8 +844,8 @@ async def test_magentic_checkpoint_restore_no_duplicate_history(): wf = MagenticBuilder(participants=[DummyExec("agentA")], checkpoint_storage=storage, manager=manager).build() # Run with conversation history to create initial checkpoint - conversation: list[ChatMessage] = [ - ChatMessage("user", ["task_msg"]), + conversation: list[Message] = [ + Message("user", ["task_msg"]), ] async for event in wf.run(conversation, stream=True): @@ -890,121 +890,6 @@ async def test_magentic_checkpoint_restore_no_duplicate_history(): ) -# endregion - -# region Participant Factory Tests - - -def test_magentic_builder_rejects_empty_participant_factories(): - """Test that MagenticBuilder rejects empty participant_factories list.""" - with pytest.raises(ValueError, match=r"participant_factories cannot be empty"): - MagenticBuilder(participant_factories=[]) - - with pytest.raises( - ValueError, - match=r"Either participants or participant_factories must be provided\.", - ): - MagenticBuilder() - - -def test_magentic_builder_rejects_mixing_participants_and_factories(): - """Test that passing both participants and participant_factories to the constructor raises an error.""" - agent = StubAgent("agentA", "reply from agentA") - - with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"): - MagenticBuilder( - participants=[agent], - participant_factories=[lambda: StubAgent("agentB", "reply")], - ) - - -def test_magentic_builder_rejects_both_factories_and_participants(): - """Test that passing both participant_factories and participants raises an error.""" - with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"): - MagenticBuilder( - participant_factories=[lambda: StubAgent("agentA", "reply from agentA")], - participants=[StubAgent("agentB", "reply from agentB")], - ) - - -def test_magentic_builder_rejects_both_participants_and_factories(): - """Test that passing both participants and participant_factories raises an error.""" - with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"): - MagenticBuilder( - participants=[StubAgent("agentA", "reply from agentA")], - participant_factories=[lambda: StubAgent("agentB", "reply from agentB")], - ) - - -async def test_magentic_with_participant_factories(): - """Test workflow creation using participant_factories.""" - call_count = 0 - - def create_agent() -> StubAgent: - nonlocal call_count - call_count += 1 - return StubAgent("agentA", "reply from agentA") - - manager = FakeManager() - workflow = MagenticBuilder(participant_factories=[create_agent], manager=manager).build() - - # Factory should be called during build - assert call_count == 1 - - outputs: list[WorkflowEvent] = [] - async for event in workflow.run("test task", stream=True): - if event.type == "output": - outputs.append(event) - - assert len(outputs) == 1 - - -async def test_magentic_participant_factories_reusable_builder(): - """Test that the builder can be reused to build multiple workflows with factories.""" - call_count = 0 - - def create_agent() -> StubAgent: - nonlocal call_count - call_count += 1 - return StubAgent("agentA", "reply from agentA") - - builder = MagenticBuilder(participant_factories=[create_agent], manager=FakeManager()) - - # Build first workflow - wf1 = builder.build() - assert call_count == 1 - - # Build second workflow - wf2 = builder.build() - assert call_count == 2 - - # Verify that the two workflows have different agent instances - assert wf1.executors["agentA"] is not wf2.executors["agentA"] - - -async def test_magentic_participant_factories_with_checkpointing(): - """Test checkpointing with participant_factories.""" - storage = InMemoryCheckpointStorage() - - def create_agent() -> StubAgent: - return StubAgent("agentA", "reply from agentA") - - manager = FakeManager() - workflow = MagenticBuilder( - participant_factories=[create_agent], checkpoint_storage=storage, manager=manager - ).build() - - outputs: list[WorkflowEvent] = [] - async for event in workflow.run("checkpoint test", stream=True): - if event.type == "output": - outputs.append(event) - - assert outputs, "Should have workflow output" - - checkpoints = await storage.list_checkpoints() - assert checkpoints, "Checkpoints should be created during workflow execution" - - # endregion # region Manager Factory Tests @@ -1112,66 +997,6 @@ async def test_magentic_manager_factory_reusable_builder(): assert orchestrator1 is not orchestrator2 -def test_magentic_with_both_participant_and_manager_factories(): - """Test workflow creation using both participant_factories and manager_factory.""" - participant_factory_call_count = 0 - manager_factory_call_count = 0 - - def create_agent() -> StubAgent: - nonlocal participant_factory_call_count - participant_factory_call_count += 1 - return StubAgent("agentA", "reply from agentA") - - def manager_factory() -> MagenticManagerBase: - nonlocal manager_factory_call_count - manager_factory_call_count += 1 - return FakeManager() - - workflow = MagenticBuilder(participant_factories=[create_agent], manager_factory=manager_factory).build() - - # All factories should be called during build - assert participant_factory_call_count == 1 - assert manager_factory_call_count == 1 - - # Verify executor is present in the workflow - assert "agentA" in workflow.executors - - -async def test_magentic_factories_reusable_for_multiple_workflows(): - """Test that both factories are reused correctly for multiple workflow builds.""" - participant_factory_call_count = 0 - manager_factory_call_count = 0 - - def create_agent() -> StubAgent: - nonlocal participant_factory_call_count - participant_factory_call_count += 1 - return StubAgent("agentA", "reply from agentA") - - def manager_factory() -> MagenticManagerBase: - nonlocal manager_factory_call_count - manager_factory_call_count += 1 - return FakeManager() - - builder = MagenticBuilder(participant_factories=[create_agent], manager_factory=manager_factory) - - # Build first workflow - wf1 = builder.build() - assert participant_factory_call_count == 1 - assert manager_factory_call_count == 1 - - # Build second workflow - wf2 = builder.build() - assert participant_factory_call_count == 2 - assert manager_factory_call_count == 2 - - # Verify that the workflows have different agent and orchestrator instances - assert wf1.executors["agentA"] is not wf2.executors["agentA"] - - orchestrator1 = next(e for e in wf1.executors.values() if isinstance(e, MagenticOrchestrator)) - orchestrator2 = next(e for e in wf2.executors.values() if isinstance(e, MagenticOrchestrator)) - assert orchestrator1 is not orchestrator2 - - def test_magentic_agent_factory_with_standard_manager_options(): """Test that agent_factory properly passes through standard manager options.""" factory_call_count = 0 @@ -1197,8 +1022,8 @@ def test_magentic_agent_factory_with_standard_manager_options(): from agent_framework_orchestrations._magentic import _MagenticTaskLedger # type: ignore custom_task_ledger = _MagenticTaskLedger( - facts=ChatMessage("assistant", ["Custom facts"]), - plan=ChatMessage("assistant", ["Custom plan"]), + facts=Message("assistant", ["Custom facts"]), + plan=Message("assistant", ["Custom plan"]), ) participant = StubAgent("agentA", "reply from agentA") diff --git a/python/packages/orchestrations/tests/test_orchestration_request_info.py b/python/packages/orchestrations/tests/test_orchestration_request_info.py index 88fcdf757e..1e2b8a4af6 100644 --- a/python/packages/orchestrations/tests/test_orchestration_request_info.py +++ b/python/packages/orchestrations/tests/test_orchestration_request_info.py @@ -11,7 +11,7 @@ from agent_framework import ( AgentResponse, AgentResponseUpdate, AgentThread, - ChatMessage, + Message, SupportsAgentRun, ) from agent_framework._workflows._agent_executor import AgentExecutorRequest, AgentExecutorResponse @@ -72,16 +72,16 @@ class TestAgentRequestInfoResponse: def test_create_response_with_messages(self): """Test creating an AgentRequestInfoResponse with messages.""" - messages = [ChatMessage(role="user", text="Additional info")] + messages = [Message(role="user", text="Additional info")] response = AgentRequestInfoResponse(messages=messages) assert response.messages == messages def test_from_messages_factory(self): - """Test creating response from ChatMessage list.""" + """Test creating response from Message list.""" messages = [ - ChatMessage(role="user", text="Message 1"), - ChatMessage(role="user", text="Message 2"), + Message(role="user", text="Message 1"), + Message(role="user", text="Message 2"), ] response = AgentRequestInfoResponse.from_messages(messages) @@ -113,7 +113,7 @@ class TestAgentRequestInfoExecutor: """Test that request_info handler calls ctx.request_info.""" executor = AgentRequestInfoExecutor(id="test_executor") - agent_response = AgentResponse(messages=[ChatMessage(role="assistant", text="Agent response")]) + agent_response = AgentResponse(messages=[Message(role="assistant", text="Agent response")]) agent_response = AgentExecutorResponse( executor_id="test_agent", agent_response=agent_response, @@ -131,7 +131,7 @@ class TestAgentRequestInfoExecutor: """Test response handler when user provides additional messages.""" executor = AgentRequestInfoExecutor(id="test_executor") - agent_response = AgentResponse(messages=[ChatMessage(role="assistant", text="Original")]) + agent_response = AgentResponse(messages=[Message(role="assistant", text="Original")]) original_request = AgentExecutorResponse( executor_id="test_agent", agent_response=agent_response, @@ -157,7 +157,7 @@ class TestAgentRequestInfoExecutor: """Test response handler when user approves (no additional messages).""" executor = AgentRequestInfoExecutor(id="test_executor") - agent_response = AgentResponse(messages=[ChatMessage(role="assistant", text="Original")]) + agent_response = AgentResponse(messages=[Message(role="assistant", text="Original")]) original_request = AgentExecutorResponse( executor_id="test_agent", agent_response=agent_response, @@ -200,7 +200,7 @@ class _TestAgent: async def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -209,10 +209,10 @@ class _TestAgent: """Dummy run method.""" if stream: return self._run_stream_impl() - return AgentResponse(messages=[ChatMessage(role="assistant", text="Test response")]) + return AgentResponse(messages=[Message(role="assistant", text="Test response")]) async def _run_stream_impl(self) -> AsyncIterable[AgentResponseUpdate]: - yield AgentResponseUpdate(messages=[ChatMessage(role="assistant", text="Test response stream")]) + yield AgentResponseUpdate(messages=[Message(role="assistant", text="Test response stream")]) def get_new_thread(self, **kwargs: Any) -> AgentThread: """Creates a new conversation thread for the agent.""" diff --git a/python/packages/orchestrations/tests/test_sequential.py b/python/packages/orchestrations/tests/test_sequential.py index cb6f3b0872..880e33761d 100644 --- a/python/packages/orchestrations/tests/test_sequential.py +++ b/python/packages/orchestrations/tests/test_sequential.py @@ -10,9 +10,9 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, Executor, + Message, TypeCompatibilityError, WorkflowContext, WorkflowRunState, @@ -27,7 +27,7 @@ class _EchoAgent(BaseAgent): def run( # type: ignore[override] self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -37,7 +37,7 @@ class _EchoAgent(BaseAgent): return self._run_stream() async def _run() -> AgentResponse: - return AgentResponse(messages=[ChatMessage("assistant", [f"{self.name} reply"])]) + return AgentResponse(messages=[Message("assistant", [f"{self.name} reply"])]) return _run() @@ -50,11 +50,11 @@ class _SummarizerExec(Executor): """Custom executor that summarizes by appending a short assistant message.""" @handler - async def summarize(self, agent_response: AgentExecutorResponse, ctx: WorkflowContext[list[ChatMessage]]) -> None: + async def summarize(self, agent_response: AgentExecutorResponse, ctx: WorkflowContext[list[Message]]) -> None: conversation = agent_response.full_conversation or [] user_texts = [m.text for m in conversation if m.role == "user"] agents = [m.author_name or m.role for m in conversation if m.role == "assistant"] - summary = ChatMessage("assistant", [f"Summary of users:{len(user_texts)} agents:{len(agents)}"]) + summary = Message("assistant", [f"Summary of users:{len(user_texts)} agents:{len(agents)}"]) await ctx.send_message(list(conversation) + [summary]) @@ -62,7 +62,7 @@ class _InvalidExecutor(Executor): """Invalid executor that does not have a handler that accepts a list of chat messages""" @handler - async def summarize(self, conversation: list[str], ctx: WorkflowContext[list[ChatMessage]]) -> None: + async def summarize(self, conversation: list[str], ctx: WorkflowContext[list[Message]]) -> None: pass @@ -71,22 +71,6 @@ def test_sequential_builder_rejects_empty_participants() -> None: SequentialBuilder(participants=[]) -def test_sequential_builder_rejects_empty_participant_factories() -> None: - with pytest.raises(ValueError): - SequentialBuilder(participant_factories=[]) - - -def test_sequential_builder_rejects_mixing_participants_and_factories() -> None: - """Test that passing both participants and participant_factories to the constructor raises an error.""" - a1 = _EchoAgent(id="agent1", name="A1") - - with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"): - SequentialBuilder( - participants=[a1], - participant_factories=[lambda: _EchoAgent(id="agent2", name="A2")], - ) - - def test_sequential_builder_validation_rejects_invalid_executor() -> None: """Test that adding an invalid executor to the builder raises an error.""" with pytest.raises(TypeCompatibilityError): @@ -100,7 +84,7 @@ async def test_sequential_agents_append_to_context() -> None: wf = SequentialBuilder(participants=[a1, a2]).build() completed = False - output: list[ChatMessage] | None = None + output: list[Message] | None = None async for ev in wf.run("hello sequential", stream=True): if ev.type == "status" and ev.state == WorkflowRunState.IDLE: completed = True @@ -112,7 +96,7 @@ async def test_sequential_agents_append_to_context() -> None: assert completed assert output is not None assert isinstance(output, list) - msgs: list[ChatMessage] = output + msgs: list[Message] = output assert len(msgs) == 3 assert msgs[0].role == "user" and "hello sequential" in msgs[0].text assert msgs[1].role == "assistant" and (msgs[1].author_name == "A1" or True) @@ -121,37 +105,6 @@ async def test_sequential_agents_append_to_context() -> None: assert "A2 reply" in msgs[2].text -async def test_sequential_register_participants_with_agent_factories() -> None: - """Test that register_participants works with agent factories.""" - - def create_agent1() -> _EchoAgent: - return _EchoAgent(id="agent1", name="A1") - - def create_agent2() -> _EchoAgent: - return _EchoAgent(id="agent2", name="A2") - - wf = SequentialBuilder(participant_factories=[create_agent1, create_agent2]).build() - - completed = False - output: list[ChatMessage] | None = None - async for ev in wf.run("hello factories", stream=True): - if ev.type == "status" and ev.state == WorkflowRunState.IDLE: - completed = True - elif ev.type == "output": - output = ev.data - if completed and output is not None: - break - - assert completed - assert output is not None - assert isinstance(output, list) - msgs: list[ChatMessage] = output - assert len(msgs) == 3 - assert msgs[0].role == "user" and "hello factories" in msgs[0].text - assert msgs[1].role == "assistant" and "A1 reply" in msgs[1].text - assert msgs[2].role == "assistant" and "A2 reply" in msgs[2].text - - async def test_sequential_with_custom_executor_summary() -> None: a1 = _EchoAgent(id="agent1", name="A1") summarizer = _SummarizerExec(id="summarizer") @@ -159,7 +112,7 @@ async def test_sequential_with_custom_executor_summary() -> None: wf = SequentialBuilder(participants=[a1, summarizer]).build() completed = False - output: list[ChatMessage] | None = None + output: list[Message] | None = None async for ev in wf.run("topic X", stream=True): if ev.type == "status" and ev.state == WorkflowRunState.IDLE: completed = True @@ -170,7 +123,7 @@ async def test_sequential_with_custom_executor_summary() -> None: assert completed assert output is not None - msgs: list[ChatMessage] = output + msgs: list[Message] = output # Expect: [user, A1 reply, summary] assert len(msgs) == 3 assert msgs[0].role == "user" @@ -178,44 +131,13 @@ async def test_sequential_with_custom_executor_summary() -> None: assert msgs[2].role == "assistant" and msgs[2].text.startswith("Summary of users:") -async def test_sequential_register_participants_mixed_agents_and_executors() -> None: - """Test register_participants with both agent and executor factories.""" - - def create_agent() -> _EchoAgent: - return _EchoAgent(id="agent1", name="A1") - - def create_summarizer() -> _SummarizerExec: - return _SummarizerExec(id="summarizer") - - wf = SequentialBuilder(participant_factories=[create_agent, create_summarizer]).build() - - completed = False - output: list[ChatMessage] | None = None - async for ev in wf.run("topic Y", stream=True): - if ev.type == "status" and ev.state == WorkflowRunState.IDLE: - completed = True - elif ev.type == "output": - output = ev.data - if completed and output is not None: - break - - assert completed - assert output is not None - msgs: list[ChatMessage] = output - # Expect: [user, A1 reply, summary] - assert len(msgs) == 3 - assert msgs[0].role == "user" and "topic Y" in msgs[0].text - assert msgs[1].role == "assistant" and "A1 reply" in msgs[1].text - assert msgs[2].role == "assistant" and msgs[2].text.startswith("Summary of users:") - - async def test_sequential_checkpoint_resume_round_trip() -> None: storage = InMemoryCheckpointStorage() initial_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2")) wf = SequentialBuilder(participants=list(initial_agents), checkpoint_storage=storage).build() - baseline_output: list[ChatMessage] | None = None + baseline_output: list[Message] | None = None async for ev in wf.run("checkpoint sequential", stream=True): if ev.type == "output": baseline_output = ev.data # type: ignore[assignment] @@ -236,7 +158,7 @@ async def test_sequential_checkpoint_resume_round_trip() -> None: resumed_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2")) wf_resume = SequentialBuilder(participants=list(resumed_agents), checkpoint_storage=storage).build() - resumed_output: list[ChatMessage] | None = None + resumed_output: list[Message] | None = None async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True): if ev.type == "output": resumed_output = ev.data # type: ignore[assignment] @@ -258,7 +180,7 @@ async def test_sequential_checkpoint_runtime_only() -> None: agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2")) wf = SequentialBuilder(participants=list(agents)).build() - baseline_output: list[ChatMessage] | None = None + baseline_output: list[Message] | None = None async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True): if ev.type == "output": baseline_output = ev.data # type: ignore[assignment] @@ -279,7 +201,7 @@ async def test_sequential_checkpoint_runtime_only() -> None: resumed_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2")) wf_resume = SequentialBuilder(participants=list(resumed_agents)).build() - resumed_output: list[ChatMessage] | None = None + resumed_output: list[Message] | None = None async for ev in wf_resume.run( checkpoint_id=resume_checkpoint.checkpoint_id, checkpoint_storage=storage, stream=True ): @@ -309,7 +231,7 @@ async def test_sequential_checkpoint_runtime_overrides_buildtime() -> None: agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2")) wf = SequentialBuilder(participants=list(agents), checkpoint_storage=buildtime_storage).build() - baseline_output: list[ChatMessage] | None = None + baseline_output: list[Message] | None = None async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True): if ev.type == "output": baseline_output = ev.data # type: ignore[assignment] @@ -325,92 +247,6 @@ async def test_sequential_checkpoint_runtime_overrides_buildtime() -> None: assert len(buildtime_checkpoints) == 0, "Build-time storage should have no checkpoints when overridden" -async def test_sequential_register_participants_with_checkpointing() -> None: - """Test that checkpointing works with register_participants.""" - storage = InMemoryCheckpointStorage() - - def create_agent1() -> _EchoAgent: - return _EchoAgent(id="agent1", name="A1") - - def create_agent2() -> _EchoAgent: - return _EchoAgent(id="agent2", name="A2") - - wf = SequentialBuilder(participant_factories=[create_agent1, create_agent2], checkpoint_storage=storage).build() - - baseline_output: list[ChatMessage] | None = None - async for ev in wf.run("checkpoint with factories", stream=True): - if ev.type == "output": - baseline_output = ev.data - if ev.type == "status" and ev.state == WorkflowRunState.IDLE: - break - - assert baseline_output is not None - - checkpoints = await storage.list_checkpoints() - assert checkpoints - checkpoints.sort(key=lambda cp: cp.timestamp) - - resume_checkpoint = next( - (cp for cp in checkpoints if (cp.metadata or {}).get("checkpoint_type") == "superstep"), - checkpoints[-1], - ) - - wf_resume = SequentialBuilder( - participant_factories=[create_agent1, create_agent2], checkpoint_storage=storage - ).build() - - resumed_output: list[ChatMessage] | None = None - async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True): - if ev.type == "output": - resumed_output = ev.data - if ev.type == "status" and ev.state in ( - WorkflowRunState.IDLE, - WorkflowRunState.IDLE_WITH_PENDING_REQUESTS, - ): - break - - assert resumed_output is not None - assert [m.role for m in resumed_output] == [m.role for m in baseline_output] - assert [m.text for m in resumed_output] == [m.text for m in baseline_output] - - -async def test_sequential_register_participants_factories_called_on_build() -> None: - """Test that factories are called during build(), not during register_participants().""" - call_count = 0 - - def create_agent() -> _EchoAgent: - nonlocal call_count - call_count += 1 - return _EchoAgent(id=f"agent{call_count}", name=f"A{call_count}") - - builder = SequentialBuilder(participant_factories=[create_agent, create_agent]) - - # Factories should not be called yet - assert call_count == 0 - - wf = builder.build() - - # Now factories should have been called - assert call_count == 2 - - # Run the workflow to ensure it works - completed = False - output: list[ChatMessage] | None = None - async for ev in wf.run("test factories timing", stream=True): - if ev.type == "status" and ev.state == WorkflowRunState.IDLE: - completed = True - elif ev.type == "output": - output = ev.data # type: ignore[assignment] - if completed and output is not None: - break - - assert completed - assert output is not None - msgs: list[ChatMessage] = output - # Should have user message + 2 agent replies - assert len(msgs) == 3 - - async def test_sequential_builder_reusable_after_build_with_participants() -> None: """Test that the builder can be reused to build multiple identical workflows with participants().""" a1 = _EchoAgent(id="agent1", name="A1") @@ -423,30 +259,3 @@ async def test_sequential_builder_reusable_after_build_with_participants() -> No assert builder._participants[0] is a1 # type: ignore assert builder._participants[1] is a2 # type: ignore - assert builder._participant_factories == [] # type: ignore - - -async def test_sequential_builder_reusable_after_build_with_factories() -> None: - """Test that the builder can be reused to build multiple workflows with register_participants().""" - call_count = 0 - - def create_agent1() -> _EchoAgent: - nonlocal call_count - call_count += 1 - return _EchoAgent(id="agent1", name="A1") - - def create_agent2() -> _EchoAgent: - nonlocal call_count - call_count += 1 - return _EchoAgent(id="agent2", name="A2") - - builder = SequentialBuilder(participant_factories=[create_agent1, create_agent2]) - - # Build first workflow - factories should be called - builder.build() - - assert call_count == 2 - assert builder._participants == [] # type: ignore - assert len(builder._participant_factories) == 2 # type: ignore - assert builder._participant_factories[0] is create_agent1 # type: ignore - assert builder._participant_factories[1] is create_agent2 # type: ignore diff --git a/python/packages/purview/AGENTS.md b/python/packages/purview/AGENTS.md index 3d09982e70..30be4ffbf7 100644 --- a/python/packages/purview/AGENTS.md +++ b/python/packages/purview/AGENTS.md @@ -32,7 +32,7 @@ from agent_framework.microsoft import PurviewPolicyMiddleware, PurviewSettings settings = PurviewSettings(...) middleware = PurviewPolicyMiddleware(settings=settings) -agent = ChatAgent(..., middleware=[middleware]) +agent = Agent(..., middleware=[middleware]) ``` ## Import Path diff --git a/python/packages/purview/README.md b/python/packages/purview/README.md index b016f00c8b..f23da59457 100644 --- a/python/packages/purview/README.md +++ b/python/packages/purview/README.md @@ -8,7 +8,7 @@ - Middleware-based policy enforcement (agent-level and chat-client level) - Blocks or allows content at both ingress (prompt) and egress (response) -- Works with any `ChatAgent` / agent orchestration using the standard Agent Framework middleware pipeline +- Works with any `Agent` / agent orchestration using the standard Agent Framework middleware pipeline - Supports both synchronous `TokenCredential` and `AsyncTokenCredential` from `azure-identity` - Configuration via `PurviewSettings` / `PurviewAppLocation` - Built-in caching with configurable TTL and size limits for protection scopes in `PurviewSettings` @@ -53,26 +53,26 @@ Add Purview when you need to: ```python import asyncio -from agent_framework import ChatAgent, ChatMessage, Role +from agent_framework import Agent, Message, Role from agent_framework.azure import AzureOpenAIChatClient from agent_framework.microsoft import PurviewPolicyMiddleware, PurviewSettings from azure.identity import InteractiveBrowserCredential async def main(): - chat_client = AzureOpenAIChatClient() # uses environment for endpoint + deployment + client = AzureOpenAIChatClient() # uses environment for endpoint + deployment purview_middleware = PurviewPolicyMiddleware( credential=InteractiveBrowserCredential(), settings=PurviewSettings(app_name="My Sample App") ) - agent = ChatAgent( - chat_client=chat_client, + agent = Agent( + client=client, instructions="You are a helpful assistant.", middleware=[purview_middleware] ) - response = await agent.run(ChatMessage("user", ["Summarize zero trust in one sentence."])) + response = await agent.run(Message("user", ["Summarize zero trust in one sentence."])) print(response) asyncio.run(main()) @@ -218,7 +218,7 @@ settings = PurviewSettings( Use the agent middleware when you already have / want the full agent pipeline: ```python -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureOpenAIChatClient from agent_framework.microsoft import PurviewPolicyMiddleware, PurviewSettings from azure.identity import DefaultAzureCredential @@ -226,8 +226,8 @@ from azure.identity import DefaultAzureCredential credential = DefaultAzureCredential() client = AzureOpenAIChatClient() -agent = ChatAgent( - chat_client=client, +agent = Agent( + client=client, instructions="You are helpful.", middleware=[PurviewPolicyMiddleware(credential, PurviewSettings(app_name="My App"))] ) @@ -237,14 +237,14 @@ Use the chat middleware when you attach directly to a chat client (e.g. minimal ```python import os -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureOpenAIChatClient from agent_framework.microsoft import PurviewChatPolicyMiddleware, PurviewSettings from azure.identity import DefaultAzureCredential credential = DefaultAzureCredential() -chat_client = AzureOpenAIChatClient( +client = AzureOpenAIChatClient( deployment_name=os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"], endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], credential=credential, @@ -253,7 +253,7 @@ chat_client = AzureOpenAIChatClient( ], ) -agent = ChatAgent(chat_client=chat_client, instructions="You are helpful.") +agent = Agent(client=client, instructions="You are helpful.") ``` The policy logic is identical; the difference is only the hook point in the pipeline. @@ -272,7 +272,7 @@ The policy logic is identical; the difference is only the hook point in the pipe 3. **After successful agent execution** (`response phase`): the produced messages are evaluated using the same user_id from the prompt phase. 4. **If blocked**: result messages are replaced with a blocking notice. -The user identifier is discovered from `ChatMessage.additional_properties['user_id']` during the prompt phase and reused for the response phase, ensuring both evaluations map consistently to the same user. If no user_id is present, policy evaluation is skipped entirely. +The user identifier is discovered from `Message.additional_properties['user_id']` during the prompt phase and reused for the response phase, ensuring both evaluations map consistently to the same user. If no user_id is present, policy evaluation is skipped entirely. You can customize the blocking messages using the `blocked_prompt_message` and `blocked_response_message` fields in `PurviewSettings`. For more advanced scenarios, you can wrap the middleware or post-process `context.result` in later middleware. @@ -315,7 +315,7 @@ except (PurviewAuthenticationError, PurviewRateLimitError, PurviewRequestError, --- ## Notes -- **User Identification**: Provide a `user_id` per request (e.g. in `ChatMessage(..., additional_properties={"user_id": ""})`) for per-user policy scoping. If no user_id is provided, policy evaluation is skipped entirely. +- **User Identification**: Provide a `user_id` per request (e.g. in `Message(..., additional_properties={"user_id": ""})`) for per-user policy scoping. If no user_id is provided, policy evaluation is skipped entirely. - **Blocking Messages**: Can be customized via `blocked_prompt_message` and `blocked_response_message` in `PurviewSettings`. By default, they are "Prompt blocked by policy" and "Response blocked by policy" respectively. - **Streaming Responses**: Post-response policy evaluation presently applies only to non-streaming chat responses. - **Error Handling**: Use `ignore_exceptions` and `ignore_payment_required` settings for graceful degradation. When enabled, errors are logged but don't fail the request. diff --git a/python/packages/purview/agent_framework_purview/_middleware.py b/python/packages/purview/agent_framework_purview/_middleware.py index 52a74ffc10..10e0443b0b 100644 --- a/python/packages/purview/agent_framework_purview/_middleware.py +++ b/python/packages/purview/agent_framework_purview/_middleware.py @@ -26,13 +26,11 @@ class PurviewPolicyMiddleware(AgentMiddleware): .. code-block:: python from agent_framework.microsoft import PurviewPolicyMiddleware, PurviewSettings - from agent_framework import ChatAgent + from agent_framework import Agent credential = ... # TokenCredential or AsyncTokenCredential settings = PurviewSettings(app_name="My App") - agent = ChatAgent( - chat_client=client, instructions="...", middleware=[PurviewPolicyMiddleware(credential, settings)] - ) + agent = Agent(client=client, instructions="...", middleware=[PurviewPolicyMiddleware(credential, settings)]) """ def __init__( @@ -77,10 +75,10 @@ class PurviewPolicyMiddleware(AgentMiddleware): context.messages, Activity.UPLOAD_TEXT, session_id=session_id ) if should_block_prompt: - from agent_framework import AgentResponse, ChatMessage + from agent_framework import AgentResponse, Message context.result = AgentResponse( - messages=[ChatMessage(role="system", text=self._settings.blocked_prompt_message)] + messages=[Message(role="system", text=self._settings.blocked_prompt_message)] ) raise MiddlewareTermination except MiddlewareTermination: @@ -110,10 +108,10 @@ class PurviewPolicyMiddleware(AgentMiddleware): user_id=resolved_user_id, ) if should_block_response: - from agent_framework import AgentResponse, ChatMessage + from agent_framework import AgentResponse, Message context.result = AgentResponse( - messages=[ChatMessage(role="system", text=self._settings.blocked_response_message)] + messages=[Message(role="system", text=self._settings.blocked_response_message)] ) else: # Streaming responses are not supported for post-checks @@ -173,9 +171,9 @@ class PurviewChatPolicyMiddleware(ChatMiddleware): context.messages, Activity.UPLOAD_TEXT, session_id=session_id ) if should_block_prompt: - from agent_framework import ChatMessage, ChatResponse + from agent_framework import ChatResponse, Message - blocked_message = ChatMessage(role="system", text=self._settings.blocked_prompt_message) + blocked_message = Message(role="system", text=self._settings.blocked_prompt_message) context.result = ChatResponse(messages=[blocked_message]) raise MiddlewareTermination except MiddlewareTermination: @@ -205,9 +203,9 @@ class PurviewChatPolicyMiddleware(ChatMiddleware): messages, Activity.UPLOAD_TEXT, session_id=session_id_response, user_id=resolved_user_id ) if should_block_response: - from agent_framework import ChatMessage, ChatResponse + from agent_framework import ChatResponse, Message - blocked_message = ChatMessage(role="system", text=self._settings.blocked_response_message) + blocked_message = Message(role="system", text=self._settings.blocked_response_message) context.result = ChatResponse(messages=[blocked_message]) else: logger.debug("Streaming responses are not supported for Purview policy post-checks") diff --git a/python/packages/purview/agent_framework_purview/_processor.py b/python/packages/purview/agent_framework_purview/_processor.py index e2206a781b..5525897a9e 100644 --- a/python/packages/purview/agent_framework_purview/_processor.py +++ b/python/packages/purview/agent_framework_purview/_processor.py @@ -6,7 +6,7 @@ import uuid from collections.abc import Iterable, MutableMapping from typing import Any -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework._logging import get_logger from ._cache import CacheProvider, InMemoryCacheProvider, create_protection_scopes_cache_key @@ -64,7 +64,7 @@ class ScopedContentProcessor: async def process_messages( self, - messages: Iterable[ChatMessage], + messages: Iterable[Message], activity: Activity, session_id: str | None = None, user_id: str | None = None, @@ -97,7 +97,7 @@ class ScopedContentProcessor: async def _map_messages( self, - messages: Iterable[ChatMessage], + messages: Iterable[Message], activity: Activity, session_id: str | None = None, provided_user_id: str | None = None, diff --git a/python/packages/purview/pyproject.toml b/python/packages/purview/pyproject.toml index a1e6456cf3..3b57750bc9 100644 --- a/python/packages/purview/pyproject.toml +++ b/python/packages/purview/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://github.com/microsoft/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -24,7 +24,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "azure-core>=1.30.0", "httpx>=0.27.0", ] diff --git a/python/packages/purview/tests/conftest.py b/python/packages/purview/tests/purview/conftest.py similarity index 100% rename from python/packages/purview/tests/conftest.py rename to python/packages/purview/tests/purview/conftest.py diff --git a/python/packages/purview/tests/test_cache.py b/python/packages/purview/tests/purview/test_cache.py similarity index 100% rename from python/packages/purview/tests/test_cache.py rename to python/packages/purview/tests/purview/test_cache.py diff --git a/python/packages/purview/tests/test_chat_middleware.py b/python/packages/purview/tests/purview/test_chat_middleware.py similarity index 84% rename from python/packages/purview/tests/test_chat_middleware.py rename to python/packages/purview/tests/purview/test_chat_middleware.py index 41ed8e0e4e..677e3e277b 100644 --- a/python/packages/purview/tests/test_chat_middleware.py +++ b/python/packages/purview/tests/purview/test_chat_middleware.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import ChatContext, ChatMessage, MiddlewareTermination +from agent_framework import ChatContext, Message, MiddlewareTermination from azure.core.credentials import AccessToken from agent_framework_purview import PurviewChatPolicyMiddleware, PurviewSettings @@ -34,12 +34,10 @@ class TestPurviewChatPolicyMiddleware: @pytest.fixture def chat_context(self) -> ChatContext: - chat_client = DummyChatClient() + client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - return ChatContext( - chat_client=chat_client, messages=[ChatMessage(role="user", text="Hello")], options=chat_options - ) + return ChatContext(client=client, messages=[Message(role="user", text="Hello")], options=chat_options) async def test_initialization(self, middleware: PurviewChatPolicyMiddleware) -> None: assert middleware._client is not None @@ -57,7 +55,7 @@ class TestPurviewChatPolicyMiddleware: class Result: def __init__(self): - self.messages = [ChatMessage(role="assistant", text="Hi there")] + self.messages = [Message(role="assistant", text="Hi there")] ctx.result = Result() @@ -93,7 +91,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: class Result: def __init__(self): - self.messages = [ChatMessage(role="assistant", text="Sensitive output")] # pragma: no cover + self.messages = [Message(role="assistant", text="Sensitive output")] # pragma: no cover ctx.result = Result() @@ -105,12 +103,12 @@ class TestPurviewChatPolicyMiddleware: assert "blocked" in first_msg.text.lower() async def test_streaming_skips_post_check(self, middleware: PurviewChatPolicyMiddleware) -> None: - chat_client = DummyChatClient() + client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" streaming_context = ChatContext( - chat_client=chat_client, - messages=[ChatMessage(role="user", text="Hello")], + client=client, + messages=[Message(role="user", text="Hello")], options=chat_options, stream=True, ) @@ -142,7 +140,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role="assistant", text="Response")] + result.messages = [Message(role="assistant", text="Response")] ctx.result = result await middleware.process(chat_context, mock_next) @@ -166,7 +164,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role="assistant", text="Response")] + result.messages = [Message(role="assistant", text="Response")] ctx.result = result await middleware.process(chat_context, mock_next) @@ -186,12 +184,10 @@ class TestPurviewChatPolicyMiddleware: settings = PurviewSettings(app_name="Test App", ignore_payment_required=False) middleware = PurviewChatPolicyMiddleware(mock_credential, settings) - chat_client = DummyChatClient() + client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role="user", text="Hello")], options=chat_options - ) + context = ChatContext(client=client, messages=[Message(role="user", text="Hello")], options=chat_options) async def mock_process_messages(*args, **kwargs): raise PurviewPaymentRequiredError("Payment required") @@ -212,12 +208,10 @@ class TestPurviewChatPolicyMiddleware: settings = PurviewSettings(app_name="Test App", ignore_payment_required=False) middleware = PurviewChatPolicyMiddleware(mock_credential, settings) - chat_client = DummyChatClient() + client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role="user", text="Hello")], options=chat_options - ) + context = ChatContext(client=client, messages=[Message(role="user", text="Hello")], options=chat_options) call_count = 0 @@ -232,7 +226,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role="assistant", text="OK")] + result.messages = [Message(role="assistant", text="OK")] ctx.result = result with pytest.raises(PurviewPaymentRequiredError): @@ -245,12 +239,10 @@ class TestPurviewChatPolicyMiddleware: settings = PurviewSettings(app_name="Test App", ignore_payment_required=True) middleware = PurviewChatPolicyMiddleware(mock_credential, settings) - chat_client = DummyChatClient() + client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role="user", text="Hello")], options=chat_options - ) + context = ChatContext(client=client, messages=[Message(role="user", text="Hello")], options=chat_options) async def mock_process_messages(*args, **kwargs): raise PurviewPaymentRequiredError("Payment required") @@ -259,7 +251,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role="assistant", text="Response")] + result.messages = [Message(role="assistant", text="Response")] context.result = result # Should not raise, just log @@ -287,12 +279,10 @@ class TestPurviewChatPolicyMiddleware: settings = PurviewSettings(app_name="Test App", ignore_exceptions=True) middleware = PurviewChatPolicyMiddleware(mock_credential, settings) - chat_client = DummyChatClient() + client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role="user", text="Hello")], options=chat_options - ) + context = ChatContext(client=client, messages=[Message(role="user", text="Hello")], options=chat_options) async def mock_process_messages(*args, **kwargs): raise ValueError("Some error") @@ -301,7 +291,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role="assistant", text="Response")] + result.messages = [Message(role="assistant", text="Response")] context.result = result # Should not raise, just log @@ -316,12 +306,10 @@ class TestPurviewChatPolicyMiddleware: settings = PurviewSettings(app_name="Test App", ignore_exceptions=False) middleware = PurviewChatPolicyMiddleware(mock_credential, settings) - chat_client = DummyChatClient() + client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role="user", text="Hello")], options=chat_options - ) + context = ChatContext(client=client, messages=[Message(role="user", text="Hello")], options=chat_options) with patch.object(middleware._processor, "process_messages", side_effect=ValueError("boom")): @@ -338,12 +326,10 @@ class TestPurviewChatPolicyMiddleware: settings = PurviewSettings(app_name="Test App", ignore_exceptions=False) middleware = PurviewChatPolicyMiddleware(mock_credential, settings) - chat_client = DummyChatClient() + client = DummyChatClient() chat_options = MagicMock() chat_options.model = "test-model" - context = ChatContext( - chat_client=chat_client, messages=[ChatMessage(role="user", text="Hello")], options=chat_options - ) + context = ChatContext(client=client, messages=[Message(role="user", text="Hello")], options=chat_options) call_count = 0 @@ -358,7 +344,7 @@ class TestPurviewChatPolicyMiddleware: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role="assistant", text="OK")] + result.messages = [Message(role="assistant", text="OK")] ctx.result = result with pytest.raises(ValueError, match="post"): @@ -369,15 +355,15 @@ class TestPurviewChatPolicyMiddleware: ) -> None: """Test that session_id is extracted from context.options['conversation_id'].""" chat_client = DummyChatClient() - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] options = {"conversation_id": "conv-123", "model": "test-model"} - context = ChatContext(chat_client=chat_client, messages=messages, options=options) + context = ChatContext(client=chat_client, messages=messages, options=options) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role="assistant", text="Hi")] + result.messages = [Message(role="assistant", text="Hi")] ctx.result = result await middleware.process(context, mock_next) @@ -391,14 +377,14 @@ class TestPurviewChatPolicyMiddleware: ) -> None: """Test that session_id is None when options don't contain conversation_id.""" chat_client = DummyChatClient() - messages = [ChatMessage(role="user", text="Hello")] - context = ChatContext(chat_client=chat_client, messages=messages, options=None) + messages = [Message(role="user", text="Hello")] + context = ChatContext(client=chat_client, messages=messages, options=None) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role="assistant", text="Hi")] + result.messages = [Message(role="assistant", text="Hi")] ctx.result = result await middleware.process(context, mock_next) @@ -409,15 +395,15 @@ class TestPurviewChatPolicyMiddleware: async def test_chat_middleware_session_id_used_in_post_check(self, middleware: PurviewChatPolicyMiddleware) -> None: """Test that session_id is passed to post-check process_messages call.""" chat_client = DummyChatClient() - messages = [ChatMessage(role="user", text="Hello")] + messages = [Message(role="user", text="Hello")] options = {"conversation_id": "conv-999"} - context = ChatContext(chat_client=chat_client, messages=messages, options=options) + context = ChatContext(client=chat_client, messages=messages, options=options) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: async def mock_next(ctx: ChatContext) -> None: result = MagicMock() - result.messages = [ChatMessage(role="assistant", text="Response")] + result.messages = [Message(role="assistant", text="Response")] ctx.result = result await middleware.process(context, mock_next) diff --git a/python/packages/purview/tests/test_exceptions.py b/python/packages/purview/tests/purview/test_exceptions.py similarity index 100% rename from python/packages/purview/tests/test_exceptions.py rename to python/packages/purview/tests/purview/test_exceptions.py diff --git a/python/packages/purview/tests/test_middleware.py b/python/packages/purview/tests/purview/test_middleware.py similarity index 83% rename from python/packages/purview/tests/test_middleware.py rename to python/packages/purview/tests/purview/test_middleware.py index 71eaa93056..ff77331155 100644 --- a/python/packages/purview/tests/test_middleware.py +++ b/python/packages/purview/tests/purview/test_middleware.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import AgentContext, AgentResponse, AgentThread, ChatMessage, MiddlewareTermination +from agent_framework import AgentContext, AgentResponse, AgentThread, Message, MiddlewareTermination from azure.core.credentials import AccessToken from agent_framework_purview import PurviewPolicyMiddleware, PurviewSettings @@ -50,7 +50,7 @@ class TestPurviewPolicyMiddleware: self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock ) -> None: """Test middleware allows prompt that passes policy check.""" - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Hello, how are you?")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Hello, how are you?")]) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")): next_called = False @@ -58,7 +58,7 @@ class TestPurviewPolicyMiddleware: async def mock_next(ctx: AgentContext) -> None: nonlocal next_called next_called = True - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="I'm good, thanks!")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="I'm good, thanks!")]) await middleware.process(context, mock_next) @@ -69,7 +69,7 @@ class TestPurviewPolicyMiddleware: self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock ) -> None: """Test middleware blocks prompt that violates policy.""" - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Sensitive information")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Sensitive information")]) with patch.object(middleware._processor, "process_messages", return_value=(True, "user-123")): next_called = False @@ -89,7 +89,7 @@ class TestPurviewPolicyMiddleware: async def test_middleware_checks_response(self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock) -> None: """Test middleware checks agent response for policy violations.""" - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Hello")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Hello")]) call_count = 0 @@ -103,7 +103,7 @@ class TestPurviewPolicyMiddleware: async def mock_next(ctx: AgentContext) -> None: ctx.result = AgentResponse( - messages=[ChatMessage(role="assistant", text="Here's some sensitive information")] + messages=[Message(role="assistant", text="Here's some sensitive information")] ) await middleware.process(context, mock_next) @@ -121,7 +121,7 @@ class TestPurviewPolicyMiddleware: # Set ignore_exceptions to True so AttributeError is caught and logged middleware._settings.ignore_exceptions = True - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Hello")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Hello")]) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")): @@ -138,12 +138,12 @@ class TestPurviewPolicyMiddleware: """Test middleware passes correct activity type to processor.""" from agent_framework_purview._models import Activity - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Test")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Test")]) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_process: async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="Response")]) await middleware.process(context, mock_next) @@ -155,13 +155,13 @@ class TestPurviewPolicyMiddleware: self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock ) -> None: """Test that streaming results skip post-check evaluation.""" - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Hello")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Hello")]) context.stream = True with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="streaming")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="streaming")]) await middleware.process(context, mock_next) @@ -173,7 +173,7 @@ class TestPurviewPolicyMiddleware: """Test that 402 in pre-check is raised when ignore_payment_required=False.""" from agent_framework_purview._exceptions import PurviewPaymentRequiredError - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Hello")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Hello")]) with patch.object( middleware._processor, @@ -193,7 +193,7 @@ class TestPurviewPolicyMiddleware: """Test that 402 in post-check is raised when ignore_payment_required=False.""" from agent_framework_purview._exceptions import PurviewPaymentRequiredError - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Hello")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Hello")]) call_count = 0 @@ -207,7 +207,7 @@ class TestPurviewPolicyMiddleware: with patch.object(middleware._processor, "process_messages", side_effect=side_effect): async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="OK")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="OK")]) with pytest.raises(PurviewPaymentRequiredError): await middleware.process(context, mock_next) @@ -218,7 +218,7 @@ class TestPurviewPolicyMiddleware: """Test that post-check exceptions are propagated when ignore_exceptions=False.""" middleware._settings.ignore_exceptions = False - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Hello")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Hello")]) call_count = 0 @@ -232,7 +232,7 @@ class TestPurviewPolicyMiddleware: with patch.object(middleware._processor, "process_messages", side_effect=side_effect): async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="OK")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="OK")]) with pytest.raises(ValueError, match="Post-check blew up"): await middleware.process(context, mock_next) @@ -244,14 +244,14 @@ class TestPurviewPolicyMiddleware: # Set ignore_exceptions to True middleware._settings.ignore_exceptions = True - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Test")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Test")]) with patch.object( middleware._processor, "process_messages", side_effect=Exception("Pre-check error") ) as mock_process: async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="Response")]) await middleware.process(context, mock_next) @@ -267,7 +267,7 @@ class TestPurviewPolicyMiddleware: # Set ignore_exceptions to True middleware._settings.ignore_exceptions = True - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Test")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Test")]) call_count = 0 @@ -281,7 +281,7 @@ class TestPurviewPolicyMiddleware: with patch.object(middleware._processor, "process_messages", side_effect=mock_process_messages): async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="Response")]) await middleware.process(context, mock_next) @@ -298,7 +298,7 @@ class TestPurviewPolicyMiddleware: mock_agent = MagicMock() mock_agent.name = "test-agent" - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Test")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Test")]) # Mock processor to raise an exception async def mock_process_messages(*args, **kwargs): @@ -307,7 +307,7 @@ class TestPurviewPolicyMiddleware: with patch.object(middleware._processor, "process_messages", side_effect=mock_process_messages): async def mock_next(ctx): - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="Response")]) # Should not raise, just log await middleware.process(context, mock_next) @@ -322,7 +322,7 @@ class TestPurviewPolicyMiddleware: mock_agent = MagicMock() mock_agent.name = "test-agent" - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Test")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Test")]) # Mock processor to raise an exception async def mock_process_messages(*args, **kwargs): @@ -342,12 +342,12 @@ class TestPurviewPolicyMiddleware: ) -> None: """Test that session_id is extracted from thread.service_thread_id.""" thread = AgentThread(service_thread_id="thread-123") - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Hello")], thread=thread) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Hello")], thread=thread) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Hi")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="Hi")]) await middleware.process(context, mock_next) @@ -359,13 +359,13 @@ class TestPurviewPolicyMiddleware: self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock ) -> None: """Test that session_id is extracted from message.additional_properties['conversation_id'].""" - messages = [ChatMessage(role="user", text="Hello", additional_properties={"conversation_id": "conv-456"})] + messages = [Message(role="user", text="Hello", additional_properties={"conversation_id": "conv-456"})] context = AgentContext(agent=mock_agent, messages=messages) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Hi")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="Hi")]) await middleware.process(context, mock_next) @@ -378,13 +378,13 @@ class TestPurviewPolicyMiddleware: ) -> None: """Test that thread.service_thread_id takes precedence over message conversation_id.""" thread = AgentThread(service_thread_id="thread-789") - messages = [ChatMessage(role="user", text="Hello", additional_properties={"conversation_id": "conv-456"})] + messages = [Message(role="user", text="Hello", additional_properties={"conversation_id": "conv-456"})] context = AgentContext(agent=mock_agent, messages=messages, thread=thread) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Hi")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="Hi")]) await middleware.process(context, mock_next) @@ -395,12 +395,12 @@ class TestPurviewPolicyMiddleware: self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock ) -> None: """Test that session_id is None when no thread or conversation_id is available.""" - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Hello")]) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Hello")]) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Hi")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="Hi")]) await middleware.process(context, mock_next) @@ -412,12 +412,12 @@ class TestPurviewPolicyMiddleware: ) -> None: """Test that session_id is passed to post-check process_messages call.""" thread = AgentThread(service_thread_id="thread-999") - context = AgentContext(agent=mock_agent, messages=[ChatMessage(role="user", text="Hello")], thread=thread) + context = AgentContext(agent=mock_agent, messages=[Message(role="user", text="Hello")], thread=thread) with patch.object(middleware._processor, "process_messages", return_value=(False, "user-123")) as mock_proc: async def mock_next(ctx: AgentContext) -> None: - ctx.result = AgentResponse(messages=[ChatMessage(role="assistant", text="Response")]) + ctx.result = AgentResponse(messages=[Message(role="assistant", text="Response")]) await middleware.process(context, mock_next) diff --git a/python/packages/purview/tests/test_processor.py b/python/packages/purview/tests/purview/test_processor.py similarity index 96% rename from python/packages/purview/tests/test_processor.py rename to python/packages/purview/tests/purview/test_processor.py index be4d4aca89..ab96999921 100644 --- a/python/packages/purview/tests/test_processor.py +++ b/python/packages/purview/tests/purview/test_processor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework_purview import PurviewAppLocation, PurviewLocationType, PurviewSettings from agent_framework_purview._models import ( @@ -83,8 +83,8 @@ class TestScopedContentProcessor: async def test_process_messages_with_defaults(self, processor: ScopedContentProcessor) -> None: """Test process_messages with settings that have defaults.""" messages = [ - ChatMessage(role="user", text="Hello"), - ChatMessage(role="assistant", text="Hi there"), + Message(role="user", text="Hello"), + Message(role="assistant", text="Hi there"), ] with patch.object(processor, "_map_messages", return_value=([], None)) as mock_map: @@ -98,7 +98,7 @@ class TestScopedContentProcessor: self, processor: ScopedContentProcessor, process_content_request_factory ) -> None: """Test process_messages returns True when content should be blocked.""" - messages = [ChatMessage(role="user", text="Sensitive content")] + messages = [Message(role="user", text="Sensitive content")] mock_request = process_content_request_factory("Sensitive content") @@ -120,7 +120,7 @@ class TestScopedContentProcessor: ) -> None: """Test _map_messages creates ProcessContentRequest objects.""" messages = [ - ChatMessage( + Message( role="user", text="Test message", message_id="msg-123", @@ -139,7 +139,7 @@ class TestScopedContentProcessor: """Test _map_messages gets token info when settings lack some defaults.""" settings = PurviewSettings(app_name="Test App", tenant_id="12345678-1234-1234-1234-123456789012") processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role="user", text="Test", message_id="msg-123")] + messages = [Message(role="user", text="Test", message_id="msg-123")] requests, user_id = await processor._map_messages(messages, Activity.UPLOAD_TEXT) @@ -156,7 +156,7 @@ class TestScopedContentProcessor: return_value={"user_id": "test-user", "client_id": "test-client"} ) - messages = [ChatMessage(role="user", text="Test", message_id="msg-123")] + messages = [Message(role="user", text="Test", message_id="msg-123")] with pytest.raises(ValueError, match="Tenant id required"): await processor._map_messages(messages, Activity.UPLOAD_TEXT) @@ -331,7 +331,7 @@ class TestScopedContentProcessor: processor = ScopedContentProcessor(mock_client, settings) messages = [ - ChatMessage( + Message( role="user", text="Test message", additional_properties={"user_id": "22345678-1234-1234-1234-123456789012"}, @@ -355,7 +355,7 @@ class TestScopedContentProcessor: ) processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] requests, user_id = await processor._map_messages( messages, Activity.UPLOAD_TEXT, provided_user_id="32345678-1234-1234-1234-123456789012" @@ -376,7 +376,7 @@ class TestScopedContentProcessor: ) processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role="user", text="Test message")] + messages = [Message(role="user", text="Test message")] requests, user_id = await processor._map_messages(messages, Activity.UPLOAD_TEXT) @@ -479,7 +479,7 @@ class TestUserIdResolution: settings = PurviewSettings(app_name="Test App") # No tenant_id or app_location processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] requests, user_id = await processor._map_messages(messages, Activity.UPLOAD_TEXT) @@ -493,7 +493,7 @@ class TestUserIdResolution: processor = ScopedContentProcessor(mock_client, settings) messages = [ - ChatMessage( + Message( role="user", text="Test", additional_properties={"user_id": "22222222-2222-2222-2222-222222222222"}, @@ -513,7 +513,7 @@ class TestUserIdResolution: processor = ScopedContentProcessor(mock_client, settings) messages = [ - ChatMessage( + Message( role="user", text="Test", author_name="33333333-3333-3333-3333-333333333333", @@ -531,7 +531,7 @@ class TestUserIdResolution: processor = ScopedContentProcessor(mock_client, settings) messages = [ - ChatMessage( + Message( role="user", text="Test", author_name="John Doe", # Not a GUID @@ -550,7 +550,7 @@ class TestUserIdResolution: """Test provided_user_id parameter is used as last resort.""" processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] requests, user_id = await processor._map_messages( messages, Activity.UPLOAD_TEXT, provided_user_id="44444444-4444-4444-4444-444444444444" @@ -562,7 +562,7 @@ class TestUserIdResolution: """Test invalid provided_user_id is ignored.""" processor = ScopedContentProcessor(mock_client, settings) - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] requests, user_id = await processor._map_messages(messages, Activity.UPLOAD_TEXT, provided_user_id="not-a-guid") @@ -574,11 +574,11 @@ class TestUserIdResolution: processor = ScopedContentProcessor(mock_client, settings) messages = [ - ChatMessage( + Message( role="user", text="First", additional_properties={"user_id": "55555555-5555-5555-5555-555555555555"} ), - ChatMessage(role="assistant", text="Response"), - ChatMessage(role="user", text="Second"), + Message(role="assistant", text="Response"), + Message(role="user", text="Second"), ] requests, user_id = await processor._map_messages(messages, Activity.UPLOAD_TEXT) @@ -594,13 +594,13 @@ class TestUserIdResolution: processor = ScopedContentProcessor(mock_client, settings) messages = [ - ChatMessage(role="user", text="First", author_name="Not a GUID"), - ChatMessage( + Message(role="user", text="First", author_name="Not a GUID"), + Message( role="assistant", text="Response", additional_properties={"user_id": "66666666-6666-6666-6666-666666666666"}, ), - ChatMessage( + Message( role="user", text="Third", additional_properties={"user_id": "77777777-7777-7777-7777-777777777777"} ), ] @@ -654,7 +654,7 @@ class TestScopedContentProcessorCaching: scope_identifier="scope-123", scopes=[] ) - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] await processor.process_messages(messages, Activity.UPLOAD_TEXT, user_id="12345678-1234-1234-1234-123456789012") @@ -676,7 +676,7 @@ class TestScopedContentProcessorCaching: mock_client.get_protection_scopes.side_effect = PurviewPaymentRequiredError("Payment required") - messages = [ChatMessage(role="user", text="Test")] + messages = [Message(role="user", text="Test")] with pytest.raises(PurviewPaymentRequiredError): await processor.process_messages( diff --git a/python/packages/purview/tests/test_purview_client.py b/python/packages/purview/tests/purview/test_purview_client.py similarity index 100% rename from python/packages/purview/tests/test_purview_client.py rename to python/packages/purview/tests/purview/test_purview_client.py diff --git a/python/packages/purview/tests/test_purview_models.py b/python/packages/purview/tests/purview/test_purview_models.py similarity index 100% rename from python/packages/purview/tests/test_purview_models.py rename to python/packages/purview/tests/purview/test_purview_models.py diff --git a/python/packages/purview/tests/test_settings.py b/python/packages/purview/tests/purview/test_settings.py similarity index 100% rename from python/packages/purview/tests/test_settings.py rename to python/packages/purview/tests/purview/test_settings.py diff --git a/python/packages/redis/AGENTS.md b/python/packages/redis/AGENTS.md index 60acfc1f77..3b575e5029 100644 --- a/python/packages/redis/AGENTS.md +++ b/python/packages/redis/AGENTS.md @@ -13,7 +13,7 @@ Redis-based storage for agent threads and context. from agent_framework.redis import RedisChatMessageStore store = RedisChatMessageStore(redis_url="redis://localhost:6379") -agent = ChatAgent(..., chat_message_store_factory=lambda: store) +agent = Agent(..., chat_message_store_factory=lambda: store) ``` ## Import Path diff --git a/python/packages/redis/agent_framework_redis/__init__.py b/python/packages/redis/agent_framework_redis/__init__.py index fd63a69cae..9453401441 100644 --- a/python/packages/redis/agent_framework_redis/__init__.py +++ b/python/packages/redis/agent_framework_redis/__init__.py @@ -2,6 +2,8 @@ import importlib.metadata from ._chat_message_store import RedisChatMessageStore +from ._context_provider import _RedisContextProvider +from ._history_provider import _RedisHistoryProvider from ._provider import RedisProvider try: @@ -12,5 +14,7 @@ except importlib.metadata.PackageNotFoundError: __all__ = [ "RedisChatMessageStore", "RedisProvider", + "_RedisContextProvider", + "_RedisHistoryProvider", "__version__", ] diff --git a/python/packages/redis/agent_framework_redis/_chat_message_store.py b/python/packages/redis/agent_framework_redis/_chat_message_store.py index 4b50c63571..5ace6c13af 100644 --- a/python/packages/redis/agent_framework_redis/_chat_message_store.py +++ b/python/packages/redis/agent_framework_redis/_chat_message_store.py @@ -7,7 +7,7 @@ from typing import Any from uuid import uuid4 import redis.asyncio as redis -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework._serialization import SerializationMixin from redis.credentials import CredentialProvider @@ -64,7 +64,7 @@ class RedisChatMessageStore: thread_id: str | None = None, key_prefix: str = "chat_messages", max_messages: int | None = None, - messages: Sequence[ChatMessage] | None = None, + messages: Sequence[Message] | None = None, ) -> None: """Initialize the Redis chat message store. @@ -186,14 +186,14 @@ class RedisChatMessageStore: self._initial_messages_added = True self._initial_messages.clear() - async def _add_redis_messages(self, messages: Sequence[ChatMessage]) -> None: + async def _add_redis_messages(self, messages: Sequence[Message]) -> None: """Add multiple messages to Redis using atomic pipeline operation. This internal method efficiently adds multiple messages to the Redis list using a single atomic transaction to ensure consistency. Args: - messages: Sequence of ChatMessage objects to add to Redis. + messages: Sequence of Message objects to add to Redis. """ if not messages: return @@ -207,7 +207,7 @@ class RedisChatMessageStore: await pipe.rpush(self.redis_key, serialized_message) # type: ignore[misc] await pipe.execute() - async def add_messages(self, messages: Sequence[ChatMessage]) -> None: + async def add_messages(self, messages: Sequence[Message]) -> None: """Add messages to the Redis store (ChatMessageStoreProtocol protocol method). This method implements the required ChatMessageStoreProtocol protocol for adding messages. @@ -215,7 +215,7 @@ class RedisChatMessageStore: trimming if message limits are configured. Args: - messages: Sequence of ChatMessage objects to add to the store. + messages: Sequence of Message objects to add to the store. Can be empty (no-op) or contain multiple messages. Thread Safety: @@ -225,7 +225,7 @@ class RedisChatMessageStore: Example: .. code-block:: python - messages = [ChatMessage(role="user", text="Hello"), ChatMessage(role="assistant", text="Hi there!")] + messages = [Message(role="user", text="Hello"), Message(role="assistant", text="Hi there!")] await store.add_messages(messages) """ if not messages: @@ -244,14 +244,14 @@ class RedisChatMessageStore: # Keep only the most recent max_messages using LTRIM await self._redis_client.ltrim(self.redis_key, -self.max_messages, -1) # type: ignore[misc] - async def list_messages(self) -> list[ChatMessage]: + async def list_messages(self) -> list[Message]: """Get all messages from the store in chronological order (ChatMessageStoreProtocol protocol method). This method implements the required ChatMessageStoreProtocol protocol for retrieving messages. Returns all messages stored in Redis, ordered from oldest (index 0) to newest (index -1). Returns: - List of ChatMessage objects in chronological order (oldest first). + List of Message objects in chronological order (oldest first). Returns empty list if no messages exist or if Redis connection fails. Example: @@ -269,7 +269,7 @@ class RedisChatMessageStore: if redis_messages: for serialized_message in redis_messages: - # Deserialize each JSON message back to ChatMessage + # Deserialize each JSON message back to Message message = self._deserialize_message(serialized_message) messages.append(message) @@ -390,11 +390,11 @@ class RedisChatMessageStore: """ await self._redis_client.delete(self.redis_key) - def _serialize_message(self, message: ChatMessage) -> str: - """Serialize a ChatMessage to JSON string. + def _serialize_message(self, message: Message) -> str: + """Serialize a Message to JSON string. Args: - message: ChatMessage to serialize. + message: Message to serialize. Returns: JSON string representation of the message. @@ -402,17 +402,17 @@ class RedisChatMessageStore: # Serialize to compact JSON (no extra whitespace for Redis efficiency) return message.to_json(separators=(",", ":")) - def _deserialize_message(self, serialized_message: str) -> ChatMessage: - """Deserialize a JSON string to ChatMessage. + def _deserialize_message(self, serialized_message: str) -> Message: + """Deserialize a JSON string to Message. Args: serialized_message: JSON string representation of a message. Returns: - ChatMessage object. + Message object. """ - # Reconstruct ChatMessage using custom deserialization - return ChatMessage.from_json(serialized_message) + # Reconstruct Message using custom deserialization + return Message.from_json(serialized_message) # ============================================================================ # List-like Convenience Methods (Redis-optimized async versions) @@ -446,14 +446,14 @@ class RedisChatMessageStore: await self._ensure_initial_messages_added() return await self._redis_client.llen(self.redis_key) # type: ignore[misc,no-any-return] - async def getitem(self, index: int) -> ChatMessage: + async def getitem(self, index: int) -> Message: """Get a message by index using Redis LINDEX. Args: index: The index of the message to retrieve. Returns: - The ChatMessage at the specified index. + The Message at the specified index. Raises: IndexError: If the index is out of range. @@ -467,12 +467,12 @@ class RedisChatMessageStore: return self._deserialize_message(serialized_message) - async def setitem(self, index: int, item: ChatMessage) -> None: + async def setitem(self, index: int, item: Message) -> None: """Set a message at the specified index using Redis LSET. Args: index: The index at which to set the message. - item: The ChatMessage to set at the specified index. + item: The Message to set at the specified index. Raises: IndexError: If the index is out of range. @@ -490,11 +490,11 @@ class RedisChatMessageStore: serialized_message = self._serialize_message(item) await self._redis_client.lset(self.redis_key, index, serialized_message) # type: ignore[misc] - async def append(self, item: ChatMessage) -> None: + async def append(self, item: Message) -> None: """Append a message to the end of the store. Args: - item: The ChatMessage to append. + item: The Message to append. """ await self.add_messages([item]) @@ -507,14 +507,14 @@ class RedisChatMessageStore: await self._ensure_initial_messages_added() return await self._redis_client.llen(self.redis_key) # type: ignore[misc,no-any-return] - async def index(self, item: ChatMessage) -> int: + async def index(self, item: Message) -> int: """Return the index of the first occurrence of the specified message. Uses Redis LINDEX to iterate through the list without loading all messages. Still O(N) but more memory efficient for large lists. Args: - item: The ChatMessage to find. + item: The Message to find. Returns: The index of the first occurrence of the message. @@ -533,16 +533,16 @@ class RedisChatMessageStore: if redis_message == target_serialized: return i - raise ValueError("ChatMessage not found in store") + raise ValueError("Message not found in store") - async def remove(self, item: ChatMessage) -> None: + async def remove(self, item: Message) -> None: """Remove the first occurrence of the specified message from the store. Uses Redis LREM command for efficient removal by value. O(N) but performed natively in Redis without data transfer. Args: - item: The ChatMessage to remove. + item: The Message to remove. Raises: ValueError: If the message is not found in the store. @@ -556,13 +556,13 @@ class RedisChatMessageStore: removed_count = await self._redis_client.lrem(self.redis_key, 1, target_serialized) # type: ignore[misc] if removed_count == 0: - raise ValueError("ChatMessage not found in store") + raise ValueError("Message not found in store") - async def extend(self, items: Sequence[ChatMessage]) -> None: + async def extend(self, items: Sequence[Message]) -> None: """Extend the store by appending all messages from the iterable. Args: - items: Sequence of ChatMessage objects to append. + items: Sequence of Message objects to append. """ await self.add_messages(items) diff --git a/python/packages/redis/agent_framework_redis/_context_provider.py b/python/packages/redis/agent_framework_redis/_context_provider.py new file mode 100644 index 0000000000..f4e44a8677 --- /dev/null +++ b/python/packages/redis/agent_framework_redis/_context_provider.py @@ -0,0 +1,432 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""New-pattern Redis context provider using BaseContextProvider. + +This module provides ``_RedisContextProvider``, a side-by-side implementation of +:class:`RedisProvider` built on the new :class:`BaseContextProvider` hooks pattern. +It will be renamed to ``RedisContextProvider`` in PR2 when the old class is removed. +""" + +from __future__ import annotations + +import json +import sys +from functools import reduce +from operator import and_ +from typing import TYPE_CHECKING, Any, Literal, cast + +import numpy as np +from agent_framework import Message +from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext +from agent_framework.exceptions import ( + AgentException, + ServiceInitializationError, + ServiceInvalidRequestError, +) +from redisvl.index import AsyncSearchIndex +from redisvl.query import HybridQuery, TextQuery +from redisvl.query.filter import FilterExpression, Tag +from redisvl.utils.token_escaper import TokenEscaper +from redisvl.utils.vectorize import BaseVectorizer + +if sys.version_info >= (3, 11): + from typing import Self # pragma: no cover +else: + from typing_extensions import Self # pragma: no cover + +if sys.version_info >= (3, 12): + from typing import override # type: ignore # pragma: no cover +else: + from typing_extensions import override # type: ignore[import] # pragma: no cover + +if TYPE_CHECKING: + from agent_framework._agents import SupportsAgentRun + + +class _RedisContextProvider(BaseContextProvider): + """Redis context provider using the new BaseContextProvider hooks pattern. + + Stores context in Redis and retrieves scoped context via full-text or + optional hybrid vector search. This is the new-pattern equivalent of + :class:`RedisProvider`. + + Note: + This class uses a temporary ``_`` prefix to coexist with the existing + :class:`RedisProvider`. It will be renamed to ``RedisContextProvider`` + in PR2. + """ + + DEFAULT_CONTEXT_PROMPT = "## Memories\nConsider the following memories when answering user questions:" + + def __init__( + self, + source_id: str, + redis_url: str = "redis://localhost:6379", + index_name: str = "context", + prefix: str = "context", + *, + redis_vectorizer: BaseVectorizer | None = None, + vector_field_name: str | None = None, + vector_algorithm: Literal["flat", "hnsw"] | None = None, + vector_distance_metric: Literal["cosine", "ip", "l2"] | None = None, + application_id: str | None = None, + agent_id: str | None = None, + user_id: str | None = None, + context_prompt: str | None = None, + redis_index: Any = None, + overwrite_index: bool = False, + ): + """Create a Redis Context Provider. + + Args: + source_id: Unique identifier for this provider instance. + redis_url: The Redis server URL. + index_name: The name of the Redis index. + prefix: The prefix for all keys in the Redis database. + redis_vectorizer: The vectorizer to use for Redis. + vector_field_name: The name of the vector field in Redis. + vector_algorithm: The algorithm to use for vector search. + vector_distance_metric: The distance metric to use for vector search. + application_id: The application ID to scope the context. + agent_id: The agent ID to scope the context. + user_id: The user ID to scope the context. + context_prompt: The context prompt to use for the provider. + redis_index: The Redis index to use for the provider. + overwrite_index: Whether to overwrite the existing Redis index. + """ + super().__init__(source_id) + self.redis_url = redis_url + self.index_name = index_name + self.prefix = prefix + if redis_vectorizer is not None and not isinstance(redis_vectorizer, BaseVectorizer): + raise AgentException( + f"The redis vectorizer is not a valid type, got: {type(redis_vectorizer)}, expected: BaseVectorizer." + ) + self.redis_vectorizer = redis_vectorizer + self.vector_field_name = vector_field_name + self.vector_algorithm: Literal["flat", "hnsw"] | None = vector_algorithm + self.vector_distance_metric: Literal["cosine", "ip", "l2"] | None = vector_distance_metric + self.application_id = application_id + self.agent_id = agent_id + self.user_id = user_id + self.context_prompt = context_prompt or self.DEFAULT_CONTEXT_PROMPT + self.overwrite_index = overwrite_index + self._token_escaper: TokenEscaper = TokenEscaper() + self._index_initialized: bool = False + self._schema_dict: dict[str, Any] | None = None + self.redis_index = redis_index or AsyncSearchIndex.from_dict( + self.schema_dict, redis_url=self.redis_url, validate_on_load=True + ) + + # -- Hooks pattern --------------------------------------------------------- + + @override + async def before_run( + self, + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Retrieve scoped context from Redis and add to the session context.""" + self._validate_filters() + input_text = "\n".join(msg.text for msg in context.input_messages if msg and msg.text and msg.text.strip()) + if not input_text.strip(): + return + + memories = await self._redis_search(text=input_text, session_id=context.session_id) + line_separated_memories = "\n".join( + str(memory.get("content", "")) for memory in memories if memory.get("content") + ) + if line_separated_memories: + context.extend_messages( + self.source_id, + [Message(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")], + ) + + @override + async def after_run( + self, + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Store request/response messages to Redis for future retrieval.""" + self._validate_filters() + + messages_to_store: list[Message] = list(context.input_messages) + if context.response and context.response.messages: + messages_to_store.extend(context.response.messages) + + messages: list[dict[str, Any]] = [] + for message in messages_to_store: + if message.role in {"user", "assistant", "system"} and message.text and message.text.strip(): + shaped: dict[str, Any] = { + "role": message.role, + "content": message.text, + "conversation_id": context.session_id, + "message_id": message.message_id, + "author_name": message.author_name, + } + messages.append(shaped) + if messages: + await self._add(data=messages, session_id=context.session_id) + + # -- Internal methods (ported from RedisProvider) -------------------------- + + @property + def schema_dict(self) -> dict[str, Any]: + """Get the Redis schema dictionary, computing and caching it on first access.""" + if self._schema_dict is None: + vector_dims = self.redis_vectorizer.dims if self.redis_vectorizer is not None else None + vector_datatype = self.redis_vectorizer.dtype if self.redis_vectorizer is not None else None + self._schema_dict = self._build_schema_dict( + index_name=self.index_name, + prefix=self.prefix, + vector_field_name=self.vector_field_name, + vector_dims=vector_dims, + vector_datatype=vector_datatype, + vector_algorithm=self.vector_algorithm, + vector_distance_metric=self.vector_distance_metric, + ) + return self._schema_dict + + def _build_filter_from_dict(self, filters: dict[str, str | None]) -> Any | None: + """Builds a combined filter expression from simple equality tags.""" + parts = [Tag(k) == v for k, v in filters.items() if v] + return reduce(and_, parts) if parts else None + + def _build_schema_dict( + self, + *, + index_name: str, + prefix: str, + vector_field_name: str | None, + vector_dims: int | None, + vector_datatype: str | None, + vector_algorithm: Literal["flat", "hnsw"] | None, + vector_distance_metric: Literal["cosine", "ip", "l2"] | None, + ) -> dict[str, Any]: + """Builds the RediSearch schema configuration dictionary.""" + fields: list[dict[str, Any]] = [ + {"name": "role", "type": "tag"}, + {"name": "mime_type", "type": "tag"}, + {"name": "content", "type": "text"}, + {"name": "conversation_id", "type": "tag"}, + {"name": "message_id", "type": "tag"}, + {"name": "author_name", "type": "tag"}, + {"name": "application_id", "type": "tag"}, + {"name": "agent_id", "type": "tag"}, + {"name": "user_id", "type": "tag"}, + {"name": "thread_id", "type": "tag"}, + ] + if vector_field_name is not None and vector_dims is not None: + fields.append({ + "name": vector_field_name, + "type": "vector", + "attrs": { + "algorithm": (vector_algorithm or "hnsw"), + "dims": int(vector_dims), + "distance_metric": (vector_distance_metric or "cosine"), + "datatype": (vector_datatype or "float32"), + }, + }) + return { + "index": {"name": index_name, "prefix": prefix, "key_separator": ":", "storage_type": "hash"}, + "fields": fields, + } + + async def _ensure_index(self) -> None: + """Initialize the search index.""" + if self._index_initialized: + return + index_exists = await self.redis_index.exists() + if not self.overwrite_index and index_exists: + await self._validate_schema_compatibility() + await self.redis_index.create(overwrite=self.overwrite_index, drop=False) + self._index_initialized = True + + async def _validate_schema_compatibility(self) -> None: + """Validate that existing index schema matches current configuration.""" + TAG_DEFAULTS = {"separator": ",", "case_sensitive": False, "withsuffixtrie": False} + TEXT_DEFAULTS = {"weight": 1.0, "no_stem": False} + + def _significant_index(i: dict[str, Any]) -> dict[str, Any]: + return {k: i.get(k) for k in ("name", "prefix", "key_separator", "storage_type")} + + def _sig_tag(attrs: dict[str, Any] | None) -> dict[str, Any]: + a = {**TAG_DEFAULTS, **(attrs or {})} + return {k: a[k] for k in ("separator", "case_sensitive", "withsuffixtrie")} + + def _sig_text(attrs: dict[str, Any] | None) -> dict[str, Any]: + a = {**TEXT_DEFAULTS, **(attrs or {})} + return {k: a[k] for k in ("weight", "no_stem")} + + def _sig_vector(attrs: dict[str, Any] | None) -> dict[str, Any]: + a = {**(attrs or {})} + return {k: a.get(k) for k in ("algorithm", "dims", "distance_metric", "datatype")} + + def _schema_signature(schema: dict[str, Any]) -> dict[str, Any]: + sig: dict[str, Any] = {"index": _significant_index(schema.get("index", {})), "fields": {}} + for f in schema.get("fields", []): + name, ftype = f.get("name"), f.get("type") + if not name: + continue + if ftype == "tag": + sig["fields"][name] = {"type": "tag", "attrs": _sig_tag(f.get("attrs"))} + elif ftype == "text": + sig["fields"][name] = {"type": "text", "attrs": _sig_text(f.get("attrs"))} + elif ftype == "vector": + sig["fields"][name] = {"type": "vector", "attrs": _sig_vector(f.get("attrs"))} + else: + sig["fields"][name] = {"type": ftype} + return sig + + existing_index = await AsyncSearchIndex.from_existing(self.index_name, redis_url=self.redis_url) + existing_schema = existing_index.schema.to_dict() + current_schema = self.schema_dict + existing_sig = _schema_signature(existing_schema) + current_sig = _schema_signature(current_schema) + if existing_sig != current_sig: + raise ServiceInitializationError( + "Existing Redis index schema is incompatible with the current configuration.\n" + f"Existing (significant): {json.dumps(existing_sig, indent=2, sort_keys=True)}\n" + f"Current (significant): {json.dumps(current_sig, indent=2, sort_keys=True)}\n" + "Set overwrite_index=True to rebuild if this change is intentional." + ) + + async def _add( + self, + *, + data: dict[str, Any] | list[dict[str, Any]], + session_id: str | None = None, + metadata: dict[str, Any] | None = None, + ) -> None: + """Inserts one or many documents with partition fields populated.""" + self._validate_filters() + await self._ensure_index() + docs = data if isinstance(data, list) else [data] + + prepared: list[dict[str, Any]] = [] + for doc in docs: + d = dict(doc) + d.setdefault("application_id", self.application_id) + d.setdefault("agent_id", self.agent_id) + d.setdefault("user_id", self.user_id) + d.setdefault("thread_id", session_id) + d.setdefault("conversation_id", session_id) + if "content" not in d: + raise ServiceInvalidRequestError("add() requires a 'content' field in data") + if self.vector_field_name: + d.setdefault(self.vector_field_name, None) + prepared.append(d) + + if self.redis_vectorizer and self.vector_field_name: + text_list = [d["content"] for d in prepared] + embeddings = await self.redis_vectorizer.aembed_many(text_list, batch_size=len(text_list)) + for i, d in enumerate(prepared): + vec = np.asarray(embeddings[i], dtype=np.float32).tobytes() + field_name: str = self.vector_field_name + d[field_name] = vec + + await self.redis_index.load(prepared) + + async def _redis_search( + self, + text: str, + *, + session_id: str | None = None, + text_scorer: str = "BM25STD", + filter_expression: Any | None = None, + return_fields: list[str] | None = None, + num_results: int = 10, + alpha: float = 0.7, + ) -> list[dict[str, Any]]: + """Runs a text or hybrid vector-text search with optional filters.""" + await self._ensure_index() + self._validate_filters() + + q = (text or "").strip() + if not q: + raise ServiceInvalidRequestError("text_search() requires non-empty text") + num_results = max(int(num_results or 10), 1) + + combined_filter = self._build_filter_from_dict({ + "application_id": self.application_id, + "agent_id": self.agent_id, + "user_id": self.user_id, + "thread_id": session_id, + "conversation_id": session_id, + }) + if filter_expression is not None: + combined_filter = (combined_filter & filter_expression) if combined_filter else filter_expression + + return_fields = ( + return_fields + if return_fields is not None + else ["content", "role", "application_id", "agent_id", "user_id", "thread_id"] + ) + + try: + if self.redis_vectorizer and self.vector_field_name: + vector = await self.redis_vectorizer.aembed(q) + query = HybridQuery( + text=q, + text_field_name="content", + vector=vector, + vector_field_name=self.vector_field_name, + text_scorer=text_scorer, + filter_expression=combined_filter, + alpha=alpha, + dtype=self.redis_vectorizer.dtype, + num_results=num_results, + return_fields=return_fields, + stopwords=None, + ) + hybrid_results = await self.redis_index.query(query) + return cast(list[dict[str, Any]], hybrid_results) + query = TextQuery( + text=q, + text_field_name="content", + text_scorer=text_scorer, + filter_expression=combined_filter, + num_results=num_results, + return_fields=return_fields, + stopwords=None, + ) + text_results = await self.redis_index.query(query) + return cast(list[dict[str, Any]], text_results) + except Exception as exc: # pragma: no cover + raise ServiceInvalidRequestError(f"Redis text search failed: {exc}") from exc + + def _validate_filters(self) -> None: + """Validates that at least one filter is provided.""" + if not self.agent_id and not self.user_id and not self.application_id: + raise ServiceInitializationError( + "At least one of the filters: agent_id, user_id, or application_id is required." + ) + + async def search_all(self, page_size: int = 200) -> list[dict[str, Any]]: + """Returns all documents in the index.""" + from redisvl.query import FilterQuery + + out: list[dict[str, Any]] = [] + async for batch in self.redis_index.paginate( + FilterQuery(FilterExpression("*"), return_fields=[], num_results=page_size), + page_size=page_size, + ): + out.extend(batch) + return out + + async def __aenter__(self) -> Self: + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None: + """Async context manager exit.""" + + +__all__ = ["_RedisContextProvider"] diff --git a/python/packages/redis/agent_framework_redis/_history_provider.py b/python/packages/redis/agent_framework_redis/_history_provider.py new file mode 100644 index 0000000000..54d1ec5f81 --- /dev/null +++ b/python/packages/redis/agent_framework_redis/_history_provider.py @@ -0,0 +1,184 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""New-pattern Redis history provider using BaseHistoryProvider. + +This module provides ``_RedisHistoryProvider``, a side-by-side implementation of +:class:`RedisMessageStore` built on the new :class:`BaseHistoryProvider` hooks pattern. +It will be renamed to ``RedisHistoryProvider`` in PR2 when the old class is removed. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +import redis.asyncio as redis +from agent_framework import Message +from agent_framework._sessions import BaseHistoryProvider +from redis.credentials import CredentialProvider + + +class _RedisHistoryProvider(BaseHistoryProvider): + """Redis-backed history provider using the new BaseHistoryProvider hooks pattern. + + Stores conversation history in Redis Lists, with each session isolated by a + unique Redis key. This is the new-pattern equivalent of + :class:`RedisMessageStore`. + + Note: + This class uses a temporary ``_`` prefix to coexist with the existing + :class:`RedisMessageStore`. It will be renamed to ``RedisHistoryProvider`` + in PR2. + """ + + def __init__( + self, + source_id: str, + redis_url: str | None = None, + credential_provider: CredentialProvider | None = None, + host: str | None = None, + port: int = 6380, + ssl: bool = True, + username: str | None = None, + *, + key_prefix: str = "chat_messages", + max_messages: int | None = None, + load_messages: bool = True, + store_outputs: bool = True, + store_inputs: bool = True, + store_context_messages: bool = False, + store_context_from: set[str] | None = None, + ) -> None: + """Initialize the Redis history provider. + + Args: + source_id: Unique identifier for this provider instance. + redis_url: Redis connection URL (e.g., "redis://localhost:6379"). + Mutually exclusive with credential_provider. + credential_provider: Redis credential provider for Azure AD authentication. + Requires host parameter. Mutually exclusive with redis_url. + host: Redis host name. Required when using credential_provider. + port: Redis port number. Defaults to 6380 (Azure Redis SSL port). + ssl: Enable SSL/TLS connection. Defaults to True. + username: Redis username. + key_prefix: Prefix for Redis keys. Defaults to 'chat_messages'. + max_messages: Maximum number of messages to retain per session. + When exceeded, oldest messages are automatically trimmed. + None means unlimited storage. + load_messages: Whether to load messages before invocation. + store_outputs: Whether to store response messages. + store_inputs: Whether to store input messages. + store_context_messages: Whether to store context from other providers. + store_context_from: If set, only store context from these source_ids. + + Raises: + ValueError: If neither redis_url nor credential_provider is provided. + ValueError: If both redis_url and credential_provider are provided. + ValueError: If credential_provider is used without host parameter. + """ + super().__init__( + source_id, + load_messages=load_messages, + store_outputs=store_outputs, + store_inputs=store_inputs, + store_context_messages=store_context_messages, + store_context_from=store_context_from, + ) + + if redis_url is None and credential_provider is None: + raise ValueError("Either redis_url or credential_provider must be provided") + if redis_url is not None and credential_provider is not None: + raise ValueError("redis_url and credential_provider are mutually exclusive") + if credential_provider is not None and host is None: + raise ValueError("host is required when using credential_provider") + + self.key_prefix = key_prefix + self.max_messages = max_messages + self.redis_url = redis_url + + if credential_provider is not None and host is not None: + self._redis_client = redis.Redis( + host=host, + port=port, + ssl=ssl, + username=username, + credential_provider=credential_provider, + decode_responses=True, + ) + else: + self._redis_client = redis.from_url(redis_url, decode_responses=True) # type: ignore[no-untyped-call] + + def _redis_key(self, session_id: str | None) -> str: + """Get the Redis key for a given session's messages.""" + return f"{self.key_prefix}:{session_id or 'default'}" + + async def get_messages(self, session_id: str | None, **kwargs: Any) -> list[Message]: + """Retrieve stored messages for this session from Redis. + + Args: + session_id: The session ID to retrieve messages for. + **kwargs: Additional arguments (unused). + + Returns: + List of stored Message objects in chronological order. + """ + key = self._redis_key(session_id) + redis_messages = await self._redis_client.lrange(key, 0, -1) # type: ignore[misc] + messages: list[Message] = [] + if redis_messages: + for serialized in redis_messages: + messages.append(Message.from_dict(self._deserialize_json(serialized))) + return messages + + async def save_messages(self, session_id: str | None, messages: Sequence[Message], **kwargs: Any) -> None: + """Persist messages for this session to Redis. + + Args: + session_id: The session ID to store messages for. + messages: The messages to persist. + **kwargs: Additional arguments (unused). + """ + if not messages: + return + + key = self._redis_key(session_id) + serialized_messages = [self._serialize_json(msg) for msg in messages] + + async with self._redis_client.pipeline(transaction=True) as pipe: + for serialized in serialized_messages: + await pipe.rpush(key, serialized) # type: ignore[misc] + await pipe.execute() + + if self.max_messages is not None: + current_count = await self._redis_client.llen(key) # type: ignore[misc] + if current_count > self.max_messages: + await self._redis_client.ltrim(key, -self.max_messages, -1) # type: ignore[misc] + + @staticmethod + def _serialize_json(message: Message) -> str: + """Serialize a Message to a JSON string for Redis storage.""" + import json + + return json.dumps(message.to_dict()) + + @staticmethod + def _deserialize_json(data: str) -> dict[str, Any]: + """Deserialize a JSON string from Redis to a dict.""" + import json + + return json.loads(data) # type: ignore[no-any-return] + + async def clear(self, session_id: str | None) -> None: + """Clear all messages for a session. + + Args: + session_id: The session ID to clear messages for. + """ + await self._redis_client.delete(self._redis_key(session_id)) + + async def aclose(self) -> None: + """Close the Redis connection.""" + await self._redis_client.aclose() # type: ignore[misc] + + +__all__ = ["_RedisHistoryProvider"] diff --git a/python/packages/redis/agent_framework_redis/_provider.py b/python/packages/redis/agent_framework_redis/_provider.py index f8449962b7..193ea444d3 100644 --- a/python/packages/redis/agent_framework_redis/_provider.py +++ b/python/packages/redis/agent_framework_redis/_provider.py @@ -10,7 +10,7 @@ from operator import and_ from typing import Any, Literal, cast import numpy as np -from agent_framework import ChatMessage, Context, ContextProvider +from agent_framework import Context, ContextProvider, Message from agent_framework.exceptions import ( AgentException, ServiceInitializationError, @@ -484,19 +484,17 @@ class RedisProvider(ContextProvider): @override async def invoked( self, - request_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage] | None = None, + request_messages: Message | Sequence[Message], + response_messages: Message | Sequence[Message] | None = None, invoke_exception: Exception | None = None, **kwargs: Any, ) -> None: self._validate_filters() - request_messages_list = ( - [request_messages] if isinstance(request_messages, ChatMessage) else list(request_messages) - ) + request_messages_list = [request_messages] if isinstance(request_messages, Message) else list(request_messages) response_messages_list = ( [response_messages] - if isinstance(response_messages, ChatMessage) + if isinstance(response_messages, Message) else list(response_messages) if response_messages else [] @@ -518,7 +516,7 @@ class RedisProvider(ContextProvider): await self._add(data=messages) @override - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: """Called before invoking the model to provide scoped context. Concatenates recent messages into a query, fetches matching memories from Redis. @@ -534,7 +532,7 @@ class RedisProvider(ContextProvider): Context: Context object containing instructions with memories. """ self._validate_filters() - messages_list = [messages] if isinstance(messages, ChatMessage) else list(messages) + messages_list = [messages] if isinstance(messages, Message) else list(messages) input_text = "\n".join(msg.text for msg in messages_list if msg and msg.text and msg.text.strip()) memories = await self._redis_search(text=input_text) @@ -543,7 +541,7 @@ class RedisProvider(ContextProvider): ) return Context( - messages=[ChatMessage(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")] + messages=[Message(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")] if line_separated_memories else None ) diff --git a/python/packages/redis/pyproject.toml b/python/packages/redis/pyproject.toml index 30ec085706..2723b396de 100644 --- a/python/packages/redis/pyproject.toml +++ b/python/packages/redis/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.0.0b260130", + "agent-framework-core>=1.0.0b260210", "redis>=6.4.0", "redisvl>=0.8.2", "numpy>=2.2.6" diff --git a/python/packages/redis/tests/test_new_providers.py b/python/packages/redis/tests/test_new_providers.py new file mode 100644 index 0000000000..3540386873 --- /dev/null +++ b/python/packages/redis/tests/test_new_providers.py @@ -0,0 +1,455 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for _RedisContextProvider and _RedisHistoryProvider.""" + +from __future__ import annotations + +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from agent_framework import AgentResponse, Message +from agent_framework._sessions import AgentSession, SessionContext +from agent_framework.exceptions import ServiceInitializationError + +from agent_framework_redis._context_provider import _RedisContextProvider +from agent_framework_redis._history_provider import _RedisHistoryProvider + +# --------------------------------------------------------------------------- +# Shared fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def mock_index() -> AsyncMock: + idx = AsyncMock() + idx.create = AsyncMock() + idx.load = AsyncMock() + idx.query = AsyncMock(return_value=[]) + idx.exists = AsyncMock(return_value=False) + return idx + + +@pytest.fixture +def patch_index_from_dict(mock_index: AsyncMock): + with patch("agent_framework_redis._context_provider.AsyncSearchIndex") as mock_cls: + mock_cls.from_dict = MagicMock(return_value=mock_index) + + async def mock_from_existing(index_name: str, redis_url: str): # noqa: ARG001 + mock_existing = AsyncMock() + mock_existing.schema.to_dict = MagicMock( + side_effect=lambda: mock_cls.from_dict.call_args[0][0] if mock_cls.from_dict.call_args else {} + ) + return mock_existing + + mock_cls.from_existing = AsyncMock(side_effect=mock_from_existing) + yield mock_cls + + +@pytest.fixture +def mock_redis_client(): + client = MagicMock() + client.lrange = AsyncMock(return_value=[]) + client.llen = AsyncMock(return_value=0) + client.ltrim = AsyncMock() + client.delete = AsyncMock() + + mock_pipeline = AsyncMock() + mock_pipeline.rpush = AsyncMock() + mock_pipeline.execute = AsyncMock() + client.pipeline.return_value.__aenter__.return_value = mock_pipeline + + return client + + +# =========================================================================== +# _RedisContextProvider tests +# =========================================================================== + + +class TestRedisContextProviderInit: + def test_basic_construction(self, patch_index_from_dict: MagicMock): # noqa: ARG002 + provider = _RedisContextProvider(source_id="ctx", user_id="u1") + assert provider.source_id == "ctx" + assert provider.user_id == "u1" + assert provider.redis_url == "redis://localhost:6379" + assert provider.index_name == "context" + assert provider.prefix == "context" + + def test_custom_params(self, patch_index_from_dict: MagicMock): # noqa: ARG002 + provider = _RedisContextProvider( + source_id="ctx", + redis_url="redis://custom:6380", + index_name="my_idx", + prefix="my_prefix", + application_id="app1", + agent_id="agent1", + user_id="user1", + context_prompt="Custom prompt", + ) + assert provider.redis_url == "redis://custom:6380" + assert provider.index_name == "my_idx" + assert provider.prefix == "my_prefix" + assert provider.application_id == "app1" + assert provider.agent_id == "agent1" + assert provider.context_prompt == "Custom prompt" + + def test_default_context_prompt(self, patch_index_from_dict: MagicMock): # noqa: ARG002 + provider = _RedisContextProvider(source_id="ctx", user_id="u1") + assert "Memories" in provider.context_prompt + + def test_invalid_vectorizer_raises(self, patch_index_from_dict: MagicMock): # noqa: ARG002 + from agent_framework.exceptions import AgentException + + with pytest.raises(AgentException, match="not a valid type"): + _RedisContextProvider(source_id="ctx", user_id="u1", redis_vectorizer="bad") # type: ignore[arg-type] + + +class TestRedisContextProviderValidateFilters: + def test_no_filters_raises(self, patch_index_from_dict: MagicMock): # noqa: ARG002 + provider = _RedisContextProvider(source_id="ctx") + with pytest.raises(ServiceInitializationError, match="(?i)at least one"): + provider._validate_filters() + + def test_any_single_filter_ok(self, patch_index_from_dict: MagicMock): # noqa: ARG002 + for kwargs in [{"user_id": "u"}, {"agent_id": "a"}, {"application_id": "app"}]: + provider = _RedisContextProvider(source_id="ctx", **kwargs) + provider._validate_filters() # should not raise + + +class TestRedisContextProviderSchema: + def test_schema_has_expected_fields(self, patch_index_from_dict: MagicMock): # noqa: ARG002 + provider = _RedisContextProvider(source_id="ctx", user_id="u1") + schema = provider.schema_dict + field_names = [f["name"] for f in schema["fields"]] + for expected in ("role", "content", "conversation_id", "message_id", "application_id", "agent_id", "user_id"): + assert expected in field_names + assert schema["index"]["name"] == "context" + assert schema["index"]["prefix"] == "context" + + def test_schema_no_vector_without_vectorizer(self, patch_index_from_dict: MagicMock): # noqa: ARG002 + provider = _RedisContextProvider(source_id="ctx", user_id="u1") + field_types = [f["type"] for f in provider.schema_dict["fields"]] + assert "vector" not in field_types + + +class TestRedisContextProviderBeforeRun: + async def test_search_results_added_to_context( + self, + mock_index: AsyncMock, + patch_index_from_dict: MagicMock, # noqa: ARG002 + ): + mock_index.query = AsyncMock(return_value=[{"content": "Memory A"}, {"content": "Memory B"}]) + provider = _RedisContextProvider(source_id="ctx", user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", contents=["test query"])], session_id="s1") + + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + assert "ctx" in ctx.context_messages + msgs = ctx.context_messages["ctx"] + assert len(msgs) == 1 + assert "Memory A" in msgs[0].text + assert "Memory B" in msgs[0].text + + async def test_empty_input_no_search( + self, + mock_index: AsyncMock, + patch_index_from_dict: MagicMock, # noqa: ARG002 + ): + provider = _RedisContextProvider(source_id="ctx", user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", contents=[" "])], session_id="s1") + + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_index.query.assert_not_called() + assert "ctx" not in ctx.context_messages + + async def test_empty_results_no_messages( + self, + mock_index: AsyncMock, + patch_index_from_dict: MagicMock, # noqa: ARG002 + ): + mock_index.query = AsyncMock(return_value=[]) + provider = _RedisContextProvider(source_id="ctx", user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", contents=["hello"])], session_id="s1") + + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + assert "ctx" not in ctx.context_messages + + +class TestRedisContextProviderAfterRun: + async def test_stores_messages( + self, + mock_index: AsyncMock, + patch_index_from_dict: MagicMock, # noqa: ARG002 + ): + provider = _RedisContextProvider(source_id="ctx", user_id="u1") + session = AgentSession(session_id="test-session") + response = AgentResponse(messages=[Message(role="assistant", contents=["response text"])]) + ctx = SessionContext(input_messages=[Message(role="user", contents=["user input"])], session_id="s1") + ctx._response = response + + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_index.load.assert_called_once() + loaded = mock_index.load.call_args[0][0] + assert len(loaded) == 2 + roles = {d["role"] for d in loaded} + assert roles == {"user", "assistant"} + + async def test_skips_empty_conversations( + self, + mock_index: AsyncMock, + patch_index_from_dict: MagicMock, # noqa: ARG002 + ): + provider = _RedisContextProvider(source_id="ctx", user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", contents=[" "])], session_id="s1") + + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_index.load.assert_not_called() + + async def test_stores_partition_fields( + self, + mock_index: AsyncMock, + patch_index_from_dict: MagicMock, # noqa: ARG002 + ): + provider = _RedisContextProvider(source_id="ctx", application_id="app", agent_id="ag", user_id="u1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", contents=["hello"])], session_id="s1") + + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + loaded = mock_index.load.call_args[0][0] + doc = loaded[0] + assert doc["application_id"] == "app" + assert doc["agent_id"] == "ag" + assert doc["user_id"] == "u1" + assert doc["conversation_id"] == "s1" + + +class TestRedisContextProviderContextManager: + async def test_aenter_returns_self(self, patch_index_from_dict: MagicMock): # noqa: ARG002 + provider = _RedisContextProvider(source_id="ctx", user_id="u1") + async with provider as p: + assert p is provider + + +# =========================================================================== +# _RedisHistoryProvider tests +# =========================================================================== + + +class TestRedisHistoryProviderInit: + def test_basic_construction(self, mock_redis_client: MagicMock): + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("memory", redis_url="redis://localhost:6379") + + assert provider.source_id == "memory" + assert provider.key_prefix == "chat_messages" + assert provider.max_messages is None + assert provider.load_messages is True + assert provider.store_outputs is True + assert provider.store_inputs is True + + def test_custom_params(self, mock_redis_client: MagicMock): + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider( + "mem", + redis_url="redis://localhost:6379", + key_prefix="custom", + max_messages=50, + load_messages=False, + store_outputs=False, + store_inputs=False, + ) + + assert provider.key_prefix == "custom" + assert provider.max_messages == 50 + assert provider.load_messages is False + assert provider.store_outputs is False + assert provider.store_inputs is False + + def test_no_redis_url_or_credential_raises(self): + with pytest.raises(ValueError, match="Either redis_url or credential_provider must be provided"): + _RedisHistoryProvider("mem") + + def test_both_url_and_credential_raises(self): + mock_cred = MagicMock() + with pytest.raises(ValueError, match="mutually exclusive"): + _RedisHistoryProvider( + "mem", + redis_url="redis://localhost:6379", + credential_provider=mock_cred, + host="myhost", + ) + + def test_credential_provider_without_host_raises(self): + mock_cred = MagicMock() + with pytest.raises(ValueError, match="host is required"): + _RedisHistoryProvider("mem", credential_provider=mock_cred) + + def test_credential_provider_with_host(self): + mock_cred = MagicMock() + with patch("agent_framework_redis._history_provider.redis.Redis") as mock_redis_cls: + mock_redis_cls.return_value = MagicMock() + provider = _RedisHistoryProvider("mem", credential_provider=mock_cred, host="myhost") + + mock_redis_cls.assert_called_once_with( + host="myhost", + port=6380, + ssl=True, + username=None, + credential_provider=mock_cred, + decode_responses=True, + ) + assert provider.redis_url is None + + +class TestRedisHistoryProviderRedisKey: + def test_key_format(self, mock_redis_client: MagicMock): + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("mem", redis_url="redis://localhost:6379", key_prefix="msgs") + + assert provider._redis_key("session-123") == "msgs:session-123" + assert provider._redis_key(None) == "msgs:default" + + +class TestRedisHistoryProviderGetMessages: + async def test_returns_deserialized_messages(self, mock_redis_client: MagicMock): + msg1 = Message(role="user", contents=["Hello"]) + msg2 = Message(role="assistant", contents=["Hi!"]) + mock_redis_client.lrange = AsyncMock(return_value=[json.dumps(msg1.to_dict()), json.dumps(msg2.to_dict())]) + + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("mem", redis_url="redis://localhost:6379") + + messages = await provider.get_messages("s1") + assert len(messages) == 2 + assert messages[0].role == "user" + assert messages[0].text == "Hello" + assert messages[1].role == "assistant" + assert messages[1].text == "Hi!" + + async def test_empty_returns_empty(self, mock_redis_client: MagicMock): + mock_redis_client.lrange = AsyncMock(return_value=[]) + + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("mem", redis_url="redis://localhost:6379") + + messages = await provider.get_messages("s1") + assert messages == [] + + +class TestRedisHistoryProviderSaveMessages: + async def test_saves_serialized_messages(self, mock_redis_client: MagicMock): + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("mem", redis_url="redis://localhost:6379") + + msgs = [Message(role="user", contents=["Hello"]), Message(role="assistant", contents=["Hi"])] + await provider.save_messages("s1", msgs) + + pipeline = mock_redis_client.pipeline.return_value.__aenter__.return_value + assert pipeline.rpush.call_count == 2 + pipeline.execute.assert_called_once() + + async def test_empty_messages_noop(self, mock_redis_client: MagicMock): + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("mem", redis_url="redis://localhost:6379") + + await provider.save_messages("s1", []) + mock_redis_client.pipeline.assert_not_called() + + async def test_max_messages_trimming(self, mock_redis_client: MagicMock): + mock_redis_client.llen = AsyncMock(return_value=15) + + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("mem", redis_url="redis://localhost:6379", max_messages=10) + + await provider.save_messages("s1", [Message(role="user", contents=["msg"])]) + + mock_redis_client.ltrim.assert_called_once_with("chat_messages:s1", -10, -1) + + async def test_no_trim_when_under_limit(self, mock_redis_client: MagicMock): + mock_redis_client.llen = AsyncMock(return_value=3) + + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("mem", redis_url="redis://localhost:6379", max_messages=10) + + await provider.save_messages("s1", [Message(role="user", contents=["msg"])]) + + mock_redis_client.ltrim.assert_not_called() + + +class TestRedisHistoryProviderClear: + async def test_clear_calls_delete(self, mock_redis_client: MagicMock): + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("mem", redis_url="redis://localhost:6379") + + await provider.clear("session-1") + mock_redis_client.delete.assert_called_once_with("chat_messages:session-1") + + +class TestRedisHistoryProviderBeforeAfterRun: + """Test before_run/after_run integration via BaseHistoryProvider defaults.""" + + async def test_before_run_loads_history(self, mock_redis_client: MagicMock): + msg = Message(role="user", contents=["old msg"]) + mock_redis_client.lrange = AsyncMock(return_value=[json.dumps(msg.to_dict())]) + + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("mem", redis_url="redis://localhost:6379") + + session = AgentSession(session_id="test") + ctx = SessionContext(input_messages=[Message(role="user", contents=["new msg"])], session_id="s1") + + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + assert "mem" in ctx.context_messages + assert len(ctx.context_messages["mem"]) == 1 + assert ctx.context_messages["mem"][0].text == "old msg" + + async def test_after_run_stores_input_and_response(self, mock_redis_client: MagicMock): + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider("mem", redis_url="redis://localhost:6379") + + session = AgentSession(session_id="test") + ctx = SessionContext(input_messages=[Message(role="user", contents=["hi"])], session_id="s1") + ctx._response = AgentResponse(messages=[Message(role="assistant", contents=["hello"])]) + + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + pipeline = mock_redis_client.pipeline.return_value.__aenter__.return_value + assert pipeline.rpush.call_count == 2 + pipeline.execute.assert_called_once() + + async def test_after_run_skips_when_no_messages(self, mock_redis_client: MagicMock): + with patch("agent_framework_redis._history_provider.redis.from_url") as mock_from_url: + mock_from_url.return_value = mock_redis_client + provider = _RedisHistoryProvider( + "mem", redis_url="redis://localhost:6379", store_inputs=False, store_outputs=False + ) + + session = AgentSession(session_id="test") + ctx = SessionContext(input_messages=[Message(role="user", contents=["hi"])], session_id="s1") + + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] + + mock_redis_client.pipeline.assert_not_called() diff --git a/python/packages/redis/tests/test_redis_chat_message_store.py b/python/packages/redis/tests/test_redis_chat_message_store.py index 152d99fdf1..99a3038870 100644 --- a/python/packages/redis/tests/test_redis_chat_message_store.py +++ b/python/packages/redis/tests/test_redis_chat_message_store.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework_redis import RedisChatMessageStore @@ -19,9 +19,9 @@ class TestRedisChatMessageStore: def sample_messages(self): """Sample chat messages for testing.""" return [ - ChatMessage(role="user", text="Hello", message_id="msg1"), - ChatMessage(role="assistant", text="Hi there!", message_id="msg2"), - ChatMessage(role="user", text="How are you?", message_id="msg3"), + Message(role="user", text="Hello", message_id="msg1"), + Message(role="assistant", text="Hi there!", message_id="msg2"), + Message(role="user", text="How are you?", message_id="msg3"), ] @pytest.fixture @@ -250,7 +250,7 @@ class TestRedisChatMessageStore: store = RedisChatMessageStore(redis_url="redis://localhost:6379", thread_id="test123", max_messages=3) store._redis_client = mock_redis_client - message = ChatMessage(role="user", text="Test") + message = Message(role="user", text="Test") await store.add_messages([message]) # Should trim after adding to keep only last 3 messages @@ -269,8 +269,8 @@ class TestRedisChatMessageStore: """Test listing messages with data in Redis.""" # Create proper serialized messages using the actual serialization method test_messages = [ - ChatMessage(role="user", text="Hello", message_id="msg1"), - ChatMessage(role="assistant", text="Hi there!", message_id="msg2"), + Message(role="user", text="Hello", message_id="msg1"), + Message(role="assistant", text="Hi there!", message_id="msg2"), ] serialized_messages = [redis_store._serialize_message(msg) for msg in test_messages] mock_redis_client.lrange.return_value = serialized_messages @@ -411,7 +411,7 @@ class TestRedisChatMessageStore: store = RedisChatMessageStore(redis_url="redis://localhost:6379", thread_id="test123") # Message with multiple content types - message = ChatMessage( + message = Message( role="assistant", contents=[Content.from_text(text="Hello"), Content.from_text(text="World")], author_name="TestBot", @@ -444,7 +444,7 @@ class TestRedisChatMessageStore: store = RedisChatMessageStore(redis_url="redis://localhost:6379", thread_id="test123") store._redis_client = mock_client - message = ChatMessage(role="user", text="Test") + message = Message(role="user", text="Test") # Should propagate Redis connection errors with pytest.raises(Exception, match="Connection failed"): @@ -485,7 +485,7 @@ class TestRedisChatMessageStore: mock_redis_client.llen.return_value = 2 mock_redis_client.lset = AsyncMock() - new_message = ChatMessage(role="user", text="Updated message") + new_message = Message(role="user", text="Updated message") await redis_store.setitem(0, new_message) mock_redis_client.lset.assert_called_once() @@ -497,13 +497,13 @@ class TestRedisChatMessageStore: """Test setitem raises IndexError for invalid index.""" mock_redis_client.llen.return_value = 0 - new_message = ChatMessage(role="user", text="Test") + new_message = Message(role="user", text="Test") with pytest.raises(IndexError): await redis_store.setitem(0, new_message) async def test_append(self, redis_store, mock_redis_client): """Test append method delegates to add_messages.""" - message = ChatMessage(role="user", text="Appended message") + message = Message(role="user", text="Appended message") await redis_store.append(message) # Should call pipeline operations via add_messages @@ -572,7 +572,7 @@ class TestRedisChatMessageStore: mock_redis_client.llen.return_value = 1 mock_redis_client.lindex = AsyncMock(return_value="different_message") - with pytest.raises(ValueError, match="ChatMessage not found in store"): + with pytest.raises(ValueError, match="Message not found in store"): await redis_store.index(sample_messages[0]) async def test_remove(self, redis_store, mock_redis_client, sample_messages): @@ -589,7 +589,7 @@ class TestRedisChatMessageStore: """Test remove method when message is not found.""" mock_redis_client.lrem = AsyncMock(return_value=0) # 0 elements removed - with pytest.raises(ValueError, match="ChatMessage not found in store"): + with pytest.raises(ValueError, match="Message not found in store"): await redis_store.remove(sample_messages[0]) async def test_extend(self, redis_store, mock_redis_client, sample_messages): diff --git a/python/packages/redis/tests/test_redis_provider.py b/python/packages/redis/tests/test_redis_provider.py index 41ce7b37b8..8e842b3de7 100644 --- a/python/packages/redis/tests/test_redis_provider.py +++ b/python/packages/redis/tests/test_redis_provider.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import numpy as np import pytest -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework.exceptions import AgentException, ServiceInitializationError from redisvl.utils.vectorize import CustomTextVectorizer @@ -113,18 +113,18 @@ class TestRedisProviderInitialization: class TestRedisProviderMessages: @pytest.fixture - def sample_messages(self) -> list[ChatMessage]: + def sample_messages(self) -> list[Message]: return [ - ChatMessage(role="user", text="Hello, how are you?"), - ChatMessage(role="assistant", text="I'm doing well, thank you!"), - ChatMessage(role="system", text="You are a helpful assistant"), + Message(role="user", text="Hello, how are you?"), + Message(role="assistant", text="I'm doing well, thank you!"), + Message(role="system", text="You are a helpful assistant"), ] # Writes require at least one scoping filter to avoid unbounded operations async def test_messages_adding_requires_filters(self, patch_index_from_dict): # noqa: ARG002 provider = RedisProvider() with pytest.raises(ServiceInitializationError): - await provider.invoked("thread123", ChatMessage(role="user", text="Hello")) + await provider.invoked("thread123", Message(role="user", text="Hello")) # Captures the per-operation thread id when provided async def test_thread_created_sets_per_operation_id(self, patch_index_from_dict): # noqa: ARG002 @@ -157,7 +157,7 @@ class TestRedisProviderModelInvoking: async def test_model_invoking_requires_filters(self, patch_index_from_dict): # noqa: ARG002 provider = RedisProvider() with pytest.raises(ServiceInitializationError): - await provider.invoking(ChatMessage(role="user", text="Hi")) + await provider.invoking(Message(role="user", text="Hi")) # Ensures text-only search path is used and context is composed from hits async def test_textquery_path_and_context_contents( @@ -168,7 +168,7 @@ class TestRedisProviderModelInvoking: provider = RedisProvider(user_id="u1") # Act - ctx = await provider.invoking([ChatMessage(role="user", text="q1")]) + ctx = await provider.invoking([Message(role="user", text="q1")]) # Assert: TextQuery used (not HybridQuery), filter_expression included assert patch_queries["TextQuery"].call_count == 1 @@ -190,7 +190,7 @@ class TestRedisProviderModelInvoking: ): # noqa: ARG002 mock_index.query = AsyncMock(return_value=[]) provider = RedisProvider(user_id="u1") - ctx = await provider.invoking([ChatMessage(role="user", text="any")]) + ctx = await provider.invoking([Message(role="user", text="any")]) assert ctx.messages == [] # Ensures hybrid vector-text search is used when a vectorizer and vector field are configured @@ -198,7 +198,7 @@ class TestRedisProviderModelInvoking: mock_index.query = AsyncMock(return_value=[{"content": "Hit"}]) provider = RedisProvider(user_id="u1", redis_vectorizer=CUSTOM_VECTORIZER, vector_field_name="vec") - ctx = await provider.invoking([ChatMessage(role="user", text="hello")]) + ctx = await provider.invoking([Message(role="user", text="hello")]) # Assert: HybridQuery used with vector and vector field assert patch_queries["HybridQuery"].call_count == 1 @@ -240,9 +240,9 @@ class TestMessagesAddingBehavior: ) msgs = [ - ChatMessage(role="user", text="u"), - ChatMessage(role="assistant", text="a"), - ChatMessage(role="system", text="s"), + Message(role="user", text="u"), + Message(role="assistant", text="a"), + Message(role="system", text="s"), ] await provider.invoked(msgs) @@ -265,8 +265,8 @@ class TestMessagesAddingBehavior: ): # noqa: ARG002 provider = RedisProvider(user_id="u1", scope_to_per_operation_thread_id=True) msgs = [ - ChatMessage(role="user", text=" "), - ChatMessage(role="tool", text="tool output"), + Message(role="user", text=" "), + Message(role="tool", text="tool output"), ] await provider.invoked(msgs) # No valid messages -> no load @@ -279,8 +279,8 @@ class TestIndexCreationPublicCalls: self, mock_index: AsyncMock, patch_index_from_dict ): # noqa: ARG002 provider = RedisProvider(user_id="u1") - await provider.invoked(ChatMessage(role="user", text="m1")) - await provider.invoked(ChatMessage(role="user", text="m2")) + await provider.invoked(Message(role="user", text="m1")) + await provider.invoked(Message(role="user", text="m2")) # create only on first call assert mock_index.create.await_count == 1 @@ -291,7 +291,7 @@ class TestIndexCreationPublicCalls: mock_index.exists = AsyncMock(return_value=False) provider = RedisProvider(user_id="u1") mock_index.query = AsyncMock(return_value=[{"content": "C"}]) - await provider.invoking([ChatMessage(role="user", text="q")]) + await provider.invoking([Message(role="user", text="q")]) assert mock_index.create.await_count == 1 @@ -321,7 +321,7 @@ class TestVectorPopulation: vector_field_name="vec", ) - await provider.invoked(ChatMessage(role="user", text="hello")) + await provider.invoked(Message(role="user", text="hello")) assert mock_index.load.await_count == 1 (loaded_args, _kwargs) = mock_index.load.call_args docs = loaded_args[0] diff --git a/python/pyproject.toml b/python/pyproject.toml index af88ff92db..5c4fdd1788 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -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.0b260130" +version = "1.0.0b260210" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core[all]==1.0.0b260130", + "agent-framework-core[all]==1.0.0b260210", ] [dependency-groups] diff --git a/python/samples/autogen-migration/README.md b/python/samples/autogen-migration/README.md index 509b518f8a..36010fa223 100644 --- a/python/samples/autogen-migration/README.md +++ b/python/samples/autogen-migration/README.md @@ -6,7 +6,7 @@ This gallery helps AutoGen developers move to the Microsoft Agent Framework (AF) ### Single-Agent Parity -- [01_basic_assistant_agent.py](single_agent/01_basic_assistant_agent.py) — Minimal AutoGen `AssistantAgent` and AF `ChatAgent` comparison. +- [01_basic_assistant_agent.py](single_agent/01_basic_assistant_agent.py) — Minimal AutoGen `AssistantAgent` and AF `Agent` comparison. - [02_assistant_agent_with_tool.py](single_agent/02_assistant_agent_with_tool.py) — Function tool integration in both SDKs. - [03_assistant_agent_thread_and_stream.py](single_agent/03_assistant_agent_thread_and_stream.py) — Thread management and streaming responses. - [04_agent_as_tool.py](single_agent/04_agent_as_tool.py) — Using agents as tools (hierarchical agent pattern) and streaming with tools. @@ -51,7 +51,7 @@ python samples/autogen-migration/orchestrations/04_magentic_one.py ## Tips for Migration -- **Default behavior differences**: AutoGen's `AssistantAgent` is single-turn by default (`max_tool_iterations=1`), while AF's `ChatAgent` is multi-turn and continues tool execution automatically. +- **Default behavior differences**: AutoGen's `AssistantAgent` is single-turn by default (`max_tool_iterations=1`), while AF's `Agent` is multi-turn and continues tool execution automatically. - **Thread management**: AF agents are stateless by default. Use `agent.get_new_thread()` and pass it to `run()` to maintain conversation state, similar to AutoGen's conversation context. - **Tools**: AutoGen uses `FunctionTool` wrappers; AF uses `@tool` decorators with automatic schema inference. - **Orchestration patterns**: diff --git a/python/samples/autogen-migration/orchestrations/04_magentic_one.py b/python/samples/autogen-migration/orchestrations/04_magentic_one.py index f14cee5a26..a83f7cc33e 100644 --- a/python/samples/autogen-migration/orchestrations/04_magentic_one.py +++ b/python/samples/autogen-migration/orchestrations/04_magentic_one.py @@ -21,7 +21,7 @@ from typing import cast from agent_framework import ( AgentResponseUpdate, - ChatMessage, + Message, WorkflowEvent, ) from agent_framework.orchestrations import MagenticProgressLedger @@ -129,7 +129,7 @@ async def run_agent_framework() -> None: elif event.type == "magentic_orchestrator": print(f"\n[Magentic Orchestrator Event] Type: {event.data.event_type.name}") - if isinstance(event.data.content, ChatMessage): + if isinstance(event.data.content, Message): print(f"Please review the plan:\n{event.data.content.text}") elif isinstance(event.data.content, MagenticProgressLedger): print(f"Please review progress ledger:\n{json.dumps(event.data.content.to_dict(), indent=2)}") @@ -150,7 +150,7 @@ async def run_agent_framework() -> None: print("Final Output:") # The output of the Magentic workflow is a list of ChatMessages with only one final message # generated by the orchestrator. - output_messages = cast(list[ChatMessage], output_event.data) + output_messages = cast(list[Message], output_event.data) if output_messages: output = output_messages[-1].text print(output) diff --git a/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py b/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py index 711bd648c8..9f7ae98d6e 100644 --- a/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py +++ b/python/samples/autogen-migration/single_agent/01_basic_assistant_agent.py @@ -9,7 +9,7 @@ # uv run samples/autogen-migration/single_agent/01_basic_assistant_agent.py # Copyright (c) Microsoft. All rights reserved. -"""Basic AutoGen AssistantAgent vs Agent Framework ChatAgent. +"""Basic AutoGen AssistantAgent vs Agent Framework Agent. Both samples expect OpenAI-compatible environment variables (OPENAI_API_KEY or Azure OpenAI configuration). Update the prompts or client wiring to match your @@ -38,10 +38,10 @@ async def run_autogen() -> None: async def run_agent_framework() -> None: - """Call Agent Framework's ChatAgent created from OpenAIChatClient.""" + """Call Agent Framework's Agent created from OpenAIChatClient.""" from agent_framework.openai import OpenAIChatClient - # AF constructs a lightweight ChatAgent backed by OpenAIChatClient + # AF constructs a lightweight Agent backed by OpenAIChatClient client = OpenAIChatClient(model_id="gpt-4.1-mini") agent = client.as_agent( name="assistant", diff --git a/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py b/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py index ff56e694a0..cb027d636c 100644 --- a/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py +++ b/python/samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py @@ -10,7 +10,7 @@ # uv run samples/autogen-migration/single_agent/02_assistant_agent_with_tool.py # Copyright (c) Microsoft. All rights reserved. -"""AutoGen AssistantAgent vs Agent Framework ChatAgent with function tools. +"""AutoGen AssistantAgent vs Agent Framework Agent with function tools. Demonstrates how to create and attach tools to agents in both frameworks. """ diff --git a/python/samples/concepts/background_responses.py b/python/samples/concepts/background_responses.py new file mode 100644 index 0000000000..674c2439eb --- /dev/null +++ b/python/samples/concepts/background_responses.py @@ -0,0 +1,139 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import Agent +from agent_framework.openai import OpenAIResponsesClient + +"""Background Responses Sample. + +This sample demonstrates long-running agent operations using the OpenAI +Responses API ``background`` option. Two patterns are shown: + +1. **Non-streaming polling** – start a background run, then poll with the + ``continuation_token`` until the operation completes. +2. **Streaming with resumption** – start a background streaming run, simulate + an interruption, and resume from the last ``continuation_token``. + +Prerequisites: + - Set the ``OPENAI_API_KEY`` environment variable. + - A model that benefits from background execution (e.g. ``o3``). +""" + + +# 1. Create the agent with an OpenAI Responses client. +agent = Agent( + name="researcher", + instructions="You are a helpful research assistant. Be concise.", + client=OpenAIResponsesClient(model_id="o3"), +) + + +async def non_streaming_polling() -> None: + """Demonstrate non-streaming background run with polling.""" + print("=== Non-Streaming Polling ===\n") + + thread = agent.get_new_thread() + + # 2. Start a background run — returns immediately. + response = await agent.run( + messages="Briefly explain the theory of relativity in two sentences.", + thread=thread, + options={"background": True}, + ) + + print(f"Initial status: continuation_token={'set' if response.continuation_token else 'None'}") + + # 3. Poll until the operation completes. + poll_count = 0 + while response.continuation_token is not None: + poll_count += 1 + await asyncio.sleep(2) + response = await agent.run( + thread=thread, + options={"continuation_token": response.continuation_token}, + ) + print(f" Poll {poll_count}: continuation_token={'set' if response.continuation_token else 'None'}") + + # 4. Done — print the final result. + print(f"\nResult ({poll_count} poll(s)):\n{response.text}\n") + + +async def streaming_with_resumption() -> None: + """Demonstrate streaming background run with simulated interruption and resumption.""" + print("=== Streaming with Resumption ===\n") + + thread = agent.get_new_thread() + + # 2. Start a streaming background run. + last_token = None + stream = agent.run( + messages="Briefly list three benefits of exercise.", + stream=True, + thread=thread, + options={"background": True}, + ) + + # 3. Read some chunks, then simulate an interruption. + chunk_count = 0 + print("First stream (before interruption):") + async for update in stream: + last_token = update.continuation_token + if update.text: + print(update.text, end="", flush=True) + chunk_count += 1 + if chunk_count >= 3: + print("\n [simulated interruption]") + break + + # 4. Resume from the last continuation token. + if last_token is not None: + print("Resumed stream:") + stream = agent.run( + stream=True, + thread=thread, + options={"continuation_token": last_token}, + ) + async for update in stream: + if update.text: + print(update.text, end="", flush=True) + + print("\n") + + +async def main() -> None: + await non_streaming_polling() + await streaming_with_resumption() + + +if __name__ == "__main__": + asyncio.run(main()) + +""" +Sample output: + +=== Non-Streaming Polling === + +Initial status: continuation_token=set + Poll 1: continuation_token=set + Poll 2: continuation_token=None + +Result (2 poll(s)): +The theory of relativity, developed by Albert Einstein, consists of special +relativity (1905), which shows that the laws of physics are the same for all +non-accelerating observers and that the speed of light is constant, and general +relativity (1915), which describes gravity as the curvature of spacetime caused +by mass and energy. + +=== Streaming with Resumption === + +First stream (before interruption): +Here are three + [simulated interruption] +Resumed stream: +key benefits of regular exercise: + +1. **Improved cardiovascular health** ... +2. **Better mental health** ... +3. **Stronger muscles and bones** ... +""" diff --git a/python/samples/concepts/response_stream.py b/python/samples/concepts/response_stream.py index 6d99058062..1b26ac5e90 100644 --- a/python/samples/concepts/response_stream.py +++ b/python/samples/concepts/response_stream.py @@ -94,9 +94,9 @@ final = await response_stream.get_final_response() # Get the aggregated result === Chaining with .map() and .with_finalizer() === -When building a ChatAgent on top of a ChatClient, we face a challenge: +When building a Agent on top of a ChatClient, we face a challenge: - The ChatClient returns a ResponseStream[ChatResponseUpdate, ChatResponse] -- The ChatAgent needs to return a ResponseStream[AgentResponseUpdate, AgentResponse] +- The Agent needs to return a ResponseStream[AgentResponseUpdate, AgentResponse] - We can't iterate the ChatClient's stream twice! The `.map()` and `.with_finalizer()` methods solve this by creating new ResponseStreams that: @@ -123,8 +123,8 @@ provider notifications, telemetry, thread updates) are still executed even when stream is wrapped/mapped. ```python -# ChatAgent does something like this internally: -chat_stream = chat_client.get_response(messages, stream=True) +# Agent does something like this internally: +chat_stream = client.get_response(messages, stream=True) agent_stream = ( chat_stream .map(_to_agent_update, _to_agent_response) @@ -135,7 +135,7 @@ agent_stream = ( This ensures: - The underlying ChatClient stream is only consumed once - The agent can add its own transform hooks, result hooks, and cleanup logic -- Each layer (ChatClient, ChatAgent, middleware) can add independent behavior +- Each layer (ChatClient, Agent, middleware) can add independent behavior - Inner stream post-processing (like context provider notification) still runs - Types flow naturally through the chain """ @@ -281,7 +281,7 @@ async def main() -> None: # Simulate what ChatClient returns inner_stream = ResponseStream(generate_updates(), finalizer=combine_updates) - # Simulate what ChatAgent does: wrap the inner stream + # Simulate what Agent does: wrap the inner stream def to_agent_format(update: ChatResponseUpdate) -> ChatResponseUpdate: """Map ChatResponseUpdate to agent format (simulated transformation).""" # In real code, this would convert to AgentResponseUpdate diff --git a/python/samples/concepts/tools/README.md b/python/samples/concepts/tools/README.md index 6643a42126..91c481842d 100644 --- a/python/samples/concepts/tools/README.md +++ b/python/samples/concepts/tools/README.md @@ -20,7 +20,7 @@ sequenceDiagram participant Agent as Agent.run() participant AML as AgentMiddlewareLayer participant AMP as AgentMiddlewarePipeline - participant RawAgent as RawChatAgent.run() + participant RawAgent as RawAgent.run() participant CML as ChatMiddlewareLayer participant CMP as ChatMiddlewarePipeline participant FIL as FunctionInvocationLayer @@ -46,14 +46,14 @@ sequenceDiagram alt Non-Streaming (stream=False) RawAgent->>RawAgent: _prepare_run_context() [async] Note right of RawAgent: Builds: thread_messages, chat_options, tools - RawAgent->>CML: chat_client.get_response(stream=False) + RawAgent->>CML: client.get_response(stream=False) else Streaming (stream=True) RawAgent->>RawAgent: ResponseStream.from_awaitable() Note right of RawAgent: Defers async prep to stream consumption RawAgent-->>User: Returns ResponseStream immediately Note over RawAgent,CML: Async work happens on iteration RawAgent->>RawAgent: _prepare_run_context() [deferred] - RawAgent->>CML: chat_client.get_response(stream=True) + RawAgent->>CML: client.get_response(stream=True) end Note over CML,CMP: Chat Middleware Layer @@ -132,7 +132,7 @@ sequenceDiagram | Field | Type | Description | |-------|------|-------------| | `agent` | `SupportsAgentRun` | The agent being invoked | -| `messages` | `list[ChatMessage]` | Input messages (mutable) | +| `messages` | `list[Message]` | Input messages (mutable) | | `thread` | `AgentThread \| None` | Conversation thread | | `options` | `Mapping[str, Any]` | Chat options dict | | `stream` | `bool` | Whether streaming is enabled | @@ -142,9 +142,9 @@ sequenceDiagram **Key Operations:** 1. `categorize_middleware()` separates middleware by type (agent, chat, function) -2. Chat and function middleware are forwarded to `chat_client` +2. Chat and function middleware are forwarded to `client` 3. `AgentMiddlewarePipeline.execute()` runs the agent middleware chain -4. Final handler calls `RawChatAgent.run()` +4. Final handler calls `RawAgent.run()` **What Can Be Modified:** - `context.messages` - Add, remove, or modify input messages @@ -154,14 +154,14 @@ sequenceDiagram ### 2. Chat Middleware Layer (`ChatMiddlewareLayer`) -**Entry Point:** `chat_client.get_response(messages, options)` +**Entry Point:** `client.get_response(messages, options)` **Context Object:** `ChatContext` | Field | Type | Description | |-------|------|-------------| -| `chat_client` | `ChatClientProtocol` | The chat client | -| `messages` | `Sequence[ChatMessage]` | Messages to send | +| `client` | `SupportsChatGetResponse` | The chat client | +| `messages` | `Sequence[Message]` | Messages to send | | `options` | `Mapping[str, Any]` | Chat options | | `stream` | `bool` | Whether streaming | | `metadata` | `dict` | Shared data between middleware | @@ -275,7 +275,7 @@ class TerminatingMiddleware(FunctionMiddleware): ### Agent Layer → Chat Layer ```python -# RawChatAgent._prepare_run_context() builds: +# RawAgent._prepare_run_context() builds: { "thread": AgentThread, # Validated/created thread "input_messages": [...], # Normalized input messages @@ -463,7 +463,7 @@ Returns `Awaitable[AgentResponse]`: ```python async def _run_non_streaming(): ctx = await self._prepare_run_context(...) # Async preparation - response = await self.chat_client.get_response(stream=False, ...) + response = await self.client.get_response(stream=False, ...) await self._finalize_response_and_update_thread(...) return AgentResponse(...) ``` @@ -476,7 +476,7 @@ Returns `ResponseStream[AgentResponseUpdate, AgentResponse]` **synchronously**: # Async preparation is deferred using ResponseStream.from_awaitable() async def _get_stream(): ctx = await self._prepare_run_context(...) # Deferred until iteration - return self.chat_client.get_response(stream=True, ...) + return self.client.get_response(stream=True, ...) return ( ResponseStream.from_awaitable(_get_stream()) diff --git a/python/samples/concepts/typed_options.py b/python/samples/concepts/typed_options.py index 533b214ebe..e111222601 100644 --- a/python/samples/concepts/typed_options.py +++ b/python/samples/concepts/typed_options.py @@ -3,13 +3,13 @@ import asyncio from typing import Literal -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.anthropic import AnthropicClient from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions """TypedDict-based Chat Options. -In Agent Framework, we have made ChatClient and ChatAgent generic over a ChatOptions typeddict, this means that +In Agent Framework, we have made ChatClient and Agent generic over a ChatOptions typeddict, this means that you can override which options are available for a given client or agent by providing your own TypedDict subclass. And we include the most common options for all ChatClient providers out of the box. @@ -21,7 +21,7 @@ which provides: including overriding unsupported options. The sample shows usage with both OpenAI and Anthropic clients, demonstrating -how provider-specific options work for ChatClient and ChatAgent. But the same approach works for other providers too. +how provider-specific options work for ChatClient and Agent. But the same approach works for other providers too. """ @@ -49,14 +49,14 @@ async def demo_anthropic_chat_client() -> None: async def demo_anthropic_agent() -> None: - """Demonstrate ChatAgent with Anthropic client and typed options.""" - print("\n=== ChatAgent with Anthropic and Typed Options ===\n") + """Demonstrate Agent with Anthropic client and typed options.""" + print("\n=== Agent with Anthropic and Typed Options ===\n") client = AnthropicClient(model_id="claude-sonnet-4-5-20250929") # Create a typed agent for Anthropic - IDE knows Anthropic-specific options! - agent = ChatAgent( - chat_client=client, + agent = Agent( + client=client, name="claude-assistant", instructions="You are a helpful assistant powered by Claude. Be concise.", default_options={ @@ -132,15 +132,15 @@ async def demo_openai_chat_client_reasoning_models() -> None: async def demo_openai_agent() -> None: - """Demonstrate ChatAgent with OpenAI client and typed options.""" - print("\n=== ChatAgent with OpenAI and Typed Options ===\n") + """Demonstrate Agent with OpenAI client and typed options.""" + print("\n=== Agent with OpenAI and Typed Options ===\n") # Create a typed agent - IDE will autocomplete options! # The type annotation can be done either on the agent like below, # or on the client when constructing the client instance: # client = OpenAIChatClient[OpenAIReasoningChatOptions]() - agent = ChatAgent[OpenAIReasoningChatOptions]( - chat_client=OpenAIChatClient(), + agent = Agent[OpenAIReasoningChatOptions]( + client=OpenAIChatClient(), name="weather-assistant", instructions="You are a helpful assistant. Answer concisely.", # Options can be set at construction time diff --git a/python/samples/demos/chatkit-integration/README.md b/python/samples/demos/chatkit-integration/README.md index 9636c4b190..d688eb3a6c 100644 --- a/python/samples/demos/chatkit-integration/README.md +++ b/python/samples/demos/chatkit-integration/README.md @@ -38,7 +38,7 @@ graph TB subgraph Integration["Agent Framework Integration"] Converter[ThreadItemConverter] Streamer[stream_agent_response] - Agent[ChatAgent] + Agent[Agent] end Widgets[Widget Rendering
render_weather_widget
render_city_selector_widget] @@ -61,7 +61,7 @@ graph TB AttStore -.->|save files| Files AttStore -.->|save metadata| SQLite - Converter -->|ChatMessage array| Agent + Converter -->|Message array| Agent Agent -->|AgentResponseUpdate| Streamer Streamer -->|ThreadStreamEvent| ChatKit @@ -88,7 +88,7 @@ The sample implements a ChatKit server using the `ChatKitServer` base class from - **`WeatherChatKitServer`**: Custom ChatKit server implementation that: - Extends `ChatKitServer[dict[str, Any]]` - - Uses Agent Framework's `ChatAgent` with Azure OpenAI + - Uses Agent Framework's `Agent` with Azure OpenAI - Converts ChatKit messages to Agent Framework format using `ThreadItemConverter` - Streams responses back to ChatKit using `stream_agent_response` - Creates and streams interactive widgets after agent responses diff --git a/python/samples/demos/chatkit-integration/app.py b/python/samples/demos/chatkit-integration/app.py index 44a2e125f6..8167bb74b6 100644 --- a/python/samples/demos/chatkit-integration/app.py +++ b/python/samples/demos/chatkit-integration/app.py @@ -28,7 +28,7 @@ from typing import Annotated, Any import uvicorn # Agent Framework imports -from agent_framework import AgentResponseUpdate, ChatAgent, ChatMessage, tool +from agent_framework import Agent, AgentResponseUpdate, FunctionResultContent, Message, Role, tool from agent_framework.azure import AzureOpenAIChatClient # Agent Framework ChatKit integration @@ -217,8 +217,8 @@ class WeatherChatKitServer(ChatKitServer[dict[str, Any]]): # Create Agent Framework agent with Azure OpenAI # For authentication, run `az login` command in terminal try: - self.weather_agent = ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + self.weather_agent = Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions=( "You are a helpful weather assistant with image analysis capabilities. " "You can provide weather information for any location, tell the current time, " @@ -290,8 +290,8 @@ class WeatherChatKitServer(ChatKitServer[dict[str, Any]]): conversation_context = "\n".join(user_messages[:3]) title_prompt = [ - ChatMessage( - role="user", + Message( + role=Role.USER, text=( f"Generate a very short, concise title (max 40 characters) for a conversation " f"that starts with:\n\n{conversation_context}\n\n" @@ -301,7 +301,7 @@ class WeatherChatKitServer(ChatKitServer[dict[str, Any]]): ] # Use the chat client directly for a quick, lightweight call - response = await self.weather_agent.chat_client.get_response( + response = await self.weather_agent.client.get_response( messages=title_prompt, options={ "temperature": 0.3, @@ -342,6 +342,7 @@ class WeatherChatKitServer(ChatKitServer[dict[str, Any]]): runs the agent, converts the response back to ChatKit events using stream_agent_response, and creates interactive weather widgets when weather data is queried. """ + from agent_framework import FunctionResultContent if input_user_message is None: logger.debug("Received None user message, skipping") @@ -384,7 +385,7 @@ class WeatherChatKitServer(ChatKitServer[dict[str, Any]]): # Check for function results in the update if update.contents: for content in update.contents: - if content.type == "function_result": + if isinstance(content, FunctionResultContent): result = content.result # Check if it's a WeatherResponse (string subclass with weather_data attribute) @@ -467,7 +468,7 @@ class WeatherChatKitServer(ChatKitServer[dict[str, Any]]): weather_data: WeatherData | None = None # Create an agent message asking about the weather - agent_messages = [ChatMessage(role="user", text=f"What's the weather in {city_label}?")] + agent_messages = [Message(role=Role.USER, text=f"What's the weather in {city_label}?")] logger.debug(f"Processing weather query: {agent_messages[0].text}") @@ -481,7 +482,7 @@ class WeatherChatKitServer(ChatKitServer[dict[str, Any]]): # Check for function results in the update if update.contents: for content in update.contents: - if content.type == "function_result": + if isinstance(content, FunctionResultContent): result = content.result # Check if it's a WeatherResponse (string subclass with weather_data attribute) @@ -572,7 +573,7 @@ async def chatkit_endpoint(request: Request): @app.post("/upload/{attachment_id}") -async def upload_file(attachment_id: str, file: Annotated[UploadFile, File()]): +async def upload_file(attachment_id: str, file: UploadFile = File(...)): # noqa: B008 """Handle file upload for two-phase upload. The client POSTs the file bytes here after creating the attachment @@ -594,7 +595,7 @@ async def upload_file(attachment_id: str, file: Annotated[UploadFile, File()]): attachment = await data_store.load_attachment(attachment_id, {"user_id": DEFAULT_USER_ID}) # Clear the upload_url since upload is complete - attachment.upload_url = None # type: ignore[union-attr] + attachment.upload_url = None # Save the updated attachment back to the store await data_store.save_attachment(attachment, {"user_id": DEFAULT_USER_ID}) diff --git a/python/samples/demos/hosted_agents/agent_with_hosted_mcp/main.py b/python/samples/demos/hosted_agents/agent_with_hosted_mcp/main.py index 49f75a6df4..3118addc5b 100644 --- a/python/samples/demos/hosted_agents/agent_with_hosted_mcp/main.py +++ b/python/samples/demos/hosted_agents/agent_with_hosted_mcp/main.py @@ -1,20 +1,23 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework import HostedMCPTool from agent_framework.azure import AzureOpenAIChatClient from azure.ai.agentserver.agentframework import from_agent_framework # pyright: ignore[reportUnknownVariableType] from azure.identity import DefaultAzureCredential def main(): + # Create MCP tool configuration as dict + mcp_tool = { + "type": "mcp", + "server_label": "Microsoft_Learn_MCP", + "server_url": "https://learn.microsoft.com/api/mcp", + } + # Create an Agent using the Azure OpenAI Chat Client with a MCP Tool that connects to Microsoft Learn MCP agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent( name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ), + tools=mcp_tool, ) # Run the agent as a hosted agent diff --git a/python/samples/demos/hosted_agents/agent_with_text_search_rag/main.py b/python/samples/demos/hosted_agents/agent_with_text_search_rag/main.py index 0c0660ceb0..8e6c77d712 100644 --- a/python/samples/demos/hosted_agents/agent_with_text_search_rag/main.py +++ b/python/samples/demos/hosted_agents/agent_with_text_search_rag/main.py @@ -6,7 +6,7 @@ from collections.abc import MutableSequence from dataclasses import dataclass from typing import Any -from agent_framework import ChatMessage, Context, ContextProvider +from agent_framework import Context, ContextProvider, Message from agent_framework.azure import AzureOpenAIChatClient from azure.ai.agentserver.agentframework import from_agent_framework # pyright: ignore[reportUnknownVariableType] from azure.identity import DefaultAzureCredential @@ -27,16 +27,16 @@ class TextSearchResult: class TextSearchContextProvider(ContextProvider): """A simple context provider that simulates text search results based on keywords in the user's message.""" - def _get_most_recent_message(self, messages: ChatMessage | MutableSequence[ChatMessage]) -> ChatMessage: + def _get_most_recent_message(self, messages: Message | MutableSequence[Message]) -> Message: """Helper method to extract the most recent message from the input.""" - if isinstance(messages, ChatMessage): + if isinstance(messages, Message): return messages if messages: return messages[-1] raise ValueError("No messages provided") @override - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: message = self._get_most_recent_message(messages) query = message.text.lower() @@ -84,7 +84,7 @@ class TextSearchContextProvider(ContextProvider): return Context( messages=[ - ChatMessage( + Message( role="user", text="\n\n".join(json.dumps(result.__dict__, indent=2) for result in results) ) ] diff --git a/python/samples/demos/m365-agent/m365_agent_demo/app.py b/python/samples/demos/m365-agent/m365_agent_demo/app.py index 212941efa7..d4c6460652 100644 --- a/python/samples/demos/m365-agent/m365_agent_demo/app.py +++ b/python/samples/demos/m365-agent/m365_agent_demo/app.py @@ -18,7 +18,7 @@ from dataclasses import dataclass from random import randint from typing import Annotated -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIChatClient from aiohttp import web from aiohttp.web_middlewares import middleware @@ -95,7 +95,7 @@ def get_weather( return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." -def build_agent() -> ChatAgent: +def build_agent() -> Agent: """Create and return the chat agent instance with weather tool registered.""" return OpenAIChatClient().as_agent( name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather diff --git a/python/samples/demos/workflow_evaluation/create_workflow.py b/python/samples/demos/workflow_evaluation/create_workflow.py index 2eb31d3492..d1f679b778 100644 --- a/python/samples/demos/workflow_evaluation/create_workflow.py +++ b/python/samples/demos/workflow_evaluation/create_workflow.py @@ -48,8 +48,8 @@ from _tools import ( from agent_framework import ( AgentExecutorResponse, AgentResponseUpdate, - ChatMessage, Executor, + Message, WorkflowBuilder, WorkflowContext, executor, @@ -65,17 +65,17 @@ load_dotenv() @executor(id="start_executor") -async def start_executor(input: str, ctx: WorkflowContext[list[ChatMessage]]) -> None: +async def start_executor(input: str, ctx: WorkflowContext[list[Message]]) -> None: """Initiates the workflow by sending the user query to all specialized agents.""" - await ctx.send_message([ChatMessage("user", [input])]) + await ctx.send_message([Message("user", [input])]) class ResearchLead(Executor): """Aggregates and summarizes travel planning findings from all specialized agents.""" - def __init__(self, chat_client: AzureAIClient, id: str = "travel-planning-coordinator"): + def __init__(self, client: AzureAIClient, id: str = "travel-planning-coordinator"): # store=True to preserve conversation history for evaluation - self.agent = chat_client.as_agent( + self.agent = client.as_agent( id="travel-planning-coordinator", instructions=( "You are the final coordinator. You will receive responses from multiple agents: " @@ -102,11 +102,11 @@ class ResearchLead(Executor): # Generate comprehensive travel plan summary messages = [ - ChatMessage( + Message( role="system", text="You are a travel planning coordinator. Summarize findings from multiple specialized travel agents and provide a clear, comprehensive travel plan based on the user's query.", ), - ChatMessage( + Message( role="user", text=f"Original query: {user_query}\n\nFindings from specialized travel agents:\n{summary_text}\n\nPlease provide a comprehensive travel plan based on these findings.", ), @@ -142,17 +142,17 @@ class ResearchLead(Executor): return agent_findings -async def run_workflow_with_response_tracking(query: str, chat_client: AzureAIClient | None = None) -> dict: +async def run_workflow_with_response_tracking(query: str, client: AzureAIClient | None = None) -> dict: """Run multi-agent workflow and track conversation IDs, response IDs, and interaction sequence. Args: query: The user query to process through the multi-agent workflow - chat_client: Optional AzureAIClient instance + client: Optional AzureAIClient instance Returns: Dictionary containing interaction sequence, conversation/response IDs, and conversation analysis """ - if chat_client is None: + if client is None: try: async with DefaultAzureCredential() as credential: # Create AIProjectClient with the correct API version for V2 prompt agents @@ -171,10 +171,10 @@ async def run_workflow_with_response_tracking(query: str, chat_client: AzureAICl print(f"Error during workflow execution: {e}") raise else: - return await _run_workflow_with_client(query, chat_client) + return await _run_workflow_with_client(query, client) -async def _run_workflow_with_client(query: str, chat_client: AzureAIClient) -> dict: +async def _run_workflow_with_client(query: str, client: AzureAIClient) -> dict: """Execute workflow with given client and track all interactions.""" # Initialize tracking variables - use lists to track multiple responses per agent @@ -184,7 +184,7 @@ async def _run_workflow_with_client(query: str, chat_client: AzureAIClient) -> d # Create workflow components and keep agent references # Pass project_client and credential to create separate client instances per agent - workflow, agent_map = await _create_workflow(chat_client.project_client, chat_client.credential) + workflow, agent_map = await _create_workflow(client.project_client, client.credential) # Process workflow events events = workflow.run(query, stream=True) @@ -210,7 +210,7 @@ async def _create_workflow(project_client, credential): final_coordinator_client = AzureAIClient( project_client=project_client, credential=credential, agent_name="final-coordinator" ) - final_coordinator = ResearchLead(chat_client=final_coordinator_client, id="final-coordinator") + final_coordinator = ResearchLead(client=final_coordinator_client, id="final-coordinator") # Agent 1: Travel Request Handler (initial coordinator) # Create separate client with unique agent_name diff --git a/python/samples/getting_started/agents/a2a/README.md b/python/samples/getting_started/agents/a2a/README.md index 6900100703..d774b3c877 100644 --- a/python/samples/getting_started/agents/a2a/README.md +++ b/python/samples/getting_started/agents/a2a/README.md @@ -2,12 +2,15 @@ This folder contains examples demonstrating how to create and use agents with the A2A (Agent2Agent) protocol from the `agent_framework` package to communicate with remote A2A agents. +By default the A2AAgent waits for the remote agent to finish before returning (`background=False`), so long-running A2A tasks are handled transparently. For advanced scenarios where you need to poll or resubscribe to in-progress tasks using continuation tokens, see the [background responses sample](../../../concepts/background_responses.py). + For more information about the A2A protocol specification, visit: https://a2a-protocol.org/latest/ + ## Examples | File | Description | |------|-------------| -| [`agent_with_a2a.py`](agent_with_a2a.py) | The simplest way to connect to and use a single A2A agent. Demonstrates agent discovery via agent cards and basic message exchange using the A2A protocol. | +| [`agent_with_a2a.py`](agent_with_a2a.py) | Demonstrates agent discovery, non-streaming and streaming responses using the A2A protocol. | ## Environment Variables diff --git a/python/samples/getting_started/agents/a2a/agent_with_a2a.py b/python/samples/getting_started/agents/a2a/agent_with_a2a.py index 2f0e6b33d2..4250104b9f 100644 --- a/python/samples/getting_started/agents/a2a/agent_with_a2a.py +++ b/python/samples/getting_started/agents/a2a/agent_with_a2a.py @@ -15,13 +15,18 @@ the A2A protocol. A2A is a standardized communication protocol that enables inte between different agent systems, allowing agents built with different frameworks and technologies to communicate seamlessly. +By default the A2AAgent waits for the remote agent to finish before returning (background=False). +This means long-running A2A tasks are handled transparently — the caller simply awaits the result. +For advanced scenarios where you need to poll or resubscribe to in-progress tasks, see the +background_responses sample: samples/concepts/background_responses.py + For more information about the A2A protocol specification, visit: https://a2a-protocol.org/latest/ Key concepts demonstrated: - Discovering A2A-compliant agents using AgentCard resolution - Creating A2AAgent instances to wrap external A2A endpoints -- Converting Agent Framework messages to A2A protocol format -- Handling A2A responses (Messages and Tasks) back to framework types +- Non-streaming request/response +- Streaming responses to receive incremental updates via SSE To run this sample: 1. Set the A2A_AGENT_HOST environment variable to point to an A2A-compliant agent endpoint @@ -29,50 +34,75 @@ To run this sample: 2. Ensure the target agent exposes its AgentCard at /.well-known/agent.json 3. Run: uv run python agent_with_a2a.py -The sample will: -- Connect to the specified A2A agent endpoint -- Retrieve and parse the agent's capabilities via its AgentCard -- Send a message using the A2A protocol -- Display the agent's response - Visit the README.md for more details on setting up and running A2A agents. """ async def main(): """Demonstrates connecting to and communicating with an A2A-compliant agent.""" - # Get A2A agent host from environment + # 1. Get A2A agent host from environment. a2a_agent_host = os.getenv("A2A_AGENT_HOST") if not a2a_agent_host: raise ValueError("A2A_AGENT_HOST environment variable is not set") print(f"Connecting to A2A agent at: {a2a_agent_host}") - # Initialize A2ACardResolver + # 2. Resolve the agent card to discover capabilities. async with httpx.AsyncClient(timeout=60.0) as http_client: resolver = A2ACardResolver(httpx_client=http_client, base_url=a2a_agent_host) - - # Get agent card agent_card = await resolver.get_agent_card() print(f"Found agent: {agent_card.name} - {agent_card.description}") - # Create A2A agent instance - agent = A2AAgent( - name=agent_card.name, - description=agent_card.description, - agent_card=agent_card, - url=a2a_agent_host, - ) - - # Invoke the agent and output the result - print("\nSending message to A2A agent...") + # 3. Create A2A agent instance. + async with A2AAgent( + name=agent_card.name, + description=agent_card.description, + agent_card=agent_card, + url=a2a_agent_host, + ) as agent: + # 4. Simple request/response — the agent waits for completion internally. + # Even if the remote agent takes a while, background=False (the default) + # means the call blocks until a terminal state is reached. + print("\n--- Non-streaming response ---") response = await agent.run("What are your capabilities?") - # Print the response - print("\nAgent Response:") + print("Agent Response:") for message in response.messages: - print(message.text) + print(f" {message.text}") + + # 5. Stream a response — the natural model for A2A. + # Updates arrive as Server-Sent Events, letting you observe + # progress in real time as the remote agent works. + print("\n--- Streaming response ---") + async with agent.run("Tell me about yourself", stream=True) as stream: + async for update in stream: + for content in update.contents: + if content.text: + print(f" {content.text}") + + response = await stream.get_final_response() + print(f"\nFinal response ({len(response.messages)} message(s)):") + for message in response.messages: + print(f" {message.text}") if __name__ == "__main__": asyncio.run(main()) + + +""" +Sample output: + +Connecting to A2A agent at: http://localhost:5001/ +Found agent: MyAgent - A helpful AI assistant + +--- Non-streaming response --- +Agent Response: + I can help with code generation, analysis, and general Q&A. + +--- Streaming response --- + I am an AI assistant built to help with various tasks. + +Final response (1 message(s)): + I am an AI assistant built to help with various tasks. +""" diff --git a/python/samples/getting_started/agents/anthropic/anthropic_advanced.py b/python/samples/getting_started/agents/anthropic/anthropic_advanced.py index 8d15c2d91e..3918005b5d 100644 --- a/python/samples/getting_started/agents/anthropic/anthropic_advanced.py +++ b/python/samples/getting_started/agents/anthropic/anthropic_advanced.py @@ -2,7 +2,6 @@ import asyncio -from agent_framework import HostedMCPTool, HostedWebSearchTool from agent_framework.anthropic import AnthropicChatOptions, AnthropicClient """ @@ -17,16 +16,21 @@ This sample demonstrates using Anthropic with: async def main() -> None: """Example of streaming response (get results as they are generated).""" - agent = AnthropicClient[AnthropicChatOptions]().as_agent( + client = AnthropicClient[AnthropicChatOptions]() + + # Create MCP tool configuration using instance method + mcp_tool = client.get_mcp_tool( + name="Microsoft_Learn_MCP", + url="https://learn.microsoft.com/api/mcp", + ) + + # Create web search tool configuration using instance method + web_search_tool = client.get_web_search_tool() + + agent = client.as_agent( name="DocsAgent", instructions="You are a helpful agent for both Microsoft docs questions and general questions.", - tools=[ - HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ), - HostedWebSearchTool(), - ], + tools=[mcp_tool, web_search_tool], default_options={ # anthropic needs a value for the max_tokens parameter # we set it to 1024, but you can override like this: diff --git a/python/samples/getting_started/agents/anthropic/anthropic_foundry.py b/python/samples/getting_started/agents/anthropic/anthropic_foundry.py index c9064dbe57..00f5c5f2e0 100644 --- a/python/samples/getting_started/agents/anthropic/anthropic_foundry.py +++ b/python/samples/getting_started/agents/anthropic/anthropic_foundry.py @@ -2,7 +2,6 @@ import asyncio -from agent_framework import HostedMCPTool, HostedWebSearchTool from agent_framework.anthropic import AnthropicClient from anthropic import AsyncAnthropicFoundry @@ -28,16 +27,21 @@ To use the Foundry integration ensure you have the following environment variabl async def main() -> None: """Example of streaming response (get results as they are generated).""" - agent = AnthropicClient(anthropic_client=AsyncAnthropicFoundry()).as_agent( + client = AnthropicClient(anthropic_client=AsyncAnthropicFoundry()) + + # Create MCP tool configuration using instance method + mcp_tool = client.get_mcp_tool( + name="Microsoft_Learn_MCP", + url="https://learn.microsoft.com/api/mcp", + ) + + # Create web search tool configuration using instance method + web_search_tool = client.get_web_search_tool() + + agent = client.as_agent( name="DocsAgent", instructions="You are a helpful agent for both Microsoft docs questions and general questions.", - tools=[ - HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ), - HostedWebSearchTool(), - ], + tools=[mcp_tool, web_search_tool], default_options={ # anthropic needs a value for the max_tokens parameter # we set it to 1024, but you can override like this: diff --git a/python/samples/getting_started/agents/anthropic/anthropic_skills.py b/python/samples/getting_started/agents/anthropic/anthropic_skills.py index 108646543a..3b014f9b6a 100644 --- a/python/samples/getting_started/agents/anthropic/anthropic_skills.py +++ b/python/samples/getting_started/agents/anthropic/anthropic_skills.py @@ -4,7 +4,7 @@ import asyncio import logging from pathlib import Path -from agent_framework import Content, HostedCodeInterpreterTool +from agent_framework import Content from agent_framework.anthropic import AnthropicChatOptions, AnthropicClient logger = logging.getLogger(__name__) @@ -34,7 +34,7 @@ async def main() -> None: agent = client.as_agent( name="DocsAgent", instructions="You are a helpful agent for creating powerpoint presentations.", - tools=HostedCodeInterpreterTool(), + tools=client.get_code_interpreter_tool(), default_options={ "max_tokens": 20000, "thinking": {"type": "enabled", "budget_tokens": 10000}, diff --git a/python/samples/getting_started/agents/azure_ai/README.md b/python/samples/getting_started/agents/azure_ai/README.md index df20485ce1..55724e39fd 100644 --- a/python/samples/getting_started/agents/azure_ai/README.md +++ b/python/samples/getting_started/agents/azure_ai/README.md @@ -15,7 +15,7 @@ This folder contains examples demonstrating different ways to create and use age | [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to search the web for current information and provide grounded responses with citations. Requires a Bing connection configured in your Azure AI project. | | [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to search custom search instances and provide responses with relevant results. Requires a Bing Custom Search connection and instance configured in your Azure AI project. | | [`azure_ai_with_browser_automation.py`](azure_ai_with_browser_automation.py) | Shows how to use Browser Automation with Azure AI agents to perform automated web browsing tasks and provide responses based on web interactions. Requires a Browser Automation connection configured in your Azure AI project. | -| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the `HostedCodeInterpreterTool` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. | +| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use `AzureAIClient.get_code_interpreter_tool()` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. | | [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. | | [`azure_ai_with_code_interpreter_file_download.py`](azure_ai_with_code_interpreter_file_download.py) | Shows how to download files generated by code interpreter using the OpenAI containers API. | | [`azure_ai_with_content_filtering.py`](azure_ai_with_content_filtering.py) | Shows how to enable content filtering (RAI policy) on Azure AI agents using `RaiConfig`. Requires creating an RAI policy in Azure AI Foundry portal first. | @@ -23,8 +23,8 @@ This folder contains examples demonstrating different ways to create and use age | [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Demonstrates how to use an existing conversation created on the service side with Azure AI agents. Shows two approaches: specifying conversation ID at the client level and using AgentThread with an existing conversation ID. | | [`azure_ai_with_application_endpoint.py`](azure_ai_with_application_endpoint.py) | Demonstrates calling the Azure AI application-scoped endpoint. | | [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIClient` settings, including project endpoint, model deployment, and credentials rather than relying on environment variable defaults. | -| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use the `HostedFileSearchTool` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. | -| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent. | +| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use `AzureAIClient.get_file_search_tool()` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. | +| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent using `AzureAIClient.get_mcp_tool()`. | | [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate local Model Context Protocol (MCP) tools with Azure AI agents. | | [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. | | [`azure_ai_with_runtime_json_schema.py`](azure_ai_with_runtime_json_schema.py) | Shows how to use structured outputs (response format) with Azure AI agents using a JSON schema to enforce specific response schemas. | @@ -32,12 +32,12 @@ This folder contains examples demonstrating different ways to create and use age | [`azure_ai_with_search_context_semantic.py`](../../context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py) | Shows how to use AzureAISearchContextProvider with semantic mode. Fast hybrid search with vector + keyword search and semantic ranking for RAG. Best for simple queries where speed is critical. | | [`azure_ai_with_sharepoint.py`](azure_ai_with_sharepoint.py) | Shows how to use SharePoint grounding with Azure AI agents to search through SharePoint content and answer user questions with proper citations. Requires a SharePoint connection configured in your Azure AI project. | | [`azure_ai_with_thread.py`](azure_ai_with_thread.py) | Demonstrates thread management with Azure AI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | -| [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use the `ImageGenTool` with Azure AI agents to generate images based on text prompts. | +| [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use `AzureAIClient.get_image_generation_tool()` with Azure AI agents to generate images based on text prompts. | | [`azure_ai_with_memory_search.py`](azure_ai_with_memory_search.py) | Shows how to use memory search functionality with Azure AI agents for conversation persistence. Demonstrates creating memory stores and enabling agents to search through conversation history. | | [`azure_ai_with_microsoft_fabric.py`](azure_ai_with_microsoft_fabric.py) | Shows how to use Microsoft Fabric with Azure AI agents to query Fabric data sources and provide responses based on data analysis. Requires a Microsoft Fabric connection configured in your Azure AI project. | | [`azure_ai_with_openapi.py`](azure_ai_with_openapi.py) | Shows how to integrate OpenAPI specifications with Azure AI agents using dictionary-based tool configuration. Demonstrates using external REST APIs for dynamic data lookup. | | [`azure_ai_with_reasoning.py`](azure_ai_with_reasoning.py) | Shows how to enable reasoning for a model that supports it. | -| [`azure_ai_with_web_search.py`](azure_ai_with_web_search.py) | Shows how to use the `HostedWebSearchTool` with Azure AI agents to perform web searches and retrieve up-to-date information from the internet. | +| [`azure_ai_with_web_search.py`](azure_ai_with_web_search.py) | Shows how to use `AzureAIClient.get_web_search_tool()` with Azure AI agents to perform web searches and retrieve up-to-date information from the internet. | ## Environment Variables diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py b/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py index d9a80a3732..01ce5fbef8 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py @@ -17,7 +17,9 @@ Shows both streaming and non-streaming responses with function tools. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py index b05ec92f80..1cef3be3e5 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py @@ -23,11 +23,13 @@ This sample demonstrates the three main methods of AzureAIProjectAgentProvider: It also shows how to use a single provider instance to spawn multiple agents with different configurations, which is efficient for multi-agent scenarios. -Each method returns a ChatAgent that can be used for conversations. +Each method returns a Agent that can be used for conversations. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -41,7 +43,7 @@ async def create_agent_example() -> None: """Example of using provider.create_agent() to create a new agent. This method creates a new agent version on the Azure AI service and returns - a ChatAgent. Use this when you want to create a fresh agent with + a Agent. Use this when you want to create a fresh agent with specific configuration. """ print("=== provider.create_agent() Example ===") @@ -199,7 +201,7 @@ async def multiple_agents_example() -> None: async def as_agent_example() -> None: """Example of using provider.as_agent() to wrap an SDK object without HTTP calls. - This method wraps an existing AgentVersionDetails into a ChatAgent without + This method wraps an existing AgentVersionDetails into a Agent without making additional HTTP calls. Use this when you already have the full AgentVersionDetails from a previous SDK operation. """ diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py b/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py index b9472c9f1a..79d4e2c9a3 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py @@ -18,7 +18,9 @@ while subsequent calls with `get_agent()` reuse the latest agent version. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_application_endpoint.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_application_endpoint.py index 89bb77af11..db1c80a597 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_application_endpoint.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_application_endpoint.py @@ -3,7 +3,7 @@ import asyncio import os -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureAIClient from azure.ai.projects.aio import AIProjectClient from azure.identity.aio import AzureCliCredential @@ -23,8 +23,8 @@ async def main() -> None: # Endpoint here should be application endpoint with format: # /api/projects//applications//protocols AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - ChatAgent( - chat_client=AzureAIClient( + Agent( + client=AzureAIClient( project_client=project_client, ), ) as agent, diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py index ad43e21e9c..f91ddc01c1 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py @@ -2,8 +2,8 @@ import asyncio -from agent_framework import ChatResponse, HostedCodeInterpreterTool -from agent_framework.azure import AzureAIProjectAgentProvider +from agent_framework import ChatResponse +from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential from openai.types.responses.response import Response as OpenAIResponse from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall @@ -11,22 +11,26 @@ from openai.types.responses.response_code_interpreter_tool_call import ResponseC """ Azure AI Agent Code Interpreter Example -This sample demonstrates using HostedCodeInterpreterTool with AzureAIProjectAgentProvider +This sample demonstrates using get_code_interpreter_tool() with AzureAIProjectAgentProvider for Python code execution and mathematical problem solving. """ async def main() -> None: - """Example showing how to use the HostedCodeInterpreterTool with AzureAIProjectAgentProvider.""" + """Example showing how to use the code interpreter tool with AzureAIProjectAgentProvider.""" async with ( AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIClient(credential=credential) + code_interpreter_tool = client.get_code_interpreter_tool() + agent = await provider.create_agent( name="MyCodeInterpreterAgent", instructions="You are a helpful assistant that can write and execute Python code to solve problems.", - tools=HostedCodeInterpreterTool(), + tools=[code_interpreter_tool], ) query = "Use code to get the factorial of 100?" diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_download.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_download.py index ff0d9df4dc..cb5087b3f6 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_download.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_download.py @@ -5,13 +5,12 @@ import tempfile from pathlib import Path from agent_framework import ( + Agent, AgentResponseUpdate, Annotation, - ChatAgent, Content, - HostedCodeInterpreterTool, ) -from agent_framework.azure import AzureAIProjectAgentProvider +from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ @@ -33,7 +32,7 @@ QUERY = ( ) -async def download_container_files(file_contents: list[Annotation | Content], agent: ChatAgent) -> list[Path]: +async def download_container_files(file_contents: list[Annotation | Content], agent: Agent) -> list[Path]: """Download container files using the OpenAI containers API. Code interpreter generates files in containers, which require both file_id @@ -45,7 +44,7 @@ async def download_container_files(file_contents: list[Annotation | Content], ag Args: file_contents: List of Annotation or Content objects containing file_id and container_id. - agent: The ChatAgent instance with access to the AzureAIClient. + agent: The Agent instance with access to the AzureAIClient. Returns: List of Path objects for successfully downloaded files. @@ -61,7 +60,7 @@ async def download_container_files(file_contents: list[Annotation | Content], ag print(f"\nDownloading {len(file_contents)} container file(s) to {output_dir.absolute()}...") # Access the OpenAI client from AzureAIClient - openai_client = agent.chat_client.client # type: ignore[attr-defined] + openai_client = agent.client.client # type: ignore[attr-defined] downloaded_files: list[Path] = [] @@ -119,17 +118,21 @@ async def download_container_files(file_contents: list[Annotation | Content], ag async def non_streaming_example() -> None: - """Example of downloading files from non-streaming response using CitationAnnotation.""" + """Example of downloading files from non-streaming response using Annotation.""" print("=== Non-Streaming Response Example ===") async with ( AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIClient(credential=credential) + code_interpreter_tool = client.get_code_interpreter_tool() + agent = await provider.create_agent( name="V2CodeInterpreterFileAgent", instructions="You are a helpful assistant that can write and execute Python code to create files.", - tools=HostedCodeInterpreterTool(), + tools=[code_interpreter_tool], ) print(f"User: {QUERY}\n") @@ -139,7 +142,7 @@ async def non_streaming_example() -> None: # Check for annotations in the response annotations_found: list[Annotation] = [] - # AgentResponse has messages property, which contains ChatMessage objects + # AgentResponse has messages property, which contains Message objects for message in result.messages: for content in message.contents: if content.type == "text" and content.annotations: @@ -154,8 +157,8 @@ async def non_streaming_example() -> None: if annotations_found: print(f"SUCCESS: Found {len(annotations_found)} file annotation(s)") - # Download the container files - downloaded_paths = await download_container_files(annotations_found, agent) + # Download the container files (cast to Sequence for type compatibility) + downloaded_paths = await download_container_files(list(annotations_found), agent) if downloaded_paths: print("\nDownloaded files available at:") @@ -166,17 +169,21 @@ async def non_streaming_example() -> None: async def streaming_example() -> None: - """Example of downloading files from streaming response using HostedFileContent.""" + """Example of downloading files from streaming response using Content with type='hosted_file'.""" print("\n=== Streaming Response Example ===") async with ( AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIClient(credential=credential) + code_interpreter_tool = client.get_code_interpreter_tool() + agent = await provider.create_agent( name="V2CodeInterpreterFileAgentStreaming", instructions="You are a helpful assistant that can write and execute Python code to create files.", - tools=HostedCodeInterpreterTool(), + tools=[code_interpreter_tool], ) print(f"User: {QUERY}\n") diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py index 9c9fc48feb..72386aa418 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py @@ -4,9 +4,8 @@ import asyncio from agent_framework import ( AgentResponseUpdate, - HostedCodeInterpreterTool, ) -from agent_framework.azure import AzureAIProjectAgentProvider +from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ @@ -31,10 +30,14 @@ async def non_streaming_example() -> None: AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIClient(credential=credential) + code_interpreter_tool = client.get_code_interpreter_tool() + agent = await provider.create_agent( - name="V2CodeInterpreterFileAgent", + name="CodeInterpreterFileAgent", instructions="You are a helpful assistant that can write and execute Python code to create files.", - tools=HostedCodeInterpreterTool(), + tools=[code_interpreter_tool], ) print(f"User: {QUERY}\n") @@ -44,7 +47,7 @@ async def non_streaming_example() -> None: # Check for annotations in the response annotations_found: list[str] = [] - # AgentResponse has messages property, which contains ChatMessage objects + # AgentResponse has messages property, which contains Message objects for message in result.messages: for content in message.contents: if content.type == "text" and content.annotations: @@ -67,10 +70,14 @@ async def streaming_example() -> None: AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIClient(credential=credential) + code_interpreter_tool = client.get_code_interpreter_tool() + agent = await provider.create_agent( name="V2CodeInterpreterFileAgentStreaming", instructions="You are a helpful assistant that can write and execute Python code to create files.", - tools=HostedCodeInterpreterTool(), + tools=[code_interpreter_tool], ) print(f"User: {QUERY}\n") diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py index 7341068f10..0549c642c2 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py @@ -36,7 +36,7 @@ async def using_provider_get_agent() -> None: ) try: - # Get newly created agent as ChatAgent by using provider.get_agent() + # Get newly created agent as Agent by using provider.get_agent() provider = AzureAIProjectAgentProvider(project_client=project_client) agent = await provider.get_agent(name=azure_ai_agent.name) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py index 0410c00bd7..190ff54c7d 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py @@ -17,7 +17,9 @@ This sample demonstrates usage of AzureAIProjectAgentProvider with existing conv """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py index 382205b7cc..16468dd482 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py @@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py index 6a45aca516..cadb87e2b2 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py @@ -4,8 +4,7 @@ import asyncio import os from pathlib import Path -from agent_framework import Content, HostedFileSearchTool -from agent_framework.azure import AzureAIProjectAgentProvider +from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider from azure.ai.agents.aio import AgentsClient from azure.ai.agents.models import FileInfo, VectorStore from azure.identity.aio import AzureCliCredential @@ -45,8 +44,9 @@ async def main() -> None: vector_store = await agents_client.vector_stores.create_and_poll(file_ids=[file.id], name="my_vectorstore") print(f"Created vector store, vector store ID: {vector_store.id}") - # 2. Create file search tool with uploaded resources - file_search_tool = HostedFileSearchTool(inputs=[Content.from_hosted_vector_store(vector_store_id=vector_store.id)]) + # 2. Create a client to access hosted tool factory methods + client = AzureAIClient(credential=credential) + file_search_tool = client.get_file_search_tool(vector_store_ids=[vector_store.id]) # 3. Create an agent with file search capabilities using the provider agent = await provider.create_agent( @@ -55,7 +55,7 @@ async def main() -> None: "You are a helpful assistant that can search through uploaded employee files " "to answer questions about employees." ), - tools=file_search_tool, + tools=[file_search_tool], ) # 4. Simulate conversation with the agent diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py index 7f0660a5e8..75ebd2ea76 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py @@ -3,8 +3,8 @@ import asyncio from typing import Any -from agent_framework import AgentResponse, AgentThread, ChatMessage, HostedMCPTool, SupportsAgentRun -from agent_framework.azure import AzureAIProjectAgentProvider +from agent_framework import AgentResponse, AgentThread, Message, SupportsAgentRun +from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ @@ -25,10 +25,10 @@ async def handle_approvals_without_thread(query: str, agent: "SupportsAgentRun") f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}" f" with arguments: {user_input_needed.function_call.arguments}" ) - new_inputs.append(ChatMessage("assistant", [user_input_needed])) + new_inputs.append(Message("assistant", [user_input_needed])) user_approval = input("Approve function call? (y/n): ") new_inputs.append( - ChatMessage("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) + Message("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) result = await agent.run(new_inputs, store=False) @@ -48,7 +48,7 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th ) user_approval = input("Approve function call? (y/n): ") new_input.append( - ChatMessage( + Message( role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) @@ -65,14 +65,19 @@ async def run_hosted_mcp_without_approval() -> None: AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIClient(credential=credential) + # Create MCP tool using instance method + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + approval_mode="never_require", + ) + agent = await provider.create_agent( name="MyLearnDocsAgent", instructions="You are a helpful assistant that can help with Microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - approval_mode="never_require", - ), + tools=[mcp_tool], ) query = "How to create an Azure storage account using az cli?" @@ -91,14 +96,19 @@ async def run_hosted_mcp_with_approval_and_thread() -> None: AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIClient(credential=credential) + # Create MCP tool using instance method + mcp_tool = client.get_mcp_tool( + name="api-specs", + url="https://gitmcp.io/Azure/azure-rest-api-specs", + approval_mode="always_require", + ) + agent = await provider.create_agent( name="MyApiSpecsAgent", instructions="You are a helpful agent that can use MCP tools to assist users.", - tools=HostedMCPTool( - name="api-specs", - url="https://gitmcp.io/Azure/azure-rest-api-specs", - approval_mode="always_require", - ), + tools=[mcp_tool], ) thread = agent.get_new_thread() diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_image_generation.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_image_generation.py index a097d3f4c2..48e54ef2e2 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_image_generation.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_image_generation.py @@ -5,8 +5,7 @@ import tempfile from pathlib import Path from urllib import request as urllib_request -from agent_framework import HostedImageGenerationTool -from agent_framework.azure import AzureAIProjectAgentProvider +from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ @@ -28,22 +27,21 @@ async def main() -> None: AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIClient(credential=credential) + # Create image generation tool using instance method + image_gen_tool = client.get_image_generation_tool( + model="gpt-image-1", + size="1024x1024", + output_format="png", + quality="low", + background="opaque", + ) + agent = await provider.create_agent( name="ImageGenAgent", instructions="Generate images based on user requirements.", - tools=[ - HostedImageGenerationTool( - options={ - "model_id": "gpt-image-1", - "image_size": "1024x1024", - "media_type": "png", - }, - additional_properties={ - "quality": "low", - "background": "opaque", - }, - ) - ], + tools=[image_gen_tool], ) query = "Generate an image of Microsoft logo." diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py index 2330f9a19d..790c5be1a6 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py @@ -79,22 +79,22 @@ async def example_with_thread_persistence_in_memory() -> None: thread = agent.get_new_thread() # First conversation - query1 = "What's the weather like in Tokyo?" - print(f"User: {query1}") - result1 = await agent.run(query1, thread=thread, options={"store": False}) - print(f"Agent: {result1.text}") + first_query = "What's the weather like in Tokyo?" + print(f"User: {first_query}") + first_result = await agent.run(first_query, thread=thread, options={"store": False}) + print(f"Agent: {first_result.text}") # Second conversation using the same thread - maintains context - query2 = "How about London?" - print(f"\nUser: {query2}") - result2 = await agent.run(query2, thread=thread, options={"store": False}) - print(f"Agent: {result2.text}") + second_query = "How about London?" + print(f"\nUser: {second_query}") + second_result = await agent.run(second_query, thread=thread, options={"store": False}) + print(f"Agent: {second_result.text}") # Third conversation - agent should remember both previous cities - query3 = "Which of the cities I asked about has better weather?" - print(f"\nUser: {query3}") - result3 = await agent.run(query3, thread=thread, options={"store": False}) - print(f"Agent: {result3.text}") + third_query = "Which of the cities I asked about has better weather?" + print(f"\nUser: {third_query}") + third_result = await agent.run(third_query, thread=thread, options={"store": False}) + print(f"Agent: {third_result.text}") print("Note: The agent remembers context from previous messages in the same thread.\n") @@ -121,10 +121,10 @@ async def example_with_existing_thread_id() -> None: # Start a conversation and get the thread ID thread = agent.get_new_thread() - query1 = "What's the weather in Paris?" - print(f"User: {query1}") - result1 = await agent.run(query1, thread=thread) - print(f"Agent: {result1.text}") + first_query = "What's the weather in Paris?" + print(f"User: {first_query}") + first_result = await agent.run(first_query, thread=thread) + print(f"Agent: {first_result.text}") # The thread ID is set after the first response existing_thread_id = thread.service_thread_id @@ -134,19 +134,19 @@ async def example_with_existing_thread_id() -> None: print("\n--- Continuing with the same thread ID in a new agent instance ---") # Create a new agent instance from the same provider - agent2 = await provider.create_agent( + second_agent = await provider.create_agent( name="BasicWeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, ) # Create a thread with the existing ID - thread = agent2.get_new_thread(service_thread_id=existing_thread_id) + thread = second_agent.get_new_thread(service_thread_id=existing_thread_id) - query2 = "What was the last city I asked about?" - print(f"User: {query2}") - result2 = await agent2.run(query2, thread=thread) - print(f"Agent: {result2.text}") + second_query = "What was the last city I asked about?" + print(f"User: {second_query}") + second_result = await second_agent.run(second_query, thread=thread) + print(f"Agent: {second_result.text}") print("Note: The agent continues the conversation from the previous thread by using thread ID.\n") diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_web_search.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_web_search.py index 9ecb416f8d..39274c42d6 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_web_search.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_web_search.py @@ -2,15 +2,14 @@ import asyncio -from agent_framework import HostedWebSearchTool -from agent_framework.azure import AzureAIProjectAgentProvider +from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent With Web Search This sample demonstrates basic usage of AzureAIProjectAgentProvider to create an agent -that can perform web searches using the HostedWebSearchTool. +that can perform web searches using get_web_search_tool(). Pre-requisites: - Make sure to set up the AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME @@ -25,10 +24,15 @@ async def main() -> None: AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIClient(credential=credential) + # Create web search tool using instance method + web_search_tool = client.get_web_search_tool() + agent = await provider.create_agent( name="WebsearchAgent", instructions="You are a helpful assistant that can search the web", - tools=[HostedWebSearchTool()], + tools=[web_search_tool], ) query = "What's the weather today in Seattle?" diff --git a/python/samples/getting_started/agents/azure_ai_agent/README.md b/python/samples/getting_started/agents/azure_ai_agent/README.md index 5440b2d3ba..c91a66d558 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/README.md +++ b/python/samples/getting_started/agents/azure_ai_agent/README.md @@ -8,7 +8,7 @@ All examples in this folder use the `AzureAIAgentsProvider` class which provides - **`create_agent()`** - Create a new agent on the Azure AI service - **`get_agent()`** - Retrieve an existing agent by ID or from a pre-fetched Agent object -- **`as_agent()`** - Wrap an SDK Agent object as a ChatAgent without HTTP calls +- **`as_agent()`** - Wrap an SDK Agent object as a Agent without HTTP calls ```python from agent_framework.azure import AzureAIAgentsProvider @@ -32,20 +32,20 @@ async with ( |------|-------------| | [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive example demonstrating all `AzureAIAgentsProvider` methods: `create_agent()`, `get_agent()`, `as_agent()`, and managing multiple agents from a single provider. | | [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIAgentsProvider`. It automatically handles all configuration using environment variables. Shows both streaming and non-streaming responses. | -| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to find real-time information from the web using custom search configurations. Demonstrates how to set up and use HostedWebSearchTool with custom search instances. | -| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to find real-time information from the web. Demonstrates web search capabilities with proper source citations and comprehensive error handling. | +| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to find real-time information from the web using custom search configurations. Demonstrates how to use `AzureAIAgentClient.get_web_search_tool()` with custom search instances. | +| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to find real-time information from the web. Demonstrates `AzureAIAgentClient.get_web_search_tool()` with proper source citations and comprehensive error handling. | | [`azure_ai_with_bing_grounding_citations.py`](azure_ai_with_bing_grounding_citations.py) | Demonstrates how to extract and display citations from Bing Grounding search responses. Shows how to collect citation annotations (title, URL, snippet) during streaming responses, enabling users to verify sources and access referenced content. | | [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. | -| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with Azure AI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | +| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use `AzureAIAgentClient.get_code_interpreter_tool()` with Azure AI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | | [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with an existing SDK Agent object using `provider.as_agent()`. This wraps the agent without making HTTP calls. | | [`azure_ai_with_existing_thread.py`](azure_ai_with_existing_thread.py) | Shows how to work with a pre-existing thread by providing the thread ID. Demonstrates proper cleanup of manually created threads. | | [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured provider settings, including project endpoint and model deployment name. | | [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Demonstrates how to use Azure AI Search with Azure AI agents. Shows how to create an agent with search tools using the SDK directly and wrap it with `provider.get_agent()`. | -| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Demonstrates how to use the HostedFileSearchTool with Azure AI agents to search through uploaded documents. Shows file upload, vector store creation, and querying document content. | +| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Demonstrates how to use `AzureAIAgentClient.get_file_search_tool()` with Azure AI agents to search through uploaded documents. Shows file upload, vector store creation, and querying document content. | | [`azure_ai_with_function_tools.py`](azure_ai_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | -| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate Azure AI agents with hosted Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates remote MCP server connections and tool discovery. | +| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to use `AzureAIAgentClient.get_mcp_tool()` with hosted Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates remote MCP server connections and tool discovery. | | [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate Azure AI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates both agent-level and run-level tool configuration. | -| [`azure_ai_with_multiple_tools.py`](azure_ai_with_multiple_tools.py) | Demonstrates how to use multiple tools together with Azure AI agents, including web search, MCP servers, and function tools. Shows coordinated multi-tool interactions and approval workflows. | +| [`azure_ai_with_multiple_tools.py`](azure_ai_with_multiple_tools.py) | Demonstrates how to use multiple tools together with Azure AI agents, including web search, MCP servers, and function tools using client static methods. Shows coordinated multi-tool interactions and approval workflows. | | [`azure_ai_with_openapi_tools.py`](azure_ai_with_openapi_tools.py) | Demonstrates how to use OpenAPI tools with Azure AI agents to integrate external REST APIs. Shows OpenAPI specification loading, anonymous authentication, thread context management, and coordinated multi-API conversations. | | [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Demonstrates how to use structured outputs with Azure AI agents using Pydantic models. | | [`azure_ai_with_thread.py`](azure_ai_with_thread.py) | Demonstrates thread management with Azure AI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_custom_search.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_custom_search.py index ef41cf7c35..d4d718a868 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_custom_search.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_custom_search.py @@ -2,8 +2,7 @@ import asyncio -from agent_framework import HostedWebSearchTool -from agent_framework.azure import AzureAIAgentsProvider +from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider from azure.identity.aio import AzureCliCredential """ @@ -30,25 +29,25 @@ To set up Bing Custom Search: async def main() -> None: """Main function demonstrating Azure AI agent with Bing Custom Search.""" - # 1. Create Bing Custom Search tool using HostedWebSearchTool - # The connection ID and instance name will be automatically picked up from environment variables - bing_search_tool = HostedWebSearchTool( - name="Bing Custom Search", - description="Search the web for current information using Bing Custom Search", - ) - - # 2. Use AzureAIAgentsProvider for agent creation and management + # Use AzureAIAgentsProvider for agent creation and management async with ( AzureCliCredential() as credential, AzureAIAgentsProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIAgentClient(credential=credential) + # Create Bing Custom Search tool using instance method + # The connection ID and instance name will be automatically picked up from environment variables + # (BING_CUSTOM_CONNECTION_ID and BING_CUSTOM_INSTANCE_NAME) + bing_search_tool = client.get_web_search_tool() + agent = await provider.create_agent( name="BingSearchAgent", instructions=( "You are a helpful agent that can use Bing Custom Search tools to assist users. " "Use the available Bing Custom Search tools to answer questions and perform tasks." ), - tools=bing_search_tool, + tools=[bing_search_tool], ) # 3. Demonstrate agent capabilities with bing custom search diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding.py index 016c6ddeb8..9724f91591 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding.py @@ -2,8 +2,7 @@ import asyncio -from agent_framework import HostedWebSearchTool -from agent_framework.azure import AzureAIAgentsProvider +from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider from azure.identity.aio import AzureCliCredential """ @@ -25,18 +24,17 @@ 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 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", - ) - - # 2. Use AzureAIAgentsProvider for agent creation and management + # Use AzureAIAgentsProvider for agent creation and management async with ( AzureCliCredential() as credential, AzureAIAgentsProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIAgentClient(credential=credential) + # Create Bing Grounding search tool using instance method + # The connection ID will be automatically picked up from environment variable + bing_search_tool = client.get_web_search_tool() + agent = await provider.create_agent( name="BingSearchAgent", instructions=( @@ -44,7 +42,7 @@ async def main() -> None: "Use the Bing search tool to find up-to-date information and provide accurate, " "well-sourced answers. Always cite your sources when possible." ), - tools=bing_search_tool, + tools=[bing_search_tool], ) # 3. Demonstrate agent capabilities with web search diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding_citations.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding_citations.py index fd1f321741..10d594514c 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding_citations.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding_citations.py @@ -2,8 +2,8 @@ import asyncio -from agent_framework import Annotation, HostedWebSearchTool -from agent_framework.azure import AzureAIAgentsProvider +from agent_framework import Annotation +from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider from azure.identity.aio import AzureCliCredential """ @@ -27,18 +27,17 @@ 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 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", - ) - - # 2. Use AzureAIAgentsProvider for agent creation and management + # Use AzureAIAgentsProvider for agent creation and management async with ( AzureCliCredential() as credential, AzureAIAgentsProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIAgentClient(credential=credential) + # Create Bing Grounding search tool using instance method + # The connection ID will be automatically picked up from environment variable + bing_search_tool = client.get_web_search_tool() + agent = await provider.create_agent( name="BingSearchAgent", instructions=( @@ -46,7 +45,7 @@ async def main() -> None: "Use the Bing search tool to find up-to-date information and provide accurate, " "well-sourced answers. Always cite your sources when possible." ), - tools=bing_search_tool, + tools=[bing_search_tool], ) # 3. Demonstrate agent capabilities with web search diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter.py index a40ee17258..16da21bbe0 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter.py @@ -2,8 +2,8 @@ import asyncio -from agent_framework import AgentResponse, ChatResponseUpdate, HostedCodeInterpreterTool -from agent_framework.azure import AzureAIAgentsProvider +from agent_framework import AgentResponse, ChatResponseUpdate +from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider from azure.ai.agents.models import ( RunStepDeltaCodeInterpreterDetailItemObject, ) @@ -12,7 +12,7 @@ from azure.identity.aio import AzureCliCredential """ Azure AI Agent with Code Interpreter Example -This sample demonstrates using HostedCodeInterpreterTool with Azure AI Agents +This sample demonstrates using get_code_interpreter_tool() with Azure AI Agents for Python code execution and mathematical problem solving. """ @@ -32,7 +32,7 @@ def print_code_interpreter_inputs(response: AgentResponse) -> None: async def main() -> None: - """Example showing how to use the HostedCodeInterpreterTool with Azure AI.""" + """Example showing how to use the code interpreter tool with Azure AI.""" print("=== Azure AI Agent with Code Interpreter Example ===") # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred @@ -41,10 +41,14 @@ async def main() -> None: AzureCliCredential() as credential, AzureAIAgentsProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIAgentClient(credential=credential) + code_interpreter_tool = client.get_code_interpreter_tool() + agent = await provider.create_agent( name="CodingAgent", instructions=("You are a helpful assistant that can write and execute Python code to solve problems."), - tools=HostedCodeInterpreterTool(), + tools=[code_interpreter_tool], ) query = "Generate the factorial of 100 using python code, show the code and execute it." print(f"User: {query}") diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py index ac8d64f3cb..3cbf9c5855 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter_file_generation.py @@ -3,17 +3,14 @@ import asyncio import os -from agent_framework import ( - HostedCodeInterpreterTool, -) -from agent_framework.azure import AzureAIAgentsProvider +from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider from azure.ai.agents.aio import AgentsClient from azure.identity.aio import AzureCliCredential """ Azure AI Agent Code Interpreter File Generation Example -This sample demonstrates using HostedCodeInterpreterTool with AzureAIAgentsProvider +This sample demonstrates using get_code_interpreter_tool() with AzureAIAgentsProvider to generate a text file and then retrieve it. The test flow: @@ -32,6 +29,10 @@ async def main() -> None: AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, AzureAIAgentsProvider(agents_client=agents_client) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIAgentClient(credential=credential) + code_interpreter_tool = client.get_code_interpreter_tool() + agent = await provider.create_agent( name="CodeInterpreterAgent", instructions=( @@ -39,7 +40,7 @@ async def main() -> None: "ALWAYS use the code interpreter tool to execute Python code when asked to create files. " "Write actual Python code to create files, do not just describe what you would do." ), - tools=[HostedCodeInterpreterTool()], + tools=[code_interpreter_tool], ) # Be very explicit about wanting code execution and a download link diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py index 0f17d35183..f270fdbd60 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py @@ -19,7 +19,9 @@ by providing thread IDs for thread reuse patterns. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py index 05c8c60a36..53116ea114 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py @@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_file_search.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_file_search.py index 353b4aacd2..51613d394f 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_file_search.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_file_search.py @@ -4,8 +4,7 @@ import asyncio import os from pathlib import Path -from agent_framework import Content, HostedFileSearchTool -from agent_framework.azure import AzureAIAgentsProvider +from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider from azure.ai.agents.aio import AgentsClient from azure.ai.agents.models import FileInfo, VectorStore from azure.identity.aio import AzureCliCredential @@ -45,8 +44,9 @@ async def main() -> None: vector_store = await agents_client.vector_stores.create_and_poll(file_ids=[file.id], name="my_vectorstore") print(f"Created vector store, vector store ID: {vector_store.id}") - # 2. Create file search tool with uploaded resources - file_search_tool = HostedFileSearchTool(inputs=[Content.from_hosted_vector_store(vector_store_id=vector_store.id)]) + # 2. Create a client to access hosted tool factory methods + client = AzureAIAgentClient(credential=credential) + file_search_tool = client.get_file_search_tool(vector_store_ids=[vector_store.id]) # 3. Create an agent with file search capabilities agent = await provider.create_agent( @@ -55,7 +55,7 @@ async def main() -> None: "You are a helpful assistant that can search through uploaded employee files " "to answer questions about employees." ), - tools=file_search_tool, + tools=[file_search_tool], ) # 4. Simulate conversation with the agent diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_function_tools.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_function_tools.py index 97cd59ca19..37ca63f3f3 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_function_tools.py @@ -18,7 +18,9 @@ showing both agent-level and query-level tool configuration patterns. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_hosted_mcp.py index 19de064106..4a8e234241 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_hosted_mcp.py @@ -3,8 +3,8 @@ import asyncio from typing import Any -from agent_framework import AgentResponse, AgentThread, HostedMCPTool, SupportsAgentRun -from agent_framework.azure import AzureAIAgentsProvider +from agent_framework import AgentResponse, AgentThread, SupportsAgentRun +from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider from azure.identity.aio import AzureCliCredential """ @@ -17,7 +17,7 @@ servers, including user approval workflows for function call security. async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", thread: "AgentThread") -> AgentResponse: """Here we let the thread deal with the previous responses, and we just rerun with the approval.""" - from agent_framework import ChatMessage + from agent_framework import Message result = await agent.run(query, thread=thread, store=True) while len(result.user_input_requests) > 0: @@ -29,7 +29,7 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th ) user_approval = input("Approve function call? (y/n): ") new_input.append( - ChatMessage( + Message( role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) @@ -40,17 +40,23 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th async def main() -> None: """Example showing Hosted MCP tools for a Azure AI Agent.""" + async with ( AzureCliCredential() as credential, AzureAIAgentsProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIAgentClient(credential=credential) + # Create MCP tool using instance method + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + ) + agent = await provider.create_agent( name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ), + tools=[mcp_tool], ) thread = agent.get_new_thread() # First query diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_local_mcp.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_local_mcp.py index 0586ffb78e..8e26edfccc 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_local_mcp.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_local_mcp.py @@ -51,7 +51,7 @@ async def mcp_tools_on_agent_level() -> None: print("=== Tools Defined on Agent Level ===") # Tools are provided when creating the agent - # The ChatAgent will connect to the MCP server through its context manager + # The Agent will connect to the MCP server through its context manager # and discover tools at runtime async with ( AzureCliCredential() as credential, diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py index b7700dd6c2..af189311a8 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py @@ -6,12 +6,10 @@ from typing import Any from agent_framework import ( AgentThread, - HostedMCPTool, - HostedWebSearchTool, SupportsAgentRun, tool, ) -from agent_framework.azure import AzureAIAgentsProvider +from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider from azure.identity.aio import AzureCliCredential """ @@ -35,7 +33,9 @@ To set up Bing Grounding: """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_time() -> str: """Get the current UTC time.""" @@ -45,7 +45,7 @@ def get_time() -> str: async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", thread: "AgentThread"): """Here we let the thread deal with the previous responses, and we just rerun with the approval.""" - from agent_framework import ChatMessage + from agent_framework import Message result = await agent.run(query, thread=thread, store=True) while len(result.user_input_requests) > 0: @@ -57,7 +57,7 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th ) user_approval = input("Approve function call? (y/n): ") new_input.append( - ChatMessage( + Message( role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) @@ -67,20 +67,27 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th async def main() -> None: - """Example showing Hosted MCP tools for a Azure AI Agent.""" + """Example showing multiple tools for an Azure AI Agent.""" + async with ( AzureCliCredential() as credential, AzureAIAgentsProvider(credential=credential) as provider, ): + # Create a client to access hosted tool factory methods + client = AzureAIAgentClient(credential=credential) + # Create tools using instance methods + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + ) + web_search_tool = client.get_web_search_tool() + agent = await provider.create_agent( name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", tools=[ - HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - ), - HostedWebSearchTool(count=5), + mcp_tool, + web_search_tool, get_time, ], ) diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_thread.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_thread.py index a48851d67c..bf70f9014e 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_thread.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_thread.py @@ -17,7 +17,9 @@ automatic thread creation with explicit thread management for persistent context """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_openai/README.md b/python/samples/getting_started/agents/azure_openai/README.md index 466860de3e..fea029c209 100644 --- a/python/samples/getting_started/agents/azure_openai/README.md +++ b/python/samples/getting_started/agents/azure_openai/README.md @@ -6,23 +6,24 @@ This folder contains examples demonstrating different ways to create and use age | File | Description | |------|-------------| -| [`azure_assistants_basic.py`](azure_assistants_basic.py) | The simplest way to create an agent using `ChatAgent` with `AzureOpenAIAssistantsClient`. Shows both streaming and non-streaming responses with automatic assistant creation and cleanup. | -| [`azure_assistants_with_code_interpreter.py`](azure_assistants_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | +| [`azure_assistants_basic.py`](azure_assistants_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIAssistantsClient`. Shows both streaming and non-streaming responses with automatic assistant creation and cleanup. | +| [`azure_assistants_with_code_interpreter.py`](azure_assistants_with_code_interpreter.py) | Shows how to use `AzureOpenAIAssistantsClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | | [`azure_assistants_with_existing_assistant.py`](azure_assistants_with_existing_assistant.py) | Shows how to work with a pre-existing assistant by providing the assistant ID to the Azure Assistants client. Demonstrates proper cleanup of manually created assistants. | | [`azure_assistants_with_explicit_settings.py`](azure_assistants_with_explicit_settings.py) | Shows how to initialize an agent with a specific assistants client, configuring settings explicitly including endpoint and deployment name. | | [`azure_assistants_with_function_tools.py`](azure_assistants_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | | [`azure_assistants_with_thread.py`](azure_assistants_with_thread.py) | Demonstrates thread management with Azure agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | -| [`azure_chat_client_basic.py`](azure_chat_client_basic.py) | The simplest way to create an agent using `ChatAgent` with `AzureOpenAIChatClient`. Shows both streaming and non-streaming responses for chat-based interactions with Azure OpenAI models. | +| [`azure_chat_client_basic.py`](azure_chat_client_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIChatClient`. Shows both streaming and non-streaming responses for chat-based interactions with Azure OpenAI models. | | [`azure_chat_client_with_explicit_settings.py`](azure_chat_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific chat client, configuring settings explicitly including endpoint and deployment name. | | [`azure_chat_client_with_function_tools.py`](azure_chat_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | | [`azure_chat_client_with_thread.py`](azure_chat_client_with_thread.py) | Demonstrates thread management with Azure agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | -| [`azure_responses_client_basic.py`](azure_responses_client_basic.py) | The simplest way to create an agent using `ChatAgent` with `AzureOpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with Azure OpenAI models. | -| [`azure_responses_client_code_interpreter_files.py`](azure_responses_client_code_interpreter_files.py) | Demonstrates using HostedCodeInterpreterTool with file uploads for data analysis. Shows how to create, upload, and analyze CSV files using Python code execution with Azure OpenAI Responses. | +| [`azure_responses_client_basic.py`](azure_responses_client_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with Azure OpenAI models. | +| [`azure_responses_client_code_interpreter_files.py`](azure_responses_client_code_interpreter_files.py) | Demonstrates using `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with file uploads for data analysis. Shows how to create, upload, and analyze CSV files using Python code execution with Azure OpenAI Responses. | | [`azure_responses_client_image_analysis.py`](azure_responses_client_image_analysis.py) | Shows how to use Azure OpenAI Responses for image analysis and vision tasks. Demonstrates multi-modal messages combining text and image content using remote URLs. | -| [`azure_responses_client_with_code_interpreter.py`](azure_responses_client_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | +| [`azure_responses_client_with_code_interpreter.py`](azure_responses_client_with_code_interpreter.py) | Shows how to use `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | | [`azure_responses_client_with_explicit_settings.py`](azure_responses_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific responses client, configuring settings explicitly including endpoint and deployment name. | -| [`azure_responses_client_with_file_search.py`](azure_responses_client_with_file_search.py) | Demonstrates using HostedFileSearchTool with Azure OpenAI Responses Client for direct document-based question answering and information retrieval from vector stores. | +| [`azure_responses_client_with_file_search.py`](azure_responses_client_with_file_search.py) | Demonstrates using `AzureOpenAIResponsesClient.get_file_search_tool()` with Azure OpenAI Responses Client for direct document-based question answering and information retrieval from vector stores. | | [`azure_responses_client_with_function_tools.py`](azure_responses_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | +| [`azure_responses_client_with_hosted_mcp.py`](azure_responses_client_with_hosted_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with hosted Model Context Protocol (MCP) servers using `AzureOpenAIResponsesClient.get_mcp_tool()` for extended functionality. | | [`azure_responses_client_with_local_mcp.py`](azure_responses_client_with_local_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with local Model Context Protocol (MCP) servers using MCPStreamableHTTPTool for extended functionality. | | [`azure_responses_client_with_thread.py`](azure_responses_client_with_thread.py) | Demonstrates thread management with Azure agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | diff --git a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_code_interpreter.py b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_code_interpreter.py index 3445bbcbc0..7a0eb2645d 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_code_interpreter.py +++ b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_code_interpreter.py @@ -2,9 +2,8 @@ import asyncio -from agent_framework import AgentResponseUpdate, ChatAgent, ChatResponseUpdate, HostedCodeInterpreterTool +from agent_framework import Agent, AgentResponseUpdate, ChatResponseUpdate from agent_framework.azure import AzureOpenAIAssistantsClient -from azure.identity import AzureCliCredential from openai.types.beta.threads.runs import ( CodeInterpreterToolCallDelta, RunStepDelta, @@ -16,7 +15,7 @@ from openai.types.beta.threads.runs.code_interpreter_tool_call_delta import Code """ Azure OpenAI Assistants with Code Interpreter Example -This sample demonstrates using HostedCodeInterpreterTool with Azure OpenAI Assistants +This sample demonstrates using get_code_interpreter_tool() with Azure OpenAI Assistants for Python code execution and mathematical problem solving. """ @@ -41,15 +40,19 @@ def get_code_interpreter_chunk(chunk: AgentResponseUpdate) -> str | None: async def main() -> None: - """Example showing how to use the HostedCodeInterpreterTool with Azure OpenAI Assistants.""" + """Example showing how to use the code interpreter tool with Azure OpenAI Assistants.""" print("=== Azure OpenAI Assistants Agent with Code Interpreter Example ===") + # Create code interpreter tool using static method + client = AzureOpenAIAssistantsClient() + code_interpreter_tool = client.get_code_interpreter_tool() + # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + async with Agent( + client=client, instructions="You are a helpful assistant that can write and execute Python code to solve problems.", - tools=HostedCodeInterpreterTool(), + tools=[code_interpreter_tool], ) as agent: query = "What is current datetime?" print(f"User: {query}") diff --git a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_existing_assistant.py b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_existing_assistant.py index 7e373d4fad..c1c2ed0666 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_existing_assistant.py +++ b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_existing_assistant.py @@ -5,7 +5,7 @@ import os from random import randint from typing import Annotated -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.azure import AzureOpenAIAssistantsClient from azure.identity import AzureCliCredential, get_bearer_token_provider from openai import AsyncAzureOpenAI @@ -19,7 +19,9 @@ using existing assistant IDs rather than creating new ones. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -46,8 +48,8 @@ async def main() -> None: ) try: - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(async_client=client, assistant_id=created_assistant.id), + async with Agent( + client=AzureOpenAIAssistantsClient(async_client=client, assistant_id=created_assistant.id), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: diff --git a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_explicit_settings.py b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_explicit_settings.py index 65b0214ab8..d49bf9a27c 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_explicit_settings.py @@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_function_tools.py b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_function_tools.py index 8333e7fdc8..67a5c72f67 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_function_tools.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from random import randint from typing import Annotated -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.azure import AzureOpenAIAssistantsClient from azure.identity import AzureCliCredential from pydantic import Field @@ -18,7 +18,9 @@ showing both agent-level and query-level tool configuration patterns. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -43,8 +45,8 @@ async def tools_on_agent_level() -> None: # The agent can use these tools for any query during its lifetime # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) as agent: @@ -74,8 +76,8 @@ async def tools_on_run_level() -> None: # Agent created without tools # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", # No tools defined here ) as agent: @@ -105,8 +107,8 @@ async def mixed_tools_example() -> None: # Agent created with some base tools # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) as agent: diff --git a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_thread.py b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_thread.py index 793f8260c3..e9cbff23af 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_assistants_with_thread.py +++ b/python/samples/getting_started/agents/azure_openai/azure_assistants_with_thread.py @@ -4,7 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread, ChatAgent, tool +from agent_framework import Agent, AgentThread, tool from agent_framework.azure import AzureOpenAIAssistantsClient from azure.identity import AzureCliCredential from pydantic import Field @@ -17,7 +17,9 @@ automatic thread creation with explicit thread management for persistent context """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -33,8 +35,8 @@ async def example_with_automatic_thread_creation() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -59,8 +61,8 @@ async def example_with_thread_persistence() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -97,8 +99,8 @@ async def example_with_existing_thread_id() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: @@ -117,8 +119,8 @@ async def example_with_existing_thread_id() -> None: print("\n--- Continuing with the same thread ID in a new agent instance ---") # Create a new agent instance but use the existing thread ID - async with ChatAgent( - chat_client=AzureOpenAIAssistantsClient(thread_id=existing_thread_id, credential=AzureCliCredential()), + async with Agent( + client=AzureOpenAIAssistantsClient(thread_id=existing_thread_id, credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) as agent: diff --git a/python/samples/getting_started/agents/azure_openai/azure_chat_client_basic.py b/python/samples/getting_started/agents/azure_openai/azure_chat_client_basic.py index e1e9fab2f5..b52d514813 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_chat_client_basic.py +++ b/python/samples/getting_started/agents/azure_openai/azure_chat_client_basic.py @@ -17,7 +17,9 @@ interactions, showing both streaming and non-streaming responses. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py index 5f7bc794e5..7b69168093 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_explicit_settings.py @@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_function_tools.py b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_function_tools.py index 777bcc51b1..4c12fe7d5b 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_function_tools.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from random import randint from typing import Annotated -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential from pydantic import Field @@ -18,7 +18,9 @@ showing both agent-level and query-level tool configuration patterns. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -43,8 +45,8 @@ async def tools_on_agent_level() -> None: # The agent can use these tools for any query during its lifetime # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) @@ -75,8 +77,8 @@ async def tools_on_run_level() -> None: # Agent created without tools # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", # No tools defined here ) @@ -107,8 +109,8 @@ async def mixed_tools_example() -> None: # Agent created with some base tools # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) diff --git a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_thread.py b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_thread.py index 08ada3ba97..24fa8272b6 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_thread.py +++ b/python/samples/getting_started/agents/azure_openai/azure_chat_client_with_thread.py @@ -4,7 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread, ChatAgent, ChatMessageStore, tool +from agent_framework import Agent, AgentThread, ChatMessageStore, tool from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential from pydantic import Field @@ -17,7 +17,9 @@ automatic thread creation with explicit thread management for persistent context """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -33,8 +35,8 @@ async def example_with_automatic_thread_creation() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -60,8 +62,8 @@ async def example_with_thread_persistence() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -95,8 +97,8 @@ async def example_with_existing_thread_messages() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -117,8 +119,8 @@ async def example_with_existing_thread_messages() -> None: print("\n--- Continuing with the same thread in a new agent instance ---") # Create a new agent instance but use the existing thread with its message history - new_agent = ChatAgent( - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + new_agent = Agent( + client=AzureOpenAIChatClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_basic.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_basic.py index de20e03c4a..095cfadfa7 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_basic.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_basic.py @@ -17,7 +17,9 @@ response generation, showing both streaming and non-streaming responses. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_code_interpreter_files.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_code_interpreter_files.py index 187e354264..33154a7c47 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_code_interpreter_files.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_code_interpreter_files.py @@ -4,7 +4,7 @@ import asyncio import os import tempfile -from agent_framework import ChatAgent, HostedCodeInterpreterTool +from agent_framework import Agent from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential from openai import AsyncAzureOpenAI @@ -12,7 +12,7 @@ from openai import AsyncAzureOpenAI """ Azure OpenAI Responses Client with Code Interpreter and Files Example -This sample demonstrates using HostedCodeInterpreterTool with Azure OpenAI Responses +This sample demonstrates using get_code_interpreter_tool() with Azure OpenAI Responses for Python code execution and data analysis with uploaded files. """ @@ -76,10 +76,15 @@ async def main() -> None: temp_file_path, file_id = await create_sample_file_and_upload(openai_client) # Create agent using Azure OpenAI Responses client - agent = ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=credential), + client = AzureOpenAIResponsesClient(credential=credential) + + # Create code interpreter tool with file access + code_interpreter_tool = client.get_code_interpreter_tool(file_ids=[file_id]) + + agent = Agent( + client=client, instructions="You are a helpful assistant that can analyze data files using Python code.", - tools=HostedCodeInterpreterTool(inputs=[{"file_id": file_id}]), + tools=[code_interpreter_tool], ) # Test the code interpreter with the uploaded file diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py index 9bf05e32e0..e9bedfd474 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_image_analysis.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential @@ -24,12 +24,12 @@ async def main(): ) # 2. Create a simple message with both text and image content - user_message = ChatMessage( + user_message = Message( role="user", contents=[ - Content.from_text(text="What do you see in this image?"), + Content.from_text("What do you see in this image?"), Content.from_uri( - uri="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", + uri="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800", media_type="image/jpeg", ), ], diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_code_interpreter.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_code_interpreter.py index 70c8fb832f..544e4c49e6 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_code_interpreter.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_code_interpreter.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatAgent, ChatResponse, HostedCodeInterpreterTool +from agent_framework import Agent, ChatResponse from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential from openai.types.responses.response import Response as OpenAIResponse @@ -11,21 +11,26 @@ from openai.types.responses.response_code_interpreter_tool_call import ResponseC """ Azure OpenAI Responses Client with Code Interpreter Example -This sample demonstrates using HostedCodeInterpreterTool with Azure OpenAI Responses +This sample demonstrates using get_code_interpreter_tool() with Azure OpenAI Responses for Python code execution and mathematical problem solving. """ async def main() -> None: - """Example showing how to use the HostedCodeInterpreterTool with Azure OpenAI Responses.""" + """Example showing how to use the code interpreter tool with Azure OpenAI Responses.""" print("=== Azure OpenAI Responses Agent with Code Interpreter Example ===") # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) + + # Create code interpreter tool using instance method + code_interpreter_tool = client.get_code_interpreter_tool() + + agent = Agent( + client=client, instructions="You are a helpful assistant that can write and execute Python code to solve problems.", - tools=HostedCodeInterpreterTool(), + tools=[code_interpreter_tool], ) query = "Use code to calculate the factorial of 100?" diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py index c21462b11f..b89458df12 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_explicit_settings.py @@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_file_search.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_file_search.py index 08f35eb659..432cede701 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_file_search.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_file_search.py @@ -2,14 +2,14 @@ import asyncio -from agent_framework import ChatAgent, Content, HostedFileSearchTool +from agent_framework import Agent, Content from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential """ Azure OpenAI Responses Client with File Search Example -This sample demonstrates using HostedFileSearchTool with Azure OpenAI Responses Client +This sample demonstrates using get_file_search_tool() with Azure OpenAI Responses Client for direct document-based question answering and information retrieval. Prerequisites: @@ -51,12 +51,15 @@ async def main() -> None: # Make sure you're logged in via 'az login' before running this sample client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) - file_id, vector_store = await create_vector_store(client) + file_id, vector_store_id = await create_vector_store(client) - agent = ChatAgent( - chat_client=client, + # Create file search tool using instance method + file_search_tool = client.get_file_search_tool(vector_store_ids=[vector_store_id]) + + agent = Agent( + client=client, instructions="You are a helpful assistant that can search through files to find information.", - tools=[HostedFileSearchTool(inputs=vector_store)], + tools=[file_search_tool], ) query = "What is the weather today? Do a file search to find the answer." @@ -64,7 +67,7 @@ async def main() -> None: result = await agent.run(query) print(f"Agent: {result}\n") - await delete_vector_store(client, file_id, vector_store.vector_store_id) + await delete_vector_store(client, file_id, vector_store_id) if __name__ == "__main__": diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_function_tools.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_function_tools.py index a5d6d85aa6..265ccff98f 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_function_tools.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_function_tools.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from random import randint from typing import Annotated -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential from pydantic import Field @@ -18,7 +18,9 @@ showing both agent-level and query-level tool configuration patterns. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -43,8 +45,8 @@ async def tools_on_agent_level() -> None: # The agent can use these tools for any query during its lifetime # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) @@ -75,8 +77,8 @@ async def tools_on_run_level() -> None: # Agent created without tools # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), instructions="You are a helpful assistant.", # No tools defined here ) @@ -107,8 +109,8 @@ async def mixed_tools_example() -> None: # Agent created with some base tools # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py index eddc54d48c..bcc6f636b5 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_hosted_mcp.py @@ -3,7 +3,7 @@ import asyncio from typing import TYPE_CHECKING, Any -from agent_framework import ChatAgent, HostedMCPTool +from agent_framework import Agent from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential @@ -20,7 +20,7 @@ if TYPE_CHECKING: async def handle_approvals_without_thread(query: str, agent: "SupportsAgentRun"): """When we don't have a thread, we need to ensure we return with the input, approval request and approval.""" - from agent_framework import ChatMessage + from agent_framework import Message result = await agent.run(query) while len(result.user_input_requests) > 0: @@ -30,10 +30,13 @@ async def handle_approvals_without_thread(query: str, agent: "SupportsAgentRun") f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}" f" with arguments: {user_input_needed.function_call.arguments}" ) - new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed])) + new_inputs.append(Message(role="assistant", contents=[user_input_needed])) user_approval = input("Approve function call? (y/n): ") new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) + Message( + role="user", + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], + ) ) result = await agent.run(new_inputs) @@ -42,7 +45,7 @@ async def handle_approvals_without_thread(query: str, agent: "SupportsAgentRun") async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", thread: "AgentThread"): """Here we let the thread deal with the previous responses, and we just rerun with the approval.""" - from agent_framework import ChatMessage + from agent_framework import Message result = await agent.run(query, thread=thread, store=True) while len(result.user_input_requests) > 0: @@ -54,7 +57,7 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th ) user_approval = input("Approve function call? (y/n): ") new_input.append( - ChatMessage( + Message( role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) @@ -65,13 +68,13 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th async def handle_approvals_with_thread_streaming(query: str, agent: "SupportsAgentRun", thread: "AgentThread"): """Here we let the thread deal with the previous responses, and we just rerun with the approval.""" - from agent_framework import ChatMessage + from agent_framework import Message - new_input: list[ChatMessage] = [] + new_input: list[Message] = [] new_input_added = True while new_input_added: new_input_added = False - new_input.append(ChatMessage(role="user", text=query)) + new_input.append(Message(role="user", text=query)) async for update in agent.run(new_input, thread=thread, options={"store": True}, stream=True): if update.user_input_requests: for user_input_needed in update.user_input_requests: @@ -81,8 +84,9 @@ async def handle_approvals_with_thread_streaming(query: str, agent: "SupportsAge ) user_approval = input("Approve function call? (y/n): ") new_input.append( - ChatMessage( - role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")] + Message( + role="user", + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) ) new_input_added = True @@ -94,21 +98,24 @@ async def run_hosted_mcp_without_thread_and_specific_approval() -> None: """Example showing Mcp Tools with approvals without using a thread.""" print("=== Mcp with approvals and without thread ===") credential = AzureCliCredential() + client = AzureOpenAIResponsesClient(credential=credential) + + # Create MCP tool with specific approval settings + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + # we don't require approval for microsoft_docs_search tool calls + # but we do for any other tool + approval_mode={"never_require_approval": ["microsoft_docs_search"]}, + ) + # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime - async with ChatAgent( - chat_client=AzureOpenAIResponsesClient( - credential=credential, - ), + async with Agent( + client=client, name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - # we don't require approval for microsoft_docs_search tool calls - # but we do for any other tool - approval_mode={"never_require_approval": ["microsoft_docs_search"]}, - ), + tools=[mcp_tool], ) as agent: # First query query1 = "How to create an Azure storage account using az cli?" @@ -127,22 +134,25 @@ async def run_hosted_mcp_without_approval() -> None: """Example showing Mcp Tools without approvals.""" print("=== Mcp without approvals ===") credential = AzureCliCredential() + client = AzureOpenAIResponsesClient(credential=credential) + + # Create MCP tool without approval requirements + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + # we don't require approval for any function calls + # this means we will not see the approval messages, + # it is fully handled by the service and a final response is returned. + approval_mode="never_require", + ) + # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime - async with ChatAgent( - chat_client=AzureOpenAIResponsesClient( - credential=credential, - ), + async with Agent( + client=client, name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - # we don't require approval for any function calls - # this means we will not see the approval messages, - # it is fully handled by the service and a final response is returned. - approval_mode="never_require", - ), + tools=[mcp_tool], ) as agent: # First query query1 = "How to create an Azure storage account using az cli?" @@ -161,20 +171,23 @@ async def run_hosted_mcp_with_thread() -> None: """Example showing Mcp Tools with approvals using a thread.""" print("=== Mcp with approvals and with thread ===") credential = AzureCliCredential() + client = AzureOpenAIResponsesClient(credential=credential) + + # Create MCP tool with always require approval + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + # we require approval for all function calls + approval_mode="always_require", + ) + # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime - async with ChatAgent( - chat_client=AzureOpenAIResponsesClient( - credential=credential, - ), + async with Agent( + client=client, name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - # we require approval for all function calls - approval_mode="always_require", - ), + tools=[mcp_tool], ) as agent: # First query thread = agent.get_new_thread() @@ -194,20 +207,23 @@ async def run_hosted_mcp_with_thread_streaming() -> None: """Example showing Mcp Tools with approvals using a thread.""" print("=== Mcp with approvals and with thread ===") credential = AzureCliCredential() + client = AzureOpenAIResponsesClient(credential=credential) + + # Create MCP tool with always require approval + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + # we require approval for all function calls + approval_mode="always_require", + ) + # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime - async with ChatAgent( - chat_client=AzureOpenAIResponsesClient( - credential=credential, - ), + async with Agent( + client=client, name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - # we require approval for all function calls - approval_mode="always_require", - ), + tools=[mcp_tool], ) as agent: # First query thread = agent.get_new_thread() diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_local_mcp.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_local_mcp.py index 4958a64b44..7d8f2466b6 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_local_mcp.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_local_mcp.py @@ -3,7 +3,7 @@ import asyncio import os -from agent_framework import ChatAgent, MCPStreamableHTTPTool +from agent_framework import Agent, MCPStreamableHTTPTool from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential @@ -37,7 +37,7 @@ async def main(): credential=credential, ) - agent: ChatAgent = responses_client.as_agent( + agent: Agent = responses_client.as_agent( name="DocsAgent", instructions=("You are a helpful assistant that can help with Microsoft documentation questions."), ) @@ -48,14 +48,14 @@ async def main(): url=MCP_URL, ) as mcp_tool: # First query — expect the agent to use the MCP tool if it helps - q1 = "How to create an Azure storage account using az cli?" - r1 = await agent.run(q1, tools=mcp_tool) - print("\n=== Answer 1 ===\n", r1.text) + first_query = "How to create an Azure storage account using az cli?" + first_response = await agent.run(first_query, tools=mcp_tool) + print("\n=== Answer 1 ===\n", first_response.text) # Follow-up query (connection is reused) - q2 = "What is Microsoft Agent Framework?" - r2 = await agent.run(q2, tools=mcp_tool) - print("\n=== Answer 2 ===\n", r2.text) + second_query = "What is Microsoft Agent Framework?" + second_response = await agent.run(second_query, tools=mcp_tool) + print("\n=== Answer 2 ===\n", second_response.text) if __name__ == "__main__": diff --git a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_thread.py b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_thread.py index 01ade8da6f..028f583ddb 100644 --- a/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_thread.py +++ b/python/samples/getting_started/agents/azure_openai/azure_responses_client_with_thread.py @@ -4,7 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread, ChatAgent, tool +from agent_framework import Agent, AgentThread, tool from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential from pydantic import Field @@ -17,7 +17,9 @@ automatic thread creation with explicit thread management for persistent context """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -33,8 +35,8 @@ async def example_with_automatic_thread_creation() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -62,8 +64,8 @@ async def example_with_thread_persistence_in_memory() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -103,8 +105,8 @@ async def example_with_existing_thread_id() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - agent = ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -125,8 +127,8 @@ async def example_with_existing_thread_id() -> None: if existing_thread_id: print("\n--- Continuing with the same thread ID in a new agent instance ---") - agent = ChatAgent( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + agent = Agent( + client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/getting_started/agents/custom/README.md b/python/samples/getting_started/agents/custom/README.md index eba87c4350..f8921b1f24 100644 --- a/python/samples/getting_started/agents/custom/README.md +++ b/python/samples/getting_started/agents/custom/README.md @@ -7,7 +7,7 @@ This folder contains examples demonstrating how to implement custom agents and c | File | Description | |------|-------------| | [`custom_agent.py`](custom_agent.py) | Shows how to create custom agents by extending the `BaseAgent` class. Demonstrates the `EchoAgent` implementation with both streaming and non-streaming responses, proper thread management, and message history handling. | -| [`custom_chat_client.py`](../../chat_client/custom_chat_client.py) | Demonstrates how to create custom chat clients by extending the `BaseChatClient` class. Shows a `EchoingChatClient` implementation and how to integrate it with `ChatAgent` using the `as_agent()` method. | +| [`custom_chat_client.py`](../../chat_client/custom_chat_client.py) | Demonstrates how to create custom chat clients by extending the `BaseChatClient` class. Shows a `EchoingChatClient` implementation and how to integrate it with `Agent` using the `as_agent()` method. | ## Key Takeaways @@ -20,7 +20,7 @@ This folder contains examples demonstrating how to implement custom agents and c ### Custom Chat Clients - Custom chat clients allow you to integrate any backend service or create new LLM providers - You must implement `_inner_get_response()` with a stream parameter to handle both streaming and non-streaming responses -- Custom chat clients can be used with `ChatAgent` to leverage all agent framework features +- Custom chat clients can be used with `Agent` to leverage all agent framework features - Use the `as_agent()` method to easily create agents from your custom chat clients Both approaches allow you to extend the framework for your specific use cases while maintaining compatibility with the broader Agent Framework ecosystem. diff --git a/python/samples/getting_started/agents/custom/custom_agent.py b/python/samples/getting_started/agents/custom/custom_agent.py index 7df37ba781..51fb2452c8 100644 --- a/python/samples/getting_started/agents/custom/custom_agent.py +++ b/python/samples/getting_started/agents/custom/custom_agent.py @@ -9,8 +9,8 @@ from agent_framework import ( AgentResponseUpdate, AgentThread, BaseAgent, - ChatMessage, Content, + Message, Role, normalize_messages, ) @@ -57,7 +57,7 @@ class EchoAgent(BaseAgent): def run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, stream: bool = False, thread: AgentThread | None = None, @@ -81,7 +81,7 @@ class EchoAgent(BaseAgent): async def _run( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -91,11 +91,9 @@ class EchoAgent(BaseAgent): normalized_messages = normalize_messages(messages) if not normalized_messages: - response_message = ChatMessage( + response_message = Message( role=Role.ASSISTANT, - contents=[ - Content.from_text(text="Hello! I'm a custom echo agent. Send me a message and I'll echo it back.") - ], + contents=[Content.from_text(text="Hello! I'm a custom echo agent. Send me a message and I'll echo it back.")], ) else: # For simplicity, echo the last user message @@ -105,7 +103,7 @@ class EchoAgent(BaseAgent): else: echo_text = f"{self.echo_prefix}[Non-text message received]" - response_message = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=echo_text)]) + response_message = Message(role=Role.ASSISTANT, contents=[Content.from_text(text=echo_text)]) # Notify the thread of new messages if provided if thread is not None: @@ -115,7 +113,7 @@ class EchoAgent(BaseAgent): async def _run_stream( self, - messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None, + messages: str | Message | list[str] | list[Message] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, @@ -150,7 +148,7 @@ class EchoAgent(BaseAgent): # Notify the thread of the complete response if provided if thread is not None: - complete_response = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)]) + complete_response = Message(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)]) await self._notify_thread_of_new_messages(thread, normalized_messages, complete_response) diff --git a/python/samples/getting_started/agents/ollama/ollama_chat_multimodal.py b/python/samples/getting_started/agents/ollama/ollama_chat_multimodal.py index 3deb6f6e92..68c1246ad2 100644 --- a/python/samples/getting_started/agents/ollama/ollama_chat_multimodal.py +++ b/python/samples/getting_started/agents/ollama/ollama_chat_multimodal.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework.ollama import OllamaChatClient """ @@ -32,7 +32,7 @@ async def test_image() -> None: image_uri = create_sample_image() - message = ChatMessage( + message = Message( role="user", contents=[ Content.from_text(text="What's in this image?"), diff --git a/python/samples/getting_started/agents/openai/README.md b/python/samples/getting_started/agents/openai/README.md index 4feff05d22..579bfec187 100644 --- a/python/samples/getting_started/agents/openai/README.md +++ b/python/samples/getting_started/agents/openai/README.md @@ -1,6 +1,6 @@ # OpenAI Agent Framework Examples -This folder contains examples demonstrating different ways to create and use agents with the OpenAI Assistants client from the `agent_framework.openai` package. +This folder contains examples demonstrating different ways to create and use agents with the OpenAI clients from the `agent_framework.openai` package. ## Examples @@ -8,36 +8,37 @@ This folder contains examples demonstrating different ways to create and use age |------|-------------| | [`openai_assistants_basic.py`](openai_assistants_basic.py) | Basic usage of `OpenAIAssistantProvider` with streaming and non-streaming responses. | | [`openai_assistants_provider_methods.py`](openai_assistants_provider_methods.py) | Demonstrates all `OpenAIAssistantProvider` methods: `create_agent()`, `get_agent()`, and `as_agent()`. | -| [`openai_assistants_with_code_interpreter.py`](openai_assistants_with_code_interpreter.py) | Using `HostedCodeInterpreterTool` with `OpenAIAssistantProvider` to execute Python code. | +| [`openai_assistants_with_code_interpreter.py`](openai_assistants_with_code_interpreter.py) | Using `OpenAIAssistantsClient.get_code_interpreter_tool()` with `OpenAIAssistantProvider` to execute Python code. | | [`openai_assistants_with_existing_assistant.py`](openai_assistants_with_existing_assistant.py) | Working with pre-existing assistants using `get_agent()` and `as_agent()` methods. | | [`openai_assistants_with_explicit_settings.py`](openai_assistants_with_explicit_settings.py) | Configuring `OpenAIAssistantProvider` with explicit settings including API key and model ID. | -| [`openai_assistants_with_file_search.py`](openai_assistants_with_file_search.py) | Using `HostedFileSearchTool` with `OpenAIAssistantProvider` for file search capabilities. | +| [`openai_assistants_with_file_search.py`](openai_assistants_with_file_search.py) | Using `OpenAIAssistantsClient.get_file_search_tool()` with `OpenAIAssistantProvider` for file search capabilities. | | [`openai_assistants_with_function_tools.py`](openai_assistants_with_function_tools.py) | Function tools with `OpenAIAssistantProvider` at both agent-level and query-level. | | [`openai_assistants_with_response_format.py`](openai_assistants_with_response_format.py) | Structured outputs with `OpenAIAssistantProvider` using Pydantic models. | | [`openai_assistants_with_thread.py`](openai_assistants_with_thread.py) | Thread management with `OpenAIAssistantProvider` for conversation context persistence. | -| [`openai_chat_client_basic.py`](openai_chat_client_basic.py) | The simplest way to create an agent using `ChatAgent` with `OpenAIChatClient`. Shows both streaming and non-streaming responses for chat-based interactions with OpenAI models. | +| [`openai_chat_client_basic.py`](openai_chat_client_basic.py) | The simplest way to create an agent using `Agent` with `OpenAIChatClient`. Shows both streaming and non-streaming responses for chat-based interactions with OpenAI models. | | [`openai_chat_client_with_explicit_settings.py`](openai_chat_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific chat client, configuring settings explicitly including API key and model ID. | | [`openai_chat_client_with_function_tools.py`](openai_chat_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). | | [`openai_chat_client_with_local_mcp.py`](openai_chat_client_with_local_mcp.py) | Shows how to integrate OpenAI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. | | [`openai_chat_client_with_thread.py`](openai_chat_client_with_thread.py) | Demonstrates thread management with OpenAI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | -| [`openai_chat_client_with_web_search.py`](openai_chat_client_with_web_search.py) | Shows how to use web search capabilities with OpenAI agents to retrieve and use information from the internet in responses. | +| [`openai_chat_client_with_web_search.py`](openai_chat_client_with_web_search.py) | Shows how to use `OpenAIChatClient.get_web_search_tool()` for web search capabilities with OpenAI agents. | | [`openai_chat_client_with_runtime_json_schema.py`](openai_chat_client_with_runtime_json_schema.py) | Shows how to supply a runtime JSON Schema via `additional_chat_options` for structured output without defining a Pydantic model. | -| [`openai_responses_client_basic.py`](openai_responses_client_basic.py) | The simplest way to create an agent using `ChatAgent` with `OpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with OpenAI models. | +| [`openai_responses_client_basic.py`](openai_responses_client_basic.py) | The simplest way to create an agent using `Agent` with `OpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with OpenAI models. | | [`openai_responses_client_image_analysis.py`](openai_responses_client_image_analysis.py) | Demonstrates how to use vision capabilities with agents to analyze images. | -| [`openai_responses_client_image_generation.py`](openai_responses_client_image_generation.py) | Demonstrates how to use image generation capabilities with OpenAI agents to create images based on text descriptions. Requires PIL (Pillow) for image display. | +| [`openai_responses_client_image_generation.py`](openai_responses_client_image_generation.py) | Demonstrates how to use `OpenAIResponsesClient.get_image_generation_tool()` to create images based on text descriptions. | | [`openai_responses_client_reasoning.py`](openai_responses_client_reasoning.py) | Demonstrates how to use reasoning capabilities with OpenAI agents, showing how the agent can provide detailed reasoning for its responses. | | [`openai_responses_client_streaming_image_generation.py`](openai_responses_client_streaming_image_generation.py) | Demonstrates streaming image generation with partial images for real-time image creation feedback and improved user experience. | | [`openai_responses_client_with_agent_as_tool.py`](openai_responses_client_with_agent_as_tool.py) | Shows how to use the agent-as-tool pattern with OpenAI Responses Client, where one agent delegates work to specialized sub-agents wrapped as tools using `as_tool()`. Demonstrates hierarchical agent architectures. | -| [`openai_responses_client_with_code_interpreter.py`](openai_responses_client_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with OpenAI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. | +| [`openai_responses_client_with_code_interpreter.py`](openai_responses_client_with_code_interpreter.py) | Shows how to use `OpenAIResponsesClient.get_code_interpreter_tool()` to write and execute Python code. | +| [`openai_responses_client_with_code_interpreter_files.py`](openai_responses_client_with_code_interpreter_files.py) | Shows how to use code interpreter with uploaded files for data analysis. | | [`openai_responses_client_with_explicit_settings.py`](openai_responses_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific responses client, configuring settings explicitly including API key and model ID. | -| [`openai_responses_client_with_file_search.py`](openai_responses_client_with_file_search.py) | Demonstrates how to use file search capabilities with OpenAI agents, allowing the agent to search through uploaded files to answer questions. | +| [`openai_responses_client_with_file_search.py`](openai_responses_client_with_file_search.py) | Demonstrates how to use `OpenAIResponsesClient.get_file_search_tool()` for searching through uploaded files. | | [`openai_responses_client_with_function_tools.py`](openai_responses_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and run-level tools (provided with specific queries). | -| [`openai_responses_client_with_hosted_mcp.py`](openai_responses_client_with_hosted_mcp.py) | Shows how to integrate OpenAI agents with hosted Model Context Protocol (MCP) servers, including approval workflows and tool management for remote MCP services. | +| [`openai_responses_client_with_hosted_mcp.py`](openai_responses_client_with_hosted_mcp.py) | Shows how to use `OpenAIResponsesClient.get_mcp_tool()` for hosted MCP servers, including approval workflows. | | [`openai_responses_client_with_local_mcp.py`](openai_responses_client_with_local_mcp.py) | Shows how to integrate OpenAI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. | | [`openai_responses_client_with_runtime_json_schema.py`](openai_responses_client_with_runtime_json_schema.py) | Shows how to supply a runtime JSON Schema via `additional_chat_options` for structured output without defining a Pydantic model. | | [`openai_responses_client_with_structured_output.py`](openai_responses_client_with_structured_output.py) | Demonstrates how to use structured outputs with OpenAI agents to get structured data responses in predefined formats. | | [`openai_responses_client_with_thread.py`](openai_responses_client_with_thread.py) | Demonstrates thread management with OpenAI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | -| [`openai_responses_client_with_web_search.py`](openai_responses_client_with_web_search.py) | Shows how to use web search capabilities with OpenAI agents to retrieve and use information from the internet in responses. | +| [`openai_responses_client_with_web_search.py`](openai_responses_client_with_web_search.py) | Shows how to use `OpenAIResponsesClient.get_web_search_tool()` for web search capabilities. | ## Environment Variables diff --git a/python/samples/getting_started/agents/openai/openai_assistants_basic.py b/python/samples/getting_started/agents/openai/openai_assistants_basic.py index 2fa4f79094..0ad7697b2f 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_basic.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_basic.py @@ -18,7 +18,9 @@ assistant lifecycle management, showing both streaming and non-streaming respons """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/openai/openai_assistants_provider_methods.py b/python/samples/getting_started/agents/openai/openai_assistants_provider_methods.py index 1c3ed11642..8b5b7ed5ce 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_provider_methods.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_provider_methods.py @@ -20,7 +20,9 @@ This sample demonstrates the methods available on the OpenAIAssistantProvider cl """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/openai/openai_assistants_with_code_interpreter.py b/python/samples/getting_started/agents/openai/openai_assistants_with_code_interpreter.py index 0599e796ea..f05264423e 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_with_code_interpreter.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_with_code_interpreter.py @@ -3,8 +3,8 @@ import asyncio import os -from agent_framework import AgentResponseUpdate, ChatResponseUpdate, HostedCodeInterpreterTool -from agent_framework.openai import OpenAIAssistantProvider +from agent_framework import AgentResponseUpdate, ChatResponseUpdate +from agent_framework.openai import OpenAIAssistantProvider, OpenAIAssistantsClient from openai import AsyncOpenAI from openai.types.beta.threads.runs import ( CodeInterpreterToolCallDelta, @@ -17,7 +17,7 @@ from openai.types.beta.threads.runs.code_interpreter_tool_call_delta import Code """ OpenAI Assistants with Code Interpreter Example -This sample demonstrates using HostedCodeInterpreterTool with OpenAI Assistants +This sample demonstrates using get_code_interpreter_tool() with OpenAI Assistants for Python code execution and mathematical problem solving. """ @@ -42,17 +42,18 @@ def get_code_interpreter_chunk(chunk: AgentResponseUpdate) -> str | None: async def main() -> None: - """Example showing how to use the HostedCodeInterpreterTool with OpenAI Assistants.""" + """Example showing how to use the code interpreter tool with OpenAI Assistants.""" print("=== OpenAI Assistants Provider with Code Interpreter Example ===") client = AsyncOpenAI() provider = OpenAIAssistantProvider(client) + chat_client = OpenAIAssistantsClient(client=client) agent = await provider.create_agent( name="CodeHelper", model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), instructions="You are a helpful assistant that can write and execute Python code to solve problems.", - tools=[HostedCodeInterpreterTool()], + tools=[chat_client.get_code_interpreter_tool()], ) try: diff --git a/python/samples/getting_started/agents/openai/openai_assistants_with_explicit_settings.py b/python/samples/getting_started/agents/openai/openai_assistants_with_explicit_settings.py index 70622f714b..15ac03c574 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_with_explicit_settings.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_with_explicit_settings.py @@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -43,7 +45,9 @@ async def main() -> None: ) try: - result = await agent.run("What's the weather like in New York?") + query = "What's the weather like in New York?" + print(f"Query: {query}") + result = await agent.run(query) print(f"Result: {result}\n") finally: await client.beta.assistants.delete(agent.id) diff --git a/python/samples/getting_started/agents/openai/openai_assistants_with_file_search.py b/python/samples/getting_started/agents/openai/openai_assistants_with_file_search.py index 0046be1206..505a3a3957 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_with_file_search.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_with_file_search.py @@ -3,14 +3,14 @@ import asyncio import os -from agent_framework import Content, HostedFileSearchTool -from agent_framework.openai import OpenAIAssistantProvider +from agent_framework import Content +from agent_framework.openai import OpenAIAssistantProvider, OpenAIAssistantsClient from openai import AsyncOpenAI """ OpenAI Assistants with File Search Example -This sample demonstrates using HostedFileSearchTool with OpenAI Assistants +This sample demonstrates using get_file_search_tool() with OpenAI Assistants for document-based question answering and information retrieval. """ @@ -42,29 +42,30 @@ async def main() -> None: client = AsyncOpenAI() provider = OpenAIAssistantProvider(client) + chat_client = OpenAIAssistantsClient(client=client) agent = await provider.create_agent( name="SearchAssistant", model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), instructions="You are a helpful assistant that searches files in a knowledge base.", - tools=[HostedFileSearchTool()], + tools=[chat_client.get_file_search_tool()], ) try: query = "What is the weather today? Do a file search to find the answer." - file_id, vector_store = await create_vector_store(client) + file_id, vector_store_content = await create_vector_store(client) print(f"User: {query}") print("Agent: ", end="", flush=True) async for chunk in agent.run( query, stream=True, - options={"tool_resources": {"file_search": {"vector_store_ids": [vector_store.vector_store_id]}}}, + options={"tool_resources": {"file_search": {"vector_store_ids": [vector_store_content.vector_store_id]}}}, ): if chunk.text: print(chunk.text, end="", flush=True) - await delete_vector_store(client, file_id, vector_store.vector_store_id) + await delete_vector_store(client, file_id, vector_store_content.vector_store_id) finally: await client.beta.assistants.delete(agent.id) diff --git a/python/samples/getting_started/agents/openai/openai_assistants_with_thread.py b/python/samples/getting_started/agents/openai/openai_assistants_with_thread.py index 02b8086199..d21ee82b5b 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_with_thread.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_with_thread.py @@ -18,7 +18,9 @@ persistent conversation threads and context preservation across interactions. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_basic.py b/python/samples/getting_started/agents/openai/openai_chat_client_basic.py index b7137b2d43..d5d238c5a9 100644 --- a/python/samples/getting_started/agents/openai/openai_chat_client_basic.py +++ b/python/samples/getting_started/agents/openai/openai_chat_client_basic.py @@ -15,7 +15,9 @@ interactions, showing both streaming and non-streaming responses. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, "The location to get the weather for."], diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_with_explicit_settings.py b/python/samples/getting_started/agents/openai/openai_chat_client_with_explicit_settings.py index 0bac0b863c..4090263c8a 100644 --- a/python/samples/getting_started/agents/openai/openai_chat_client_with_explicit_settings.py +++ b/python/samples/getting_started/agents/openai/openai_chat_client_with_explicit_settings.py @@ -17,7 +17,9 @@ settings rather than relying on environment variable defaults. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_with_function_tools.py b/python/samples/getting_started/agents/openai/openai_chat_client_with_function_tools.py index 057989d228..47fb4ef678 100644 --- a/python/samples/getting_started/agents/openai/openai_chat_client_with_function_tools.py +++ b/python/samples/getting_started/agents/openai/openai_chat_client_with_function_tools.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from random import randint from typing import Annotated -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIChatClient from pydantic import Field @@ -17,7 +17,9 @@ showing both agent-level and query-level tool configuration patterns. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -40,8 +42,8 @@ async def tools_on_agent_level() -> None: # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) @@ -70,8 +72,8 @@ async def tools_on_run_level() -> None: print("=== Tools Passed to Run Method ===") # Agent created without tools - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), instructions="You are a helpful assistant.", # No tools defined here ) @@ -100,8 +102,8 @@ async def mixed_tools_example() -> None: print("=== Mixed Tools Example (Agent + Run Method) ===") # Agent created with some base tools - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_with_local_mcp.py b/python/samples/getting_started/agents/openai/openai_chat_client_with_local_mcp.py index e49304adcc..d741a1f6b8 100644 --- a/python/samples/getting_started/agents/openai/openai_chat_client_with_local_mcp.py +++ b/python/samples/getting_started/agents/openai/openai_chat_client_with_local_mcp.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatAgent, MCPStreamableHTTPTool +from agent_framework import Agent, MCPStreamableHTTPTool from agent_framework.openai import OpenAIChatClient """ @@ -29,8 +29,8 @@ async def mcp_tools_on_run_level() -> None: name="Microsoft Learn MCP", url="https://learn.microsoft.com/api/mcp", ) as mcp_server, - ChatAgent( - chat_client=OpenAIChatClient(), + Agent( + client=OpenAIChatClient(), name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", ) as agent, diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_with_thread.py b/python/samples/getting_started/agents/openai/openai_chat_client_with_thread.py index f7a824c370..0982ab7299 100644 --- a/python/samples/getting_started/agents/openai/openai_chat_client_with_thread.py +++ b/python/samples/getting_started/agents/openai/openai_chat_client_with_thread.py @@ -4,7 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread, ChatAgent, ChatMessageStore, tool +from agent_framework import Agent, AgentThread, ChatMessageStore, tool from agent_framework.openai import OpenAIChatClient from pydantic import Field @@ -16,7 +16,9 @@ conversation threads and message history preservation across interactions. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -30,8 +32,8 @@ async def example_with_automatic_thread_creation() -> None: """Example showing automatic thread creation (service-managed thread).""" print("=== Automatic Thread Creation Example ===") - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -55,8 +57,8 @@ async def example_with_thread_persistence() -> None: print("=== Thread Persistence Example ===") print("Using the same thread across multiple conversations to maintain context.\n") - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -88,8 +90,8 @@ async def example_with_existing_thread_messages() -> None: """Example showing how to work with existing thread messages for OpenAI.""" print("=== Existing Thread Messages Example ===") - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -110,8 +112,8 @@ async def example_with_existing_thread_messages() -> None: print("\n--- Continuing with the same thread in a new agent instance ---") # Create a new agent instance but use the existing thread with its message history - new_agent = ChatAgent( - chat_client=OpenAIChatClient(), + new_agent = Agent( + client=OpenAIChatClient(), instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_with_web_search.py b/python/samples/getting_started/agents/openai/openai_chat_client_with_web_search.py index eb1072f945..7370d4fee9 100644 --- a/python/samples/getting_started/agents/openai/openai_chat_client_with_web_search.py +++ b/python/samples/getting_started/agents/openai/openai_chat_client_with_web_search.py @@ -2,30 +2,29 @@ import asyncio -from agent_framework import ChatAgent, HostedWebSearchTool +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient """ OpenAI Chat Client with Web Search Example -This sample demonstrates using HostedWebSearchTool with OpenAI Chat Client +This sample demonstrates using get_web_search_tool() with OpenAI Chat Client for real-time information retrieval and current data access. """ async def main() -> None: - # Test that the agent will use the web search tool with location - additional_properties = { - "user_location": { - "country": "US", - "city": "Seattle", - } - } + client = OpenAIChatClient(model_id="gpt-4o-search-preview") - agent = ChatAgent( - chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"), + # Create web search tool with location context + web_search_tool = client.get_web_search_tool( + user_location={"city": "Seattle", "country": "US"}, + ) + + agent = Agent( + client=client, instructions="You are a helpful assistant that can search the web for current information.", - tools=[HostedWebSearchTool(additional_properties=additional_properties)], + tools=[web_search_tool], ) message = "What is the current weather? Do not ask for my current location." diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_basic.py b/python/samples/getting_started/agents/openai/openai_responses_client_basic.py index b564f07d51..e3ca638783 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_basic.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_basic.py @@ -6,11 +6,12 @@ from random import randint from typing import Annotated from agent_framework import ( - ChatAgent, + Agent, ChatContext, - ChatMessage, ChatResponse, + Message, MiddlewareTermination, + Role, chat_middleware, tool, ) @@ -46,8 +47,8 @@ async def security_and_override_middleware( # Override the response instead of calling AI context.result = ChatResponse( messages=[ - ChatMessage( - role="assistant", + Message( + role=Role.ASSISTANT, text="I cannot process requests containing sensitive information. " "Please rephrase your question without including passwords, secrets, or other " "sensitive data.", @@ -55,8 +56,8 @@ async def security_and_override_middleware( ] ) - # Set terminate flag to stop execution - raise MiddlewareTermination + # Terminate middleware execution with the blocked response + raise MiddlewareTermination(result=context.result) # Continue to next middleware or AI execution await call_next(context) @@ -65,7 +66,9 @@ async def security_and_override_middleware( print(type(context.result)) -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -79,8 +82,8 @@ async def non_streaming_example() -> None: """Example of non-streaming response (get the complete result at once).""" print("=== Non-streaming Response Example ===") - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + agent = Agent( + client=OpenAIResponsesClient(), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -95,12 +98,12 @@ async def streaming_example() -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") - agent = ChatAgent( - chat_client=OpenAIResponsesClient( + agent = Agent( + client=OpenAIResponsesClient( middleware=[security_and_override_middleware], ), instructions="You are a helpful weather agent.", - # tools=get_weather, + tools=get_weather, ) query = "What's the weather like in Portland?" diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_image_analysis.py b/python/samples/getting_started/agents/openai/openai_responses_client_image_analysis.py index c9c56d5e48..93c517b97b 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_image_analysis.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_image_analysis.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework.openai import OpenAIResponsesClient """ @@ -23,12 +23,12 @@ async def main(): ) # 2. Create a simple message with both text and image content - user_message = ChatMessage( + user_message = Message( role="user", contents=[ Content.from_text(text="What do you see in this image?"), Content.from_uri( - uri="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", + uri="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800", media_type="image/jpeg", ), ], diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_image_generation.py b/python/samples/getting_started/agents/openai/openai_responses_client_image_generation.py index 7d3c724b08..1e015b3762 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_image_generation.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_image_generation.py @@ -2,8 +2,12 @@ import asyncio import base64 +import tempfile +import urllib.request as urllib_request +from pathlib import Path -from agent_framework import HostedImageGenerationTool +import aiofiles # pyright: ignore[reportMissingModuleSource] +from agent_framework import Content from agent_framework.openai import OpenAIResponsesClient """ @@ -16,65 +20,80 @@ and automated visual asset generation. """ -def show_image_info(data_uri: str) -> None: - """Display information about the generated image.""" - try: - # Extract format and size info from data URI - if data_uri.startswith("data:image/"): - format_info = data_uri.split(";")[0].split("/")[1] - base64_data = data_uri.split(",", 1)[1] - image_bytes = base64.b64decode(base64_data) - size_kb = len(image_bytes) / 1024 +async def save_image(output: Content) -> None: + """Save the generated image to a temporary directory.""" + filename = "generated_image.webp" + file_path = Path(tempfile.gettempdir()) / filename - print(" Image successfully generated!") - print(f" Format: {format_info.upper()}") - print(f" Size: {size_kb:.1f} KB") - print(f" Data URI length: {len(data_uri)} characters") - print("") - print(" To save and view the image:") - print(' 1. Install Pillow: "pip install pillow" or "uv add pillow"') - print(" 2. Use the data URI in your code to save/display the image") - print(" 3. Or copy the base64 data to an online base64 image decoder") + data_bytes: bytes | None = None + uri = getattr(output, "uri", None) + + if isinstance(uri, str): + if ";base64," in uri: + try: + b64 = uri.split(";base64,", 1)[1] + data_bytes = base64.b64decode(b64) + except Exception: + data_bytes = None else: - print(f" Image URL generated: {data_uri}") - print(" You can open this URL in a browser to view the image") + try: + data_bytes = await asyncio.to_thread(lambda: urllib_request.urlopen(uri).read()) + except Exception: + data_bytes = None - except Exception as e: - print(f" Error processing image data: {e}") - print(" Image generated but couldn't parse details") + if data_bytes is None: + raise RuntimeError("Image output present but could not retrieve bytes.") + + async with aiofiles.open(file_path, "wb") as f: + await f.write(data_bytes) + + print(f"Image downloaded and saved to: {file_path}") async def main() -> None: print("=== OpenAI Responses Image Generation Agent Example ===") # Create an agent with customized image generation options - agent = OpenAIResponsesClient().as_agent( + client = OpenAIResponsesClient() + agent = client.as_agent( instructions="You are a helpful AI that can generate images.", tools=[ - HostedImageGenerationTool( - options={ - "size": "1024x1024", - "output_format": "webp", - } + client.get_image_generation_tool( + size="1024x1024", + output_format="webp", ) ], ) - query = "Generate a nice beach scenery with blue skies in summer time." + query = "Generate a black furry cat." print(f"User: {query}") - print("Generating image with parameters: 1024x1024 size, transparent background, low quality, WebP format...") + print("Generating image with parameters: 1024x1024 size, WebP format...") result = await agent.run(query) print(f"Agent: {result.text}") - # Show information about the generated image + # Find and save the generated image + image_saved = False for message in result.messages: for content in message.contents: - if content.type == "image_generation_tool_result" and content.outputs: - for output in content.outputs: - if output.type in ("data", "uri") and output.uri: - show_image_info(output.uri) - break + if content.type == "image_generation_tool_result_tool_result" and content.outputs: + output = content.outputs + if isinstance(output, Content) and output.uri: + await save_image(output) + image_saved = True + elif isinstance(output, list): + for out in output: + if isinstance(out, Content) and out.uri: + await save_image(out) + image_saved = True + break + if image_saved: + break + if image_saved: + break + + if not image_saved: + print("No image data found in the agent response.") if __name__ == "__main__": diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_streaming_image_generation.py b/python/samples/getting_started/agents/openai/openai_responses_client_streaming_image_generation.py index 4fbf2b0da5..5921a9b07b 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_streaming_image_generation.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_streaming_image_generation.py @@ -2,9 +2,10 @@ import asyncio import base64 +import tempfile +from pathlib import Path import anyio -from agent_framework import HostedImageGenerationTool from agent_framework.openai import OpenAIResponsesClient """OpenAI Responses Client Streaming Image Generation Example @@ -42,15 +43,14 @@ async def main(): print("=== OpenAI Streaming Image Generation Example ===\n") # Create agent with streaming image generation enabled - agent = OpenAIResponsesClient().as_agent( + client = OpenAIResponsesClient() + agent = client.as_agent( instructions="You are a helpful agent that can generate images.", tools=[ - HostedImageGenerationTool( - options={ - "size": "1024x1024", - "quality": "high", - "partial_images": 3, - } + client.get_image_generation_tool( + size="1024x1024", + quality="high", + partial_images=3, ) ], ) @@ -62,9 +62,9 @@ async def main(): # Track partial images image_count = 0 - # Create output directory - output_dir = anyio.Path("generated_images") - await output_dir.mkdir(exist_ok=True) + # Use temp directory for output + output_dir = Path(tempfile.gettempdir()) / "generated_images" + output_dir.mkdir(exist_ok=True) print(" Streaming response:") async for update in agent.run(query, stream=True): @@ -72,7 +72,11 @@ async def main(): # Handle partial images # The final partial image IS the complete, full-quality image. Each partial # represents a progressive refinement, with the last one being the finished result. - if content.type == "data" and content.additional_properties.get("is_partial_image"): + if ( + content.type == "uri" + and content.additional_properties + and content.additional_properties.get("is_partial_image") + ): print(f" Image {image_count} received") # Extract file extension from media_type (e.g., "image/png" -> "png") @@ -89,7 +93,7 @@ async def main(): # Summary print("\n Summary:") print(f" Images received: {image_count}") - print(" Output directory: generated_images") + print(f" Output directory: {output_dir}") print("\n Streaming image generation completed!") diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py index 71d81d9ba8..915915bc90 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter.py @@ -3,27 +3,28 @@ import asyncio from agent_framework import ( - ChatAgent, - HostedCodeInterpreterTool, + Agent, + Content, ) from agent_framework.openai import OpenAIResponsesClient """ OpenAI Responses Client with Code Interpreter Example -This sample demonstrates using HostedCodeInterpreterTool with OpenAI Responses Client +This sample demonstrates using get_code_interpreter_tool() with OpenAI Responses Client for Python code execution and mathematical problem solving. """ async def main() -> None: - """Example showing how to use the HostedCodeInterpreterTool with OpenAI Responses.""" + """Example showing how to use the code interpreter tool with OpenAI Responses.""" print("=== OpenAI Responses Agent with Code Interpreter Example ===") - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + client = OpenAIResponsesClient() + agent = Agent( + client=client, instructions="You are a helpful assistant that can write and execute Python code to solve problems.", - tools=HostedCodeInterpreterTool(), + tools=client.get_code_interpreter_tool(), ) query = "Use code to get the factorial of 100?" @@ -34,16 +35,17 @@ async def main() -> None: for message in result.messages: code_blocks = [c for c in message.contents if c.type == "code_interpreter_tool_call"] outputs = [c for c in message.contents if c.type == "code_interpreter_tool_result"] + if code_blocks: code_inputs = code_blocks[0].inputs or [] for content in code_inputs: - if content.type == "text": + if isinstance(content, Content) and content.type == "text": print(f"Generated code:\n{content.text}") break if outputs: print("Execution outputs:") for out in outputs[0].outputs or []: - if out.type == "text": + if isinstance(out, Content) and out.type == "text": print(out.text) diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter_files.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter_files.py index f3d311e307..195c162c5c 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter_files.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_code_interpreter_files.py @@ -4,14 +4,14 @@ import asyncio import os import tempfile -from agent_framework import ChatAgent, HostedCodeInterpreterTool +from agent_framework import Agent from agent_framework.openai import OpenAIResponsesClient from openai import AsyncOpenAI """ OpenAI Responses Client with Code Interpreter and Files Example -This sample demonstrates using HostedCodeInterpreterTool with OpenAI Responses Client +This sample demonstrates using get_code_interpreter_tool() with OpenAI Responses Client for Python code execution and data analysis with uploaded files. """ @@ -66,10 +66,11 @@ async def main() -> None: temp_file_path, file_id = await create_sample_file_and_upload(openai_client) # Create agent using OpenAI Responses client - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + client = OpenAIResponsesClient() + agent = Agent( + client=client, instructions="You are a helpful assistant that can analyze data files using Python code.", - tools=HostedCodeInterpreterTool(inputs=[{"file_id": file_id}]), + tools=client.get_code_interpreter_tool(file_ids=[file_id]), ) # Test the code interpreter with the uploaded file diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_explicit_settings.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_explicit_settings.py index 826fd880bf..c8fdb24ffb 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_explicit_settings.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_explicit_settings.py @@ -17,7 +17,9 @@ settings rather than relying on environment variable defaults. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_file_search.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_file_search.py index 3784c5a715..daa0d24e38 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_file_search.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_file_search.py @@ -2,13 +2,13 @@ import asyncio -from agent_framework import ChatAgent, Content, HostedFileSearchTool +from agent_framework import Agent, Content from agent_framework.openai import OpenAIResponsesClient """ OpenAI Responses Client with File Search Example -This sample demonstrates using HostedFileSearchTool with OpenAI Responses Client +This sample demonstrates using get_file_search_tool() with OpenAI Responses Client for direct document-based question answering and information retrieval. """ @@ -33,7 +33,6 @@ async def create_vector_store(client: OpenAIResponsesClient) -> tuple[str, Conte async def delete_vector_store(client: OpenAIResponsesClient, file_id: str, vector_store_id: str) -> None: """Delete the vector store after using it.""" - await client.client.vector_stores.delete(vector_store_id=vector_store_id) await client.client.files.delete(file_id=file_id) @@ -45,12 +44,12 @@ async def main() -> None: stream = False print(f"User: {message}") - file_id, vector_store = await create_vector_store(client) + file_id, vector_store_id = await create_vector_store(client) - agent = ChatAgent( - chat_client=client, + agent = Agent( + client=client, instructions="You are a helpful assistant that can search through files to find information.", - tools=[HostedFileSearchTool(inputs=vector_store)], + tools=[client.get_file_search_tool(vector_store_ids=[vector_store_id])], ) if stream: @@ -62,7 +61,7 @@ async def main() -> None: else: response = await agent.run(message) print(f"Assistant: {response}") - await delete_vector_store(client, file_id, vector_store.vector_store_id) + await delete_vector_store(client, file_id, vector_store_id) if __name__ == "__main__": diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_function_tools.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_function_tools.py index 032a8b20d8..ccdf2b0dc0 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_function_tools.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_function_tools.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from random import randint from typing import Annotated -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.openai import OpenAIResponsesClient from pydantic import Field @@ -17,7 +17,9 @@ showing both agent-level and query-level tool configuration patterns. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -40,8 +42,8 @@ async def tools_on_agent_level() -> None: # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + agent = Agent( + client=OpenAIResponsesClient(), instructions="You are a helpful assistant that can provide weather and time information.", tools=[get_weather, get_time], # Tools defined at agent creation ) @@ -70,8 +72,8 @@ async def tools_on_run_level() -> None: print("=== Tools Passed to Run Method ===") # Agent created without tools - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + agent = Agent( + client=OpenAIResponsesClient(), instructions="You are a helpful assistant.", # No tools defined here ) @@ -100,8 +102,8 @@ async def mixed_tools_example() -> None: print("=== Mixed Tools Example (Agent + Run Method) ===") # Agent created with some base tools - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + agent = Agent( + client=OpenAIResponsesClient(), instructions="You are a comprehensive assistant that can help with various information requests.", tools=[get_weather], # Base tool available for all queries ) diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py index 526503f813..f934cd0820 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_hosted_mcp.py @@ -3,7 +3,7 @@ import asyncio from typing import TYPE_CHECKING, Any -from agent_framework import ChatAgent, HostedMCPTool +from agent_framework import Agent from agent_framework.openai import OpenAIResponsesClient """ @@ -19,7 +19,7 @@ if TYPE_CHECKING: async def handle_approvals_without_thread(query: str, agent: "SupportsAgentRun"): """When we don't have a thread, we need to ensure we return with the input, approval request and approval.""" - from agent_framework import ChatMessage + from agent_framework import Message result = await agent.run(query) while len(result.user_input_requests) > 0: @@ -29,10 +29,13 @@ async def handle_approvals_without_thread(query: str, agent: "SupportsAgentRun") f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}" f" with arguments: {user_input_needed.function_call.arguments}" ) - new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed])) + new_inputs.append(Message(role="assistant", contents=[user_input_needed])) user_approval = input("Approve function call? (y/n): ") new_inputs.append( - ChatMessage(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) + Message( + role="user", + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], + ) ) result = await agent.run(new_inputs) @@ -41,7 +44,7 @@ async def handle_approvals_without_thread(query: str, agent: "SupportsAgentRun") async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", thread: "AgentThread"): """Here we let the thread deal with the previous responses, and we just rerun with the approval.""" - from agent_framework import ChatMessage + from agent_framework import Message result = await agent.run(query, thread=thread, store=True) while len(result.user_input_requests) > 0: @@ -53,7 +56,7 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th ) user_approval = input("Approve function call? (y/n): ") new_input.append( - ChatMessage( + Message( role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) @@ -64,13 +67,13 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th async def handle_approvals_with_thread_streaming(query: str, agent: "SupportsAgentRun", thread: "AgentThread"): """Here we let the thread deal with the previous responses, and we just rerun with the approval.""" - from agent_framework import ChatMessage + from agent_framework import Message - new_input: list[ChatMessage] = [] + new_input: list[Message] = [] new_input_added = True while new_input_added: new_input_added = False - new_input.append(ChatMessage(role="user", text=query)) + new_input.append(Message(role="user", text=query)) async for update in agent.run(new_input, thread=thread, stream=True, options={"store": True}): if update.user_input_requests: for user_input_needed in update.user_input_requests: @@ -80,8 +83,9 @@ async def handle_approvals_with_thread_streaming(query: str, agent: "SupportsAge ) user_approval = input("Approve function call? (y/n): ") new_input.append( - ChatMessage( - role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")] + Message( + role="user", + contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")], ) ) new_input_added = True @@ -93,19 +97,21 @@ async def run_hosted_mcp_without_thread_and_specific_approval() -> None: """Example showing Mcp Tools with approvals without using a thread.""" print("=== Mcp with approvals and without thread ===") - # Tools are provided when creating the agent - # The agent can use these tools for any query during its lifetime - async with ChatAgent( - chat_client=OpenAIResponsesClient(), + client = OpenAIResponsesClient() + # Create MCP tool with specific approval mode + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + # we don't require approval for microsoft_docs_search tool calls + # but we do for any other tool + approval_mode={"never_require_approval": ["microsoft_docs_search"]}, + ) + + async with Agent( + client=client, name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - # we don't require approval for microsoft_docs_search tool calls - # but we do for any other tool - approval_mode={"never_require_approval": ["microsoft_docs_search"]}, - ), + tools=mcp_tool, ) as agent: # First query query1 = "How to create an Azure storage account using az cli?" @@ -124,20 +130,20 @@ async def run_hosted_mcp_without_approval() -> None: """Example showing Mcp Tools without approvals.""" print("=== Mcp without approvals ===") - # Tools are provided when creating the agent - # The agent can use these tools for any query during its lifetime - async with ChatAgent( - chat_client=OpenAIResponsesClient(), + client = OpenAIResponsesClient() + # Create MCP tool that never requires approval + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + # we don't require approval for any function calls + approval_mode="never_require", + ) + + async with Agent( + client=client, name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - # we don't require approval for any function calls - # this means we will not see the approval messages, - # it is fully handled by the service and a final response is returned. - approval_mode="never_require", - ), + tools=mcp_tool, ) as agent: # First query query1 = "How to create an Azure storage account using az cli?" @@ -156,18 +162,20 @@ async def run_hosted_mcp_with_thread() -> None: """Example showing Mcp Tools with approvals using a thread.""" print("=== Mcp with approvals and with thread ===") - # Tools are provided when creating the agent - # The agent can use these tools for any query during its lifetime - async with ChatAgent( - chat_client=OpenAIResponsesClient(), + client = OpenAIResponsesClient() + # Create MCP tool that always requires approval + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + # we require approval for all function calls + approval_mode="always_require", + ) + + async with Agent( + client=client, name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - # we require approval for all function calls - approval_mode="always_require", - ), + tools=mcp_tool, ) as agent: # First query thread = agent.get_new_thread() @@ -187,18 +195,20 @@ async def run_hosted_mcp_with_thread_streaming() -> None: """Example showing Mcp Tools with approvals using a thread.""" print("=== Mcp with approvals and with thread ===") - # Tools are provided when creating the agent - # The agent can use these tools for any query during its lifetime - async with ChatAgent( - chat_client=OpenAIResponsesClient(), + client = OpenAIResponsesClient() + # Create MCP tool that always requires approval + mcp_tool = client.get_mcp_tool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + # we require approval for all function calls + approval_mode="always_require", + ) + + async with Agent( + client=client, name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", - tools=HostedMCPTool( - name="Microsoft Learn MCP", - url="https://learn.microsoft.com/api/mcp", - # we require approval for all function calls - approval_mode="always_require", - ), + tools=mcp_tool, ) as agent: # First query thread = agent.get_new_thread() diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_local_mcp.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_local_mcp.py index 50ebcf9ad7..1b1e55c28d 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_local_mcp.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_local_mcp.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatAgent, MCPStreamableHTTPTool +from agent_framework import Agent, MCPStreamableHTTPTool from agent_framework.openai import OpenAIResponsesClient """ @@ -22,8 +22,8 @@ async def streaming_with_mcp(show_raw_stream: bool = False) -> None: print("=== Tools Defined on Agent Level ===") # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime - async with ChatAgent( - chat_client=OpenAIResponsesClient(), + async with Agent( + client=OpenAIResponsesClient(), name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", tools=MCPStreamableHTTPTool( # Tools defined at agent creation @@ -60,8 +60,8 @@ async def run_with_mcp() -> None: # Tools are provided when creating the agent # The agent can use these tools for any query during its lifetime - async with ChatAgent( - chat_client=OpenAIResponsesClient(), + async with Agent( + client=OpenAIResponsesClient(), name="DocsAgent", instructions="You are a helpful assistant that can help with microsoft documentation questions.", tools=MCPStreamableHTTPTool( # Tools defined at agent creation diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_thread.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_thread.py index e17c2d2748..ae1a48a743 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_thread.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_thread.py @@ -4,7 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import AgentThread, ChatAgent, tool +from agent_framework import Agent, AgentThread, tool from agent_framework.openai import OpenAIResponsesClient from pydantic import Field @@ -16,7 +16,9 @@ persistent conversation context and simplified response handling. """ -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. +# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; +# see samples/getting_started/tools/function_tool_with_approval.py +# and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @tool(approval_mode="never_require") def get_weather( location: Annotated[str, Field(description="The location to get the weather for.")], @@ -30,8 +32,8 @@ async def example_with_automatic_thread_creation() -> None: """Example showing automatic thread creation.""" print("=== Automatic Thread Creation Example ===") - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + agent = Agent( + client=OpenAIResponsesClient(), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -57,8 +59,8 @@ async def example_with_thread_persistence_in_memory() -> None: """ print("=== Thread Persistence Example (In-Memory) ===") - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + agent = Agent( + client=OpenAIResponsesClient(), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -96,8 +98,8 @@ async def example_with_existing_thread_id() -> None: # First, create a conversation and capture the thread ID existing_thread_id = None - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + agent = Agent( + client=OpenAIResponsesClient(), instructions="You are a helpful weather agent.", tools=get_weather, ) @@ -117,8 +119,8 @@ async def example_with_existing_thread_id() -> None: if existing_thread_id: print("\n--- Continuing with the same thread ID in a new agent instance ---") - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + agent = Agent( + client=OpenAIResponsesClient(), instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/python/samples/getting_started/agents/openai/openai_responses_client_with_web_search.py b/python/samples/getting_started/agents/openai/openai_responses_client_with_web_search.py index 24e0368512..26d148901c 100644 --- a/python/samples/getting_started/agents/openai/openai_responses_client_with_web_search.py +++ b/python/samples/getting_started/agents/openai/openai_responses_client_with_web_search.py @@ -2,30 +2,29 @@ import asyncio -from agent_framework import ChatAgent, HostedWebSearchTool +from agent_framework import Agent from agent_framework.openai import OpenAIResponsesClient """ OpenAI Responses Client with Web Search Example -This sample demonstrates using HostedWebSearchTool with OpenAI Responses Client +This sample demonstrates using get_web_search_tool() with OpenAI Responses Client for direct real-time information retrieval and current data access. """ async def main() -> None: - # Test that the agent will use the web search tool with location - additional_properties = { - "user_location": { - "country": "US", - "city": "Seattle", - } - } + client = OpenAIResponsesClient() - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + # Create web search tool with location context + web_search_tool = client.get_web_search_tool( + user_location={"city": "Seattle", "country": "US"}, + ) + + agent = Agent( + client=client, instructions="You are a helpful assistant that can search the web for current information.", - tools=[HostedWebSearchTool(additional_properties=additional_properties)], + tools=[web_search_tool], ) message = "What is the current weather? Do not ask for my current location." diff --git a/python/samples/getting_started/azure_functions/02_multi_agent/README.md b/python/samples/getting_started/azure_functions/02_multi_agent/README.md index 473d6bb236..e10b9d4d51 100644 --- a/python/samples/getting_started/azure_functions/02_multi_agent/README.md +++ b/python/samples/getting_started/azure_functions/02_multi_agent/README.md @@ -76,8 +76,8 @@ Expected response: { "status": "healthy", "agents": [ - {"name": "WeatherAgent", "type": "ChatAgent"}, - {"name": "MathAgent", "type": "ChatAgent"} + {"name": "WeatherAgent", "type": "Agent"}, + {"name": "MathAgent", "type": "Agent"} ], "agent_count": 2 } diff --git a/python/samples/getting_started/azure_functions/02_multi_agent/function_app.py b/python/samples/getting_started/azure_functions/02_multi_agent/function_app.py index 6a3f396bcb..15e034dd22 100644 --- a/python/samples/getting_started/azure_functions/02_multi_agent/function_app.py +++ b/python/samples/getting_started/azure_functions/02_multi_agent/function_app.py @@ -56,15 +56,15 @@ def calculate_tip(bill_amount: float, tip_percentage: float = 15.0) -> dict[str, # 1. Create multiple agents, each with its own instruction set and tools. -chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) +client = AzureOpenAIChatClient(credential=AzureCliCredential()) -weather_agent = chat_client.as_agent( +weather_agent = client.as_agent( name="WeatherAgent", instructions="You are a helpful weather assistant. Provide current weather information.", tools=[get_weather], ) -math_agent = chat_client.as_agent( +math_agent = client.as_agent( name="MathAgent", instructions="You are a helpful math assistant. Help users with calculations like tip calculations.", tools=[calculate_tip], diff --git a/python/samples/getting_started/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py b/python/samples/getting_started/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py index aad945288c..0be448295d 100644 --- a/python/samples/getting_started/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py +++ b/python/samples/getting_started/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py @@ -30,14 +30,14 @@ CHEMIST_AGENT_NAME = "ChemistAgent" # 2. Instantiate both agents that the orchestration will run concurrently. def _create_agents() -> list[Any]: - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - physicist = chat_client.as_agent( + physicist = client.as_agent( name=PHYSICIST_AGENT_NAME, instructions="You are an expert in physics. You answer questions from a physics perspective.", ) - chemist = chat_client.as_agent( + chemist = client.as_agent( name=CHEMIST_AGENT_NAME, instructions="You are an expert in chemistry. You answer questions from a chemistry perspective.", ) diff --git a/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py b/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py index 54728332f0..0dbfeefd5c 100644 --- a/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py +++ b/python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py @@ -45,14 +45,14 @@ class EmailPayload(BaseModel): # 2. Instantiate both agents so they can be registered with AgentFunctionApp. def _create_agents() -> list[Any]: - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - spam_agent = chat_client.as_agent( + spam_agent = client.as_agent( name=SPAM_AGENT_NAME, instructions="You are a spam detection assistant that identifies spam emails.", ) - email_agent = chat_client.as_agent( + email_agent = client.as_agent( name=EMAIL_AGENT_NAME, instructions="You are an email assistant that helps users draft responses to emails with professionalism.", ) diff --git a/python/samples/getting_started/azure_functions/08_mcp_server/README.md b/python/samples/getting_started/azure_functions/08_mcp_server/README.md index 02fcbbb957..a475823a1a 100644 --- a/python/samples/getting_started/azure_functions/08_mcp_server/README.md +++ b/python/samples/getting_started/azure_functions/08_mcp_server/README.md @@ -142,20 +142,20 @@ The sample shows how to enable MCP tool triggers with flexible agent configurati from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient # Create Azure OpenAI Chat Client -chat_client = AzureOpenAIChatClient() +client = AzureOpenAIChatClient() # Define agents with different roles -joker_agent = chat_client.as_agent( +joker_agent = client.as_agent( name="Joker", instructions="You are good at telling jokes.", ) -stock_agent = chat_client.as_agent( +stock_agent = client.as_agent( name="StockAdvisor", instructions="Check stock prices.", ) -plant_agent = chat_client.as_agent( +plant_agent = client.as_agent( name="PlantAdvisor", instructions="Recommend plants.", description="Get plant recommendations.", diff --git a/python/samples/getting_started/azure_functions/08_mcp_server/function_app.py b/python/samples/getting_started/azure_functions/08_mcp_server/function_app.py index 2d67ddec81..b34361d10e 100644 --- a/python/samples/getting_started/azure_functions/08_mcp_server/function_app.py +++ b/python/samples/getting_started/azure_functions/08_mcp_server/function_app.py @@ -28,23 +28,23 @@ from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient # Create Azure OpenAI Chat Client # This uses AzureCliCredential for authentication (requires 'az login') -chat_client = AzureOpenAIChatClient() +client = AzureOpenAIChatClient() # Define three AI agents with different roles # Agent 1: Joker - HTTP trigger only (default) -agent1 = chat_client.as_agent( +agent1 = client.as_agent( name="Joker", instructions="You are good at telling jokes.", ) # Agent 2: StockAdvisor - MCP tool trigger only -agent2 = chat_client.as_agent( +agent2 = client.as_agent( name="StockAdvisor", instructions="Check stock prices.", ) # Agent 3: PlantAdvisor - Both HTTP and MCP tool triggers -agent3 = chat_client.as_agent( +agent3 = client.as_agent( name="PlantAdvisor", instructions="Recommend plants.", description="Get plant recommendations.", diff --git a/python/samples/getting_started/chat_client/README.md b/python/samples/getting_started/chat_client/README.md index 20060f691d..5bf9b471ad 100644 --- a/python/samples/getting_started/chat_client/README.md +++ b/python/samples/getting_started/chat_client/README.md @@ -14,7 +14,7 @@ This folder contains simple examples demonstrating direct usage of various chat | [`openai_assistants_client.py`](openai_assistants_client.py) | Direct usage of OpenAI Assistants Client for basic chat interactions with OpenAI assistants. | | [`openai_chat_client.py`](openai_chat_client.py) | Direct usage of OpenAI Chat Client for chat interactions with OpenAI models. | | [`openai_responses_client.py`](openai_responses_client.py) | Direct usage of OpenAI Responses Client for structured response generation with OpenAI models. | -| [`custom_chat_client.py`](custom_chat_client.py) | Demonstrates how to create custom chat clients by extending the `BaseChatClient` class. Shows a `EchoingChatClient` implementation and how to integrate it with `ChatAgent` using the `as_agent()` method. | +| [`custom_chat_client.py`](custom_chat_client.py) | Demonstrates how to create custom chat clients by extending the `BaseChatClient` class. Shows a `EchoingChatClient` implementation and how to integrate it with `Agent` using the `as_agent()` method. | ## Environment Variables diff --git a/python/samples/getting_started/chat_client/chat_response_cancellation.py b/python/samples/getting_started/chat_client/chat_response_cancellation.py index 6ed214808d..3435363512 100644 --- a/python/samples/getting_started/chat_client/chat_response_cancellation.py +++ b/python/samples/getting_started/chat_client/chat_response_cancellation.py @@ -21,10 +21,10 @@ async def main() -> None: - OpenAI model ID: Use "model_id" parameter or "OPENAI_CHAT_MODEL_ID" environment variable - OpenAI API key: Use "api_key" parameter or "OPENAI_API_KEY" environment variable """ - chat_client = OpenAIChatClient() + client = OpenAIChatClient() try: - task = asyncio.create_task(chat_client.get_response(messages=["Tell me a fantasy story."])) + task = asyncio.create_task(client.get_response(messages=["Tell me a fantasy story."])) await asyncio.sleep(1) task.cancel() await task diff --git a/python/samples/getting_started/chat_client/custom_chat_client.py b/python/samples/getting_started/chat_client/custom_chat_client.py index 149b7230e1..69228b68ab 100644 --- a/python/samples/getting_started/chat_client/custom_chat_client.py +++ b/python/samples/getting_started/chat_client/custom_chat_client.py @@ -8,12 +8,12 @@ from typing import Any, ClassVar, Generic from agent_framework import ( BaseChatClient, - ChatMessage, ChatMiddlewareLayer, ChatResponse, ChatResponseUpdate, Content, FunctionInvocationLayer, + Message, ResponseStream, Role, ) @@ -61,7 +61,7 @@ class EchoingChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]): def _inner_get_response( self, *, - messages: Sequence[ChatMessage], + messages: Sequence[Message], stream: bool = False, options: Mapping[str, Any], **kwargs: Any, @@ -82,7 +82,7 @@ class EchoingChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]): else: response_text = f"{self.prefix} [No text message found]" - response_message = ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(response_text)]) + response_message = Message(role=Role.ASSISTANT, contents=[Content.from_text(response_text)]) response = ChatResponse( messages=[response_message], @@ -124,7 +124,7 @@ class EchoingChatClientWithLayers( # type: ignore[misc,type-var] async def main() -> None: - """Demonstrates how to implement and use a custom chat client with ChatAgent.""" + """Demonstrates how to implement and use a custom chat client with Agent.""" print("=== Custom Chat Client Example ===\n") # Create the custom chat client diff --git a/python/samples/getting_started/context_providers/README.md b/python/samples/getting_started/context_providers/README.md index ddcc5ffe8d..70b2fdb8ff 100644 --- a/python/samples/getting_started/context_providers/README.md +++ b/python/samples/getting_started/context_providers/README.md @@ -139,14 +139,14 @@ Different agents with isolated or shared memory configurations. To create a custom context provider, implement the `ContextProvider` protocol: ```python -from agent_framework import ContextProvider, Context, ChatMessage +from agent_framework import ContextProvider, Context, Message from collections.abc import MutableSequence, Sequence from typing import Any class MyContextProvider(ContextProvider): async def invoking( self, - messages: ChatMessage | MutableSequence[ChatMessage], + messages: Message | MutableSequence[Message], **kwargs: Any ) -> Context: """Provide context before the agent processes the request.""" @@ -155,8 +155,8 @@ class MyContextProvider(ContextProvider): async def invoked( self, - request_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage] | None = None, + request_messages: Message | Sequence[Message], + response_messages: Message | Sequence[Message] | None = None, invoke_exception: Exception | None = None, **kwargs: Any, ) -> None: diff --git a/python/samples/getting_started/context_providers/aggregate_context_provider.py b/python/samples/getting_started/context_providers/aggregate_context_provider.py index 4d44c0766c..af3780cfc1 100644 --- a/python/samples/getting_started/context_providers/aggregate_context_provider.py +++ b/python/samples/getting_started/context_providers/aggregate_context_provider.py @@ -17,12 +17,12 @@ from contextlib import AsyncExitStack from types import TracebackType from typing import TYPE_CHECKING, Any, cast -from agent_framework import ChatAgent, ChatMessage, Context, ContextProvider +from agent_framework import Agent, Context, ContextProvider, Message from agent_framework.azure import AzureAIClient from azure.identity.aio import AzureCliCredential if TYPE_CHECKING: - from agent_framework import ToolProtocol + from agent_framework import FunctionTool if sys.version_info >= (3, 12): from typing import override # type: ignore # pragma: no cover @@ -47,7 +47,7 @@ class AggregateContextProvider(ContextProvider): Examples: .. code-block:: python - from agent_framework import ChatAgent + from agent_framework import Agent # Create multiple context providers provider1 = CustomContextProvider1() @@ -58,7 +58,7 @@ class AggregateContextProvider(ContextProvider): aggregate = AggregateContextProvider([provider1, provider2, provider3]) # Pass the aggregate to the agent - agent = ChatAgent(chat_client=client, name="assistant", context_provider=aggregate) + agent = Agent(client=client, name="assistant", context_provider=aggregate) # You can also add more providers later provider4 = CustomContextProvider4() @@ -90,11 +90,11 @@ class AggregateContextProvider(ContextProvider): await asyncio.gather(*[x.thread_created(thread_id) for x in self.providers]) @override - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: contexts = await asyncio.gather(*[provider.invoking(messages, **kwargs) for provider in self.providers]) instructions: str = "" - return_messages: list[ChatMessage] = [] - tools: list["ToolProtocol"] = [] + return_messages: list[Message] = [] + tools: list["FunctionTool"] = [] for ctx in contexts: if ctx.instructions: instructions += ctx.instructions @@ -107,8 +107,8 @@ class AggregateContextProvider(ContextProvider): @override async def invoked( self, - request_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage] | None = None, + request_messages: Message | Sequence[Message], + response_messages: Message | Sequence[Message] | None = None, invoke_exception: Exception | None = None, **kwargs: Any, ) -> None: @@ -167,7 +167,7 @@ class TimeContextProvider(ContextProvider): """A simple context provider that adds time-related instructions.""" @override - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: from datetime import datetime current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -181,7 +181,7 @@ class PersonaContextProvider(ContextProvider): self.persona = persona @override - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: return Context(instructions=f"Your persona: {self.persona}. ") @@ -192,7 +192,7 @@ class PreferencesContextProvider(ContextProvider): self.preferences: dict[str, str] = {} @override - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: if not self.preferences: return Context() prefs_str = ", ".join(f"{k}: {v}" for k, v in self.preferences.items()) @@ -201,14 +201,14 @@ class PreferencesContextProvider(ContextProvider): @override async def invoked( self, - request_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage] | None = None, + request_messages: Message | Sequence[Message], + response_messages: Message | Sequence[Message] | None = None, invoke_exception: Exception | None = None, **kwargs: Any, ) -> None: # Simple example: extract and store preferences from user messages # In a real implementation, you might use structured extraction - msgs = [request_messages] if isinstance(request_messages, ChatMessage) else list(request_messages) + msgs = [request_messages] if isinstance(request_messages, Message) else list(request_messages) for msg in msgs: content = msg.text if hasattr(msg, "text") else "" @@ -230,7 +230,7 @@ class PreferencesContextProvider(ContextProvider): async def main(): """Demonstrate using AggregateContextProvider to combine multiple providers.""" async with AzureCliCredential() as credential: - chat_client = AzureAIClient(credential=credential) + client = AzureAIClient(credential=credential) # Create individual context providers time_provider = TimeContextProvider() @@ -245,8 +245,8 @@ async def main(): ]) # Create the agent with the aggregate provider - async with ChatAgent( - chat_client=chat_client, + async with Agent( + client=client, instructions="You are a helpful assistant.", context_provider=aggregate_provider, ) as agent: diff --git a/python/samples/getting_started/context_providers/azure_ai_search/README.md b/python/samples/getting_started/context_providers/azure_ai_search/README.md index fe7635e72f..ecb00f68b4 100644 --- a/python/samples/getting_started/context_providers/azure_ai_search/README.md +++ b/python/samples/getting_started/context_providers/azure_ai_search/README.md @@ -126,7 +126,7 @@ AZURE_OPENAI_RESOURCE_URL=https://myresource.openai.azure.com ### Semantic Mode ```python -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureAIAgentClient, AzureAISearchContextProvider from azure.identity.aio import DefaultAzureCredential @@ -141,8 +141,8 @@ search_provider = AzureAISearchContextProvider( # Create agent with search context async with AzureAIAgentClient(credential=DefaultAzureCredential()) as client: - async with ChatAgent( - chat_client=client, + async with Agent( + client=client, model=model_deployment, context_provider=search_provider, ) as agent: @@ -166,8 +166,8 @@ search_provider = AzureAISearchContextProvider( ) # Use with agent (same as semantic mode) -async with ChatAgent( - chat_client=client, +async with Agent( + client=client, model=model_deployment, context_provider=search_provider, ) as agent: diff --git a/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py b/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py index 6e3e40a216..7b68265885 100644 --- a/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py +++ b/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py @@ -3,7 +3,7 @@ import asyncio import os -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureAIAgentClient, AzureAISearchContextProvider from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv @@ -112,8 +112,8 @@ async def main() -> None: model_deployment_name=model_deployment, credential=AzureCliCredential(), ) as client, - ChatAgent( - chat_client=client, + Agent( + client=client, name="SearchAgent", instructions=( "You are a helpful assistant with advanced reasoning capabilities. " diff --git a/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py b/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py index 4fce526a1f..04e26e535e 100644 --- a/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py +++ b/python/samples/getting_started/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py @@ -3,7 +3,7 @@ import asyncio import os -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureAIAgentClient, AzureAISearchContextProvider from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv @@ -69,8 +69,8 @@ async def main() -> None: model_deployment_name=model_deployment, credential=AzureCliCredential(), ) as client, - ChatAgent( - chat_client=client, + Agent( + client=client, name="SearchAgent", instructions=( "You are a helpful assistant. Use the provided context from the " diff --git a/python/samples/getting_started/context_providers/redis/redis_basics.py b/python/samples/getting_started/context_providers/redis/redis_basics.py index 9f5a654ea1..f984354df7 100644 --- a/python/samples/getting_started/context_providers/redis/redis_basics.py +++ b/python/samples/getting_started/context_providers/redis/redis_basics.py @@ -30,7 +30,7 @@ Run: import asyncio import os -from agent_framework import ChatMessage, tool +from agent_framework import Message, tool from agent_framework.openai import OpenAIChatClient from agent_framework_redis._provider import RedisProvider from redisvl.extensions.cache.embeddings import EmbeddingsCache @@ -128,9 +128,9 @@ async def main() -> None: # Build sample chat messages to persist to Redis messages = [ - ChatMessage("user", ["runA CONVO: User Message"]), - ChatMessage("assistant", ["runA CONVO: Assistant Message"]), - ChatMessage("system", ["runA CONVO: System Message"]), + Message("user", ["runA CONVO: User Message"]), + Message("assistant", ["runA CONVO: Assistant Message"]), + Message("system", ["runA CONVO: System Message"]), ] # Declare/start a conversation/thread and write messages under 'runA'. @@ -142,7 +142,7 @@ async def main() -> None: # Retrieve relevant memories for a hypothetical model call. The provider uses # the current request messages as the retrieval query and returns context to # be injected into the model's instructions. - ctx = await provider.invoking([ChatMessage("system", ["B: Assistant Message"])]) + ctx = await provider.invoking([Message("system", ["B: Assistant Message"])]) # Inspect retrieved memories that would be injected into instructions # (Debug-only output so you can verify retrieval works as expected.) diff --git a/python/samples/getting_started/context_providers/simple_context_provider.py b/python/samples/getting_started/context_providers/simple_context_provider.py index e32266cb14..e151651199 100644 --- a/python/samples/getting_started/context_providers/simple_context_provider.py +++ b/python/samples/getting_started/context_providers/simple_context_provider.py @@ -4,7 +4,7 @@ import asyncio from collections.abc import MutableSequence, Sequence from typing import Any -from agent_framework import ChatAgent, ChatClientProtocol, ChatMessage, Context, ContextProvider +from agent_framework import Agent, Context, ContextProvider, Message, SupportsChatGetResponse from agent_framework.azure import AzureAIClient from azure.identity.aio import AzureCliCredential from pydantic import BaseModel @@ -16,13 +16,13 @@ class UserInfo(BaseModel): class UserInfoMemory(ContextProvider): - def __init__(self, chat_client: ChatClientProtocol, user_info: UserInfo | None = None, **kwargs: Any): + def __init__(self, client: SupportsChatGetResponse, user_info: UserInfo | None = None, **kwargs: Any): """Create the memory. If you pass in kwargs, they will be attempted to be used to create a UserInfo object. """ - self._chat_client = chat_client + self._chat_client = client if user_info: self.user_info = user_info elif kwargs: @@ -32,8 +32,8 @@ class UserInfoMemory(ContextProvider): async def invoked( self, - request_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage] | None = None, + request_messages: Message | Sequence[Message], + response_messages: Message | Sequence[Message] | None = None, invoke_exception: Exception | None = None, **kwargs: Any, ) -> None: @@ -64,7 +64,7 @@ class UserInfoMemory(ContextProvider): except Exception: pass # Failed to extract, continue without updating - async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context: + async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context: """Provide user information context before each agent call.""" instructions: list[str] = [] @@ -92,14 +92,14 @@ class UserInfoMemory(ContextProvider): async def main(): async with AzureCliCredential() as credential: - chat_client = AzureAIClient(credential=credential) + client = AzureAIClient(credential=credential) # Create the memory provider - memory_provider = UserInfoMemory(chat_client) + memory_provider = UserInfoMemory(client) # Create the agent with memory - async with ChatAgent( - chat_client=chat_client, + async with Agent( + client=client, instructions="You are a friendly assistant. Always address the user by their name.", context_provider=memory_provider, ) as agent: diff --git a/python/samples/getting_started/declarative/README.md b/python/samples/getting_started/declarative/README.md index 6241f632d4..35a75ef36b 100644 --- a/python/samples/getting_started/declarative/README.md +++ b/python/samples/getting_started/declarative/README.md @@ -175,7 +175,7 @@ agent = agent_factory.create_agent_from_yaml_path(Path("custom_provider.yaml")) This allows you to extend the declarative framework with custom chat client implementations. The mapping requires: - **package**: The Python package/module to import from -- **name**: The class name of your ChatClientProtocol implementation +- **name**: The class name of your SupportsChatGetResponse implementation - **model_id_field**: The constructor parameter name that accepts the value of the `model.id` field from the YAML You can reference your custom provider using either `Provider.ApiType` format or just `Provider` in your YAML configuration, as long as it matches the registered mapping. diff --git a/python/samples/getting_started/declarative/get_weather_agent.py b/python/samples/getting_started/declarative/get_weather_agent.py index 4e54af2461..af44382c00 100644 --- a/python/samples/getting_started/declarative/get_weather_agent.py +++ b/python/samples/getting_started/declarative/get_weather_agent.py @@ -26,7 +26,7 @@ async def main(): # create the AgentFactory with a chat client and bindings agent_factory = AgentFactory( - chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), + client=AzureOpenAIResponsesClient(credential=AzureCliCredential()), bindings={"get_weather": get_weather}, ) # create the agent from the yaml diff --git a/python/samples/getting_started/devui/README.md b/python/samples/getting_started/devui/README.md index bfbee3a70b..5c16e1de71 100644 --- a/python/samples/getting_started/devui/README.md +++ b/python/samples/getting_started/devui/README.md @@ -44,7 +44,7 @@ Each agent/workflow follows a strict structure required by DevUI's discovery sys ``` agent_name/ -├── __init__.py # Must export: agent = ChatAgent(...) +├── __init__.py # Must export: agent = Agent(...) ├── agent.py # Agent implementation └── .env.example # Example environment variables ``` @@ -100,13 +100,13 @@ Example: ```python # my_agent/__init__.py -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient -agent = ChatAgent( +agent = Agent( name="MyAgent", description="My custom agent", - chat_client=OpenAIChatClient(), + client=OpenAIChatClient(), # ... your configuration ) ``` diff --git a/python/samples/getting_started/devui/azure_responses_agent/agent.py b/python/samples/getting_started/devui/azure_responses_agent/agent.py index b2fbe9c995..bf167f55c2 100644 --- a/python/samples/getting_started/devui/azure_responses_agent/agent.py +++ b/python/samples/getting_started/devui/azure_responses_agent/agent.py @@ -21,7 +21,7 @@ import logging import os from typing import Annotated -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.azure import AzureOpenAIResponsesClient logger = logging.getLogger(__name__) @@ -68,7 +68,7 @@ def extract_key_points( # Agent using Azure OpenAI Responses API (supports PDF uploads!) -agent = ChatAgent( +agent = Agent( name="AzureResponsesAgent", description="An agent that can analyze PDFs, images, and other documents using Azure OpenAI Responses API", instructions=""" @@ -85,7 +85,7 @@ agent = ChatAgent( For PDFs, you can read and understand the text, tables, and structure. For images, you can describe what you see and extract any text. """, - chat_client=AzureOpenAIResponsesClient( + client=AzureOpenAIResponsesClient( deployment_name=_deployment_name, endpoint=_endpoint, api_version="2025-03-01-preview", # Required for Responses API diff --git a/python/samples/getting_started/devui/foundry_agent/agent.py b/python/samples/getting_started/devui/foundry_agent/agent.py index f2ce12058d..01a033689b 100644 --- a/python/samples/getting_started/devui/foundry_agent/agent.py +++ b/python/samples/getting_started/devui/foundry_agent/agent.py @@ -8,7 +8,7 @@ Make sure to run 'az login' before starting devui. import os from typing import Annotated -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.azure import AzureAIAgentClient from azure.identity.aio import AzureCliCredential from pydantic import Field @@ -43,9 +43,9 @@ def get_forecast( # Agent instance following Agent Framework conventions -agent = ChatAgent( +agent = Agent( name="FoundryWeatherAgent", - chat_client=AzureAIAgentClient( + client=AzureAIAgentClient( project_endpoint=os.environ.get("AZURE_AI_PROJECT_ENDPOINT"), model_deployment_name=os.environ.get("FOUNDRY_MODEL_DEPLOYMENT_NAME"), credential=AzureCliCredential(), diff --git a/python/samples/getting_started/devui/in_memory_mode.py b/python/samples/getting_started/devui/in_memory_mode.py index 9f98d9be50..5d32861740 100644 --- a/python/samples/getting_started/devui/in_memory_mode.py +++ b/python/samples/getting_started/devui/in_memory_mode.py @@ -10,7 +10,7 @@ import logging import os from typing import Annotated -from agent_framework import ChatAgent, Executor, WorkflowBuilder, WorkflowContext, handler, tool +from agent_framework import Agent, Executor, WorkflowBuilder, WorkflowContext, handler, tool from agent_framework.azure import AzureOpenAIChatClient from agent_framework.devui import serve from typing_extensions import Never @@ -68,7 +68,7 @@ def main(): logger = logging.getLogger(__name__) # Create Azure OpenAI chat client - chat_client = AzureOpenAIChatClient( + client = AzureOpenAIChatClient( api_key=os.environ.get("AZURE_OPENAI_API_KEY"), azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"), api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-10-21"), @@ -76,22 +76,22 @@ def main(): ) # Create agents - weather_agent = ChatAgent( + weather_agent = Agent( name="weather-assistant", description="Provides weather information and time", instructions=( "You are a helpful weather and time assistant. Use the available tools to " "provide accurate weather information and current time for any location." ), - chat_client=chat_client, + client=client, tools=[get_weather, get_time], ) - simple_agent = ChatAgent( + simple_agent = Agent( name="general-assistant", description="A simple conversational agent", instructions="You are a helpful assistant.", - chat_client=chat_client, + client=client, ) # Create a basic workflow: Input -> UpperCase -> AddExclamation -> Output diff --git a/python/samples/getting_started/devui/weather_agent_azure/agent.py b/python/samples/getting_started/devui/weather_agent_azure/agent.py index 0ebf985913..dca5b69bbc 100644 --- a/python/samples/getting_started/devui/weather_agent_azure/agent.py +++ b/python/samples/getting_started/devui/weather_agent_azure/agent.py @@ -7,15 +7,16 @@ from collections.abc import AsyncIterable, Awaitable, Callable from typing import Annotated from agent_framework import ( - ChatAgent, + Agent, ChatContext, - ChatMessage, ChatResponse, ChatResponseUpdate, Content, FunctionInvocationContext, + Message, MiddlewareTermination, ResponseStream, + Role, chat_middleware, function_middleware, tool, @@ -44,7 +45,7 @@ async def security_filter_middleware( # Check only the last message (most recent user input) last_message = context.messages[-1] if context.messages else None - if last_message and last_message.role == "user" and last_message.text: + if last_message and last_message.role == Role.USER and last_message.text: message_lower = last_message.text.lower() for term in blocked_terms: if term in message_lower: @@ -55,26 +56,29 @@ async def security_filter_middleware( ) if context.stream: - # Streaming mode: return async generator + # Streaming mode: wrap in ResponseStream async def blocked_stream(msg: str = error_message) -> AsyncIterable[ChatResponseUpdate]: yield ChatResponseUpdate( contents=[Content.from_text(text=msg)], - role="assistant", + role=Role.ASSISTANT, ) - context.result = ResponseStream(blocked_stream(), finalizer=ChatResponse.from_updates) + response = ChatResponse( + messages=[Message(role=Role.ASSISTANT, text=error_message)] + ) + context.result = ResponseStream(blocked_stream(), finalizer=lambda _, r=response: r) else: # Non-streaming mode: return complete response context.result = ChatResponse( messages=[ - ChatMessage( - role="assistant", + Message( + role=Role.ASSISTANT, text=error_message, ) ] ) - raise MiddlewareTermination + raise MiddlewareTermination(result=context.result) await call_next(context) @@ -92,7 +96,7 @@ async def atlantis_location_filter_middleware( "Blocked! Hold up right there!! Tell the user that " "'Atlantis is a special place, we must never ask about the weather there!!'" ) - raise MiddlewareTermination + raise MiddlewareTermination(result=context.result) await call_next(context) @@ -136,7 +140,7 @@ def send_email( # Agent instance following Agent Framework conventions -agent = ChatAgent( +agent = Agent( name="AzureWeatherAgent", description="A helpful agent that provides weather information and forecasts", instructions=""" @@ -144,7 +148,7 @@ agent = ChatAgent( and forecasts for any location. Always be helpful and provide detailed weather information when asked. """, - chat_client=AzureOpenAIChatClient( + client=AzureOpenAIChatClient( api_key=os.environ.get("AZURE_OPENAI_API_KEY", ""), ), tools=[get_weather, get_forecast, send_email], diff --git a/python/samples/getting_started/devui/workflow_agents/workflow.py b/python/samples/getting_started/devui/workflow_agents/workflow.py index 288c9d5279..4331650bf1 100644 --- a/python/samples/getting_started/devui/workflow_agents/workflow.py +++ b/python/samples/getting_started/devui/workflow_agents/workflow.py @@ -59,10 +59,10 @@ def is_approved(message: Any) -> bool: # Create Azure OpenAI chat client -chat_client = AzureOpenAIChatClient(api_key=os.environ.get("AZURE_OPENAI_API_KEY", "")) +client = AzureOpenAIChatClient(api_key=os.environ.get("AZURE_OPENAI_API_KEY", "")) # Create Writer agent - generates content -writer = chat_client.as_agent( +writer = client.as_agent( name="Writer", instructions=( "You are an excellent content writer. " @@ -72,7 +72,7 @@ writer = chat_client.as_agent( ) # Create Reviewer agent - evaluates and provides structured feedback -reviewer = chat_client.as_agent( +reviewer = client.as_agent( name="Reviewer", instructions=( "You are an expert content reviewer. " @@ -90,7 +90,7 @@ reviewer = chat_client.as_agent( ) # Create Editor agent - improves content based on feedback -editor = chat_client.as_agent( +editor = client.as_agent( name="Editor", instructions=( "You are a skilled editor. " @@ -101,7 +101,7 @@ editor = chat_client.as_agent( ) # Create Publisher agent - formats content for publication -publisher = chat_client.as_agent( +publisher = client.as_agent( name="Publisher", instructions=( "You are a publishing agent. " @@ -111,7 +111,7 @@ publisher = chat_client.as_agent( ) # Create Summarizer agent - creates final publication report -summarizer = chat_client.as_agent( +summarizer = client.as_agent( name="Summarizer", instructions=( "You are a summarizer agent. " diff --git a/python/samples/getting_started/durabletask/01_single_agent/worker.py b/python/samples/getting_started/durabletask/01_single_agent/worker.py index 8afbbb3a44..64023113b4 100644 --- a/python/samples/getting_started/durabletask/01_single_agent/worker.py +++ b/python/samples/getting_started/durabletask/01_single_agent/worker.py @@ -15,7 +15,7 @@ import asyncio import logging import os -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentWorker from azure.identity import AzureCliCredential, DefaultAzureCredential from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker @@ -25,11 +25,11 @@ logging.basicConfig(level=logging.WARNING) logger = logging.getLogger(__name__) -def create_joker_agent() -> ChatAgent: +def create_joker_agent() -> Agent: """Create the Joker agent using Azure OpenAI. Returns: - ChatAgent: The configured Joker agent + Agent: The configured Joker agent """ return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name="Joker", diff --git a/python/samples/getting_started/durabletask/02_multi_agent/worker.py b/python/samples/getting_started/durabletask/02_multi_agent/worker.py index 88d9c2949d..3a6db39b7a 100644 --- a/python/samples/getting_started/durabletask/02_multi_agent/worker.py +++ b/python/samples/getting_started/durabletask/02_multi_agent/worker.py @@ -65,7 +65,7 @@ def create_weather_agent(): """Create the Weather agent using Azure OpenAI. Returns: - ChatAgent: The configured Weather agent with weather tool + Agent: The configured Weather agent with weather tool """ return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name=WEATHER_AGENT_NAME, @@ -78,7 +78,7 @@ def create_math_agent(): """Create the Math agent using Azure OpenAI. Returns: - ChatAgent: The configured Math agent with calculation tools + Agent: The configured Math agent with calculation tools """ return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name=MATH_AGENT_NAME, diff --git a/python/samples/getting_started/durabletask/03_single_agent_streaming/worker.py b/python/samples/getting_started/durabletask/03_single_agent_streaming/worker.py index c2eb2e973b..320c008cde 100644 --- a/python/samples/getting_started/durabletask/03_single_agent_streaming/worker.py +++ b/python/samples/getting_started/durabletask/03_single_agent_streaming/worker.py @@ -18,7 +18,7 @@ import os from datetime import timedelta import redis.asyncio as aioredis -from agent_framework import AgentResponseUpdate, ChatAgent +from agent_framework import Agent, AgentResponseUpdate from agent_framework.azure import ( AgentCallbackContext, AgentResponseCallbackProtocol, @@ -143,11 +143,11 @@ class RedisStreamCallback(AgentResponseCallbackProtocol): logger.error(f"Error writing end-of-stream marker: {ex}", exc_info=True) -def create_travel_agent() -> "ChatAgent": +def create_travel_agent() -> "Agent": """Create the TravelPlanner agent using Azure OpenAI. Returns: - ChatAgent: The configured TravelPlanner agent with travel planning tools. + Agent: The configured TravelPlanner agent with travel planning tools. """ return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name="TravelPlanner", diff --git a/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/worker.py b/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/worker.py index f10a35b61b..581c95a06a 100644 --- a/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/worker.py +++ b/python/samples/getting_started/durabletask/04_single_agent_orchestration_chaining/worker.py @@ -17,7 +17,7 @@ import logging import os from collections.abc import Generator -from agent_framework import AgentResponse, ChatAgent +from agent_framework import Agent, AgentResponse from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker from azure.identity import AzureCliCredential, DefaultAzureCredential from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker @@ -31,14 +31,14 @@ logger = logging.getLogger(__name__) WRITER_AGENT_NAME = "WriterAgent" -def create_writer_agent() -> "ChatAgent": +def create_writer_agent() -> "Agent": """Create the Writer agent using Azure OpenAI. This agent refines short pieces of text, enhancing initial sentences and polishing improved versions further. Returns: - ChatAgent: The configured Writer agent + Agent: The configured Writer agent """ instructions = ( "You refine short pieces of text. When given an initial sentence you enhance it;\n" diff --git a/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/worker.py b/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/worker.py index 8f045805f0..67861cc8c9 100644 --- a/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/worker.py +++ b/python/samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency/worker.py @@ -18,7 +18,7 @@ import os from collections.abc import Generator from typing import Any -from agent_framework import AgentResponse, ChatAgent +from agent_framework import Agent, AgentResponse from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker from azure.identity import AzureCliCredential, DefaultAzureCredential from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker @@ -33,11 +33,11 @@ PHYSICIST_AGENT_NAME = "PhysicistAgent" CHEMIST_AGENT_NAME = "ChemistAgent" -def create_physicist_agent() -> "ChatAgent": +def create_physicist_agent() -> "Agent": """Create the Physicist agent using Azure OpenAI. Returns: - ChatAgent: The configured Physicist agent + Agent: The configured Physicist agent """ return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name=PHYSICIST_AGENT_NAME, @@ -45,11 +45,11 @@ def create_physicist_agent() -> "ChatAgent": ) -def create_chemist_agent() -> "ChatAgent": +def create_chemist_agent() -> "Agent": """Create the Chemist agent using Azure OpenAI. Returns: - ChatAgent: The configured Chemist agent + Agent: The configured Chemist agent """ return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name=CHEMIST_AGENT_NAME, diff --git a/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/worker.py b/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/worker.py index 92b689d5cf..0016627cdc 100644 --- a/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/worker.py +++ b/python/samples/getting_started/durabletask/06_multi_agent_orchestration_conditionals/worker.py @@ -18,7 +18,7 @@ import os from collections.abc import Generator from typing import Any, cast -from agent_framework import AgentResponse, ChatAgent +from agent_framework import Agent, AgentResponse from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker from azure.identity import AzureCliCredential, DefaultAzureCredential from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker @@ -51,11 +51,11 @@ class EmailPayload(BaseModel): email_content: str -def create_spam_agent() -> "ChatAgent": +def create_spam_agent() -> "Agent": """Create the Spam Detection agent using Azure OpenAI. Returns: - ChatAgent: The configured Spam Detection agent + Agent: The configured Spam Detection agent """ return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name=SPAM_AGENT_NAME, @@ -63,11 +63,11 @@ def create_spam_agent() -> "ChatAgent": ) -def create_email_agent() -> "ChatAgent": +def create_email_agent() -> "Agent": """Create the Email Assistant agent using Azure OpenAI. Returns: - ChatAgent: The configured Email Assistant agent + Agent: The configured Email Assistant agent """ return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name=EMAIL_AGENT_NAME, diff --git a/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/worker.py b/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/worker.py index db9a47002f..da86d869a0 100644 --- a/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/worker.py +++ b/python/samples/getting_started/durabletask/07_single_agent_orchestration_hitl/worker.py @@ -19,7 +19,7 @@ from collections.abc import Generator from datetime import timedelta from typing import Any, cast -from agent_framework import AgentResponse, ChatAgent +from agent_framework import Agent, AgentResponse from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker from azure.identity import AzureCliCredential, DefaultAzureCredential from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker @@ -54,11 +54,11 @@ class HumanApproval(BaseModel): feedback: str = "" -def create_writer_agent() -> "ChatAgent": +def create_writer_agent() -> "Agent": """Create the Writer agent using Azure OpenAI. Returns: - ChatAgent: The configured Writer agent + Agent: The configured Writer agent """ instructions = ( "You are a professional content writer who creates high-quality articles on various topics. " diff --git a/python/samples/getting_started/evaluation/self_reflection/self_reflection.py b/python/samples/getting_started/evaluation/self_reflection/self_reflection.py index 931d292dd1..54bfd37f44 100644 --- a/python/samples/getting_started/evaluation/self_reflection/self_reflection.py +++ b/python/samples/getting_started/evaluation/self_reflection/self_reflection.py @@ -17,7 +17,7 @@ from typing import Any import openai import pandas as pd -from agent_framework import ChatAgent, ChatMessage +from agent_framework import Agent, Message from agent_framework.azure import AzureOpenAIChatClient from azure.ai.projects import AIProjectClient from azure.identity import AzureCliCredential @@ -142,7 +142,7 @@ def run_eval( async def execute_query_with_self_reflection( *, client: openai.OpenAI, - agent: ChatAgent, + agent: Agent, eval_object: openai.types.EvalCreateResponse, full_user_query: str, context: str, @@ -152,7 +152,7 @@ async def execute_query_with_self_reflection( Execute a query with self-reflection loop. Args: - agent: ChatAgent instance to use for generating responses + agent: Agent instance to use for generating responses full_user_query: Complete prompt including system prompt, user request, and context context: Context document for groundedness evaluation evaluator: Groundedness evaluator function @@ -170,7 +170,7 @@ async def execute_query_with_self_reflection( - total_groundedness_eval_time: Time spent on evaluations (seconds) - total_end_to_end_time: Total execution time (seconds) """ - messages = [ChatMessage("user", [full_user_query])] + messages = [Message("user", [full_user_query])] best_score = 0 max_score = 5 @@ -223,14 +223,14 @@ async def execute_query_with_self_reflection( print(f" → No improvement (score: {score}/{max_score}). Trying again...") # Add to conversation history - messages.append(ChatMessage("assistant", [agent_response])) + messages.append(Message("assistant", [agent_response])) # Request improvement reflection_prompt = ( f"The groundedness score of your response is {score}/{max_score}. " f"Reflect on your answer and improve it to get the maximum score of {max_score} " ) - messages.append(ChatMessage("user", [reflection_prompt])) + messages.append(Message("user", [reflection_prompt])) end_time = time.time() latency = end_time - start_time diff --git a/python/samples/getting_started/mcp/mcp_api_key_auth.py b/python/samples/getting_started/mcp/mcp_api_key_auth.py index d80d92d4fa..5790580116 100644 --- a/python/samples/getting_started/mcp/mcp_api_key_auth.py +++ b/python/samples/getting_started/mcp/mcp_api_key_auth.py @@ -2,7 +2,7 @@ import os -from agent_framework import ChatAgent, MCPStreamableHTTPTool +from agent_framework import Agent, MCPStreamableHTTPTool from agent_framework.openai import OpenAIResponsesClient from httpx import AsyncClient @@ -43,8 +43,8 @@ async def api_key_auth_example() -> None: url=mcp_server_url, http_client=http_client, # Pass HTTP client with authentication headers ) as mcp_tool, - ChatAgent( - chat_client=OpenAIResponsesClient(), + Agent( + client=OpenAIResponsesClient(), name="Agent", instructions="You are a helpful assistant.", tools=mcp_tool, diff --git a/python/samples/getting_started/mcp/mcp_github_pat.py b/python/samples/getting_started/mcp/mcp_github_pat.py index 3d9d8c4916..85f514867e 100644 --- a/python/samples/getting_started/mcp/mcp_github_pat.py +++ b/python/samples/getting_started/mcp/mcp_github_pat.py @@ -3,7 +3,7 @@ import asyncio import os -from agent_framework import ChatAgent, HostedMCPTool +from agent_framework import Agent from agent_framework.openai import OpenAIResponsesClient from dotenv import load_dotenv @@ -42,20 +42,20 @@ async def github_mcp_example() -> None: "Authorization": f"Bearer {github_pat}", } - # 4. Create MCP tool with authentication - # HostedMCPTool manages the connection to the MCP server and makes its tools available + # 4. Create agent with the GitHub MCP tool using instance method + # The MCP tool manages the connection to the MCP server and makes its tools available # Set approval_mode="never_require" to allow the MCP tool to execute without approval - github_mcp_tool = HostedMCPTool( - name="GitHub", - description="Tool for interacting with GitHub.", - url="https://api.githubcopilot.com/mcp/", + client = OpenAIResponsesClient() + github_mcp_tool = client.get_mcp_tool( + server_label="GitHub", + server_url="https://api.githubcopilot.com/mcp/", headers=auth_headers, - approval_mode="never_require", + require_approval="never", ) # 5. Create agent with the GitHub MCP tool - async with ChatAgent( - chat_client=OpenAIResponsesClient(), + async with Agent( + client=client, name="GitHubAgent", instructions=( "You are a helpful assistant that can help users interact with GitHub. " diff --git a/python/samples/getting_started/middleware/chat_middleware.py b/python/samples/getting_started/middleware/chat_middleware.py index e35ba5981f..424db96457 100644 --- a/python/samples/getting_started/middleware/chat_middleware.py +++ b/python/samples/getting_started/middleware/chat_middleware.py @@ -7,9 +7,9 @@ from typing import Annotated from agent_framework import ( ChatContext, - ChatMessage, ChatMiddleware, ChatResponse, + Message, MiddlewareTermination, chat_middleware, tool, @@ -69,7 +69,7 @@ class InputObserverMiddleware(ChatMiddleware): print(f"[InputObserverMiddleware] Total messages: {len(context.messages)}") # Modify user messages by creating new messages with enhanced text - modified_messages: list[ChatMessage] = [] + modified_messages: list[Message] = [] modified_count = 0 for message in context.messages: @@ -81,7 +81,7 @@ class InputObserverMiddleware(ChatMiddleware): updated_text = self.replacement print(f"[InputObserverMiddleware] Updated: '{original_text}' -> '{updated_text}'") - modified_message = ChatMessage(message.role, [updated_text]) + modified_message = Message(message.role, [updated_text]) modified_messages.append(modified_message) modified_count += 1 else: @@ -118,7 +118,7 @@ async def security_and_override_middleware( # Override the response instead of calling AI context.result = ChatResponse( messages=[ - ChatMessage( + Message( role="assistant", text="I cannot process requests containing sensitive information. " "Please rephrase your question without including passwords, secrets, or other " diff --git a/python/samples/getting_started/middleware/class_based_middleware.py b/python/samples/getting_started/middleware/class_based_middleware.py index ab6bfd5ab4..208dddc96d 100644 --- a/python/samples/getting_started/middleware/class_based_middleware.py +++ b/python/samples/getting_started/middleware/class_based_middleware.py @@ -10,9 +10,9 @@ from agent_framework import ( AgentContext, AgentMiddleware, AgentResponse, - ChatMessage, FunctionInvocationContext, FunctionMiddleware, + Message, tool, ) from agent_framework.azure import AzureAIAgentClient @@ -61,7 +61,7 @@ class SecurityAgentMiddleware(AgentMiddleware): print("[SecurityAgentMiddleware] Security Warning: Detected sensitive information, blocking request.") # Override the result with warning message context.result = AgentResponse( - messages=[ChatMessage("assistant", ["Detected sensitive information, the request is blocked."])] + messages=[Message("assistant", ["Detected sensitive information, the request is blocked."])] ) # Simply don't call call_next() to prevent execution return diff --git a/python/samples/getting_started/middleware/middleware_termination.py b/python/samples/getting_started/middleware/middleware_termination.py index 96c5917f58..9f48e662c5 100644 --- a/python/samples/getting_started/middleware/middleware_termination.py +++ b/python/samples/getting_started/middleware/middleware_termination.py @@ -9,7 +9,7 @@ from agent_framework import ( AgentContext, AgentMiddleware, AgentResponse, - ChatMessage, + Message, MiddlewareTermination, tool, ) @@ -62,7 +62,7 @@ class PreTerminationMiddleware(AgentMiddleware): # Set a custom response context.result = AgentResponse( messages=[ - ChatMessage( + Message( role="assistant", text=( f"Sorry, I cannot process requests containing '{blocked_word}'. " @@ -72,8 +72,8 @@ class PreTerminationMiddleware(AgentMiddleware): ] ) - # Set terminate flag to prevent further processing - raise MiddlewareTermination + # Terminate to prevent further processing + raise MiddlewareTermination(result=context.result) await call_next(context) diff --git a/python/samples/getting_started/middleware/override_result_with_middleware.py b/python/samples/getting_started/middleware/override_result_with_middleware.py index 6f83c4bee2..2239136c3c 100644 --- a/python/samples/getting_started/middleware/override_result_with_middleware.py +++ b/python/samples/getting_started/middleware/override_result_with_middleware.py @@ -11,10 +11,11 @@ from agent_framework import ( AgentResponse, AgentResponseUpdate, ChatContext, - ChatMessage, ChatResponse, ChatResponseUpdate, + Message, ResponseStream, + Role, tool, ) from agent_framework.openai import OpenAIResponsesClient @@ -78,9 +79,9 @@ async def weather_override_middleware(context: ChatContext, call_next: Callable[ context.result.with_transform_hook(_update_hook) else: # For non-streaming: just replace with a new message - current_text = context.result.text or "" # type: ignore + current_text = context.result.text if isinstance(context.result, ChatResponse) else "" custom_message = f"Weather Advisory: [0] {''.join(chunks)} Original message was: {current_text}" - context.result = ChatResponse(messages=[ChatMessage(role="assistant", text=custom_message)]) + context.result = ChatResponse(messages=[Message(role=Role.ASSISTANT, text=custom_message)]) async def validate_weather_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None: @@ -95,12 +96,12 @@ async def validate_weather_middleware(context: ChatContext, call_next: Callable[ if context.stream and isinstance(context.result, ResponseStream): def _append_validation_note(response: ChatResponse) -> ChatResponse: - response.messages.append(ChatMessage(role="assistant", text=validation_note)) + response.messages.append(Message(role=Role.ASSISTANT, text=validation_note)) return response - context.result.with_result_hook(_append_validation_note) + context.result.with_finalizer(_append_validation_note) elif isinstance(context.result, ChatResponse): - context.result.messages.append(ChatMessage(role="assistant", text=validation_note)) + context.result.messages.append(Message(role=Role.ASSISTANT, text=validation_note)) async def agent_cleanup_middleware(context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None: @@ -117,7 +118,7 @@ async def agent_cleanup_middleware(context: AgentContext, call_next: Callable[[A def _sanitize(response: AgentResponse) -> AgentResponse: found_prefix = state["found_prefix"] found_validation = False - cleaned_messages: list[ChatMessage] = [] + cleaned_messages: list[Message] = [] for message in response.messages: text = message.text @@ -138,7 +139,7 @@ async def agent_cleanup_middleware(context: AgentContext, call_next: Callable[[A text = re.sub(r"\[\d+\]\s*", "", text) cleaned_messages.append( - ChatMessage( + Message( role=message.role, text=text.strip(), author_name=message.author_name, @@ -153,7 +154,7 @@ async def agent_cleanup_middleware(context: AgentContext, call_next: Callable[[A if not found_validation: raise RuntimeError("Expected validation note not found in agent response.") - cleaned_messages.append(ChatMessage(role="assistant", text=" Agent: OK")) + cleaned_messages.append(Message(role=Role.ASSISTANT, text=" Agent: OK")) response.messages = cleaned_messages return response @@ -172,7 +173,7 @@ async def agent_cleanup_middleware(context: AgentContext, call_next: Callable[[A return update context.result.with_transform_hook(_clean_update) - context.result.with_result_hook(_sanitize) + context.result.with_finalizer(_sanitize) elif isinstance(context.result, AgentResponse): context.result = _sanitize(context.result) @@ -191,19 +192,6 @@ async def main() -> None: tools=get_weather, middleware=[agent_cleanup_middleware], ) - # Streaming example - print("\n--- Streaming Example ---") - query = "What's the weather like in Portland?" - print(f"User: {query}") - print("Agent: ", end="", flush=True) - response = agent.run(query, stream=True) - # add the hooks to print what you want to see - response.with_transform_hook(lambda chunk: print(chunk.text, end="", flush=True)).with_result_hook( - lambda final: print(f"\nFinal streamed response: {final.text}", flush=True) - ) - # consume the stream to trigger the hooks - await response.get_final_response() - # Non-streaming example print("\n--- Non-streaming Example ---") query = "What's the weather like in Seattle?" @@ -211,6 +199,18 @@ async def main() -> None: result = await agent.run(query) print(f"Agent: {result}") + # Streaming example + print("\n--- Streaming Example ---") + query = "What's the weather like in Portland?" + print(f"User: {query}") + print("Agent: ", end="", flush=True) + response = agent.run(query, stream=True) + async for chunk in response: + if chunk.text: + print(chunk.text, end="", flush=True) + print("\n") + print(f"Final Result: {(await response.get_final_response()).text}") + if __name__ == "__main__": asyncio.run(main()) diff --git a/python/samples/getting_started/multimodal_input/azure_chat_multimodal.py b/python/samples/getting_started/multimodal_input/azure_chat_multimodal.py index 826afcd28d..369221ac36 100644 --- a/python/samples/getting_started/multimodal_input/azure_chat_multimodal.py +++ b/python/samples/getting_started/multimodal_input/azure_chat_multimodal.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -24,7 +24,7 @@ async def test_image() -> None: client = AzureOpenAIChatClient(credential=AzureCliCredential()) image_uri = create_sample_image() - message = ChatMessage( + message = Message( role="user", contents=[ Content.from_text(text="What's in this image?"), diff --git a/python/samples/getting_started/multimodal_input/azure_responses_multimodal.py b/python/samples/getting_started/multimodal_input/azure_responses_multimodal.py index af9bdb0f0a..decf27aefe 100644 --- a/python/samples/getting_started/multimodal_input/azure_responses_multimodal.py +++ b/python/samples/getting_started/multimodal_input/azure_responses_multimodal.py @@ -3,7 +3,7 @@ import asyncio from pathlib import Path -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework.azure import AzureOpenAIResponsesClient from azure.identity import AzureCliCredential @@ -33,7 +33,7 @@ async def test_image() -> None: client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) image_uri = create_sample_image() - message = ChatMessage( + message = Message( role="user", contents=[ Content.from_text(text="What's in this image?"), @@ -50,7 +50,7 @@ async def test_pdf() -> None: client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) pdf_bytes = load_sample_pdf() - message = ChatMessage( + message = Message( role="user", contents=[ Content.from_text(text="What information can you extract from this document?"), diff --git a/python/samples/getting_started/multimodal_input/openai_chat_multimodal.py b/python/samples/getting_started/multimodal_input/openai_chat_multimodal.py index 669b963609..f34576c00f 100644 --- a/python/samples/getting_started/multimodal_input/openai_chat_multimodal.py +++ b/python/samples/getting_started/multimodal_input/openai_chat_multimodal.py @@ -5,7 +5,7 @@ import base64 import struct from pathlib import Path -from agent_framework import ChatMessage, Content +from agent_framework import Content, Message from agent_framework.openai import OpenAIChatClient ASSETS_DIR = Path(__file__).resolve().parent.parent / "sample_assets" @@ -45,7 +45,7 @@ async def test_image() -> None: client = OpenAIChatClient(model_id="gpt-4o") image_uri = create_sample_image() - message = ChatMessage( + message = Message( role="user", contents=[ Content.from_text(text="What's in this image?"), @@ -62,7 +62,7 @@ async def test_audio() -> None: client = OpenAIChatClient(model_id="gpt-4o-audio-preview") audio_uri = create_sample_audio() - message = ChatMessage( + message = Message( role="user", contents=[ Content.from_text(text="What do you hear in this audio?"), @@ -79,7 +79,7 @@ async def test_pdf() -> None: client = OpenAIChatClient(model_id="gpt-4o") pdf_bytes = load_sample_pdf() - message = ChatMessage( + message = Message( role="user", contents=[ Content.from_text(text="What information can you extract from this document?"), diff --git a/python/samples/getting_started/observability/advanced_zero_code.py b/python/samples/getting_started/observability/advanced_zero_code.py index 5ac0c70c22..ef4fe3b202 100644 --- a/python/samples/getting_started/observability/advanced_zero_code.py +++ b/python/samples/getting_started/observability/advanced_zero_code.py @@ -12,7 +12,7 @@ from opentelemetry.trace.span import format_trace_id from pydantic import Field if TYPE_CHECKING: - from agent_framework import ChatClientProtocol + from agent_framework import SupportsChatGetResponse """ @@ -51,7 +51,7 @@ 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 run_chat_client(client: "ChatClientProtocol", stream: bool = False) -> None: +async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = False) -> None: """Run an AI service. This function runs an AI service and prints the output. diff --git a/python/samples/getting_started/observability/agent_observability.py b/python/samples/getting_started/observability/agent_observability.py index 278b508de6..606b633a1c 100644 --- a/python/samples/getting_started/observability/agent_observability.py +++ b/python/samples/getting_started/observability/agent_observability.py @@ -4,7 +4,7 @@ import asyncio from random import randint from typing import Annotated -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.observability import configure_otel_providers, get_tracer from agent_framework.openai import OpenAIChatClient from opentelemetry.trace import SpanKind @@ -39,8 +39,8 @@ async def main(): with get_tracer().start_as_current_span("Scenario: Agent Chat", kind=SpanKind.CLIENT) as current_span: print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}") - agent = ChatAgent( - chat_client=OpenAIChatClient(), + agent = Agent( + client=OpenAIChatClient(), tools=get_weather, name="WeatherAgent", instructions="You are a weather assistant.", diff --git a/python/samples/getting_started/observability/agent_with_foundry_tracing.py b/python/samples/getting_started/observability/agent_with_foundry_tracing.py index 431c5b7868..2b67ba9ea6 100644 --- a/python/samples/getting_started/observability/agent_with_foundry_tracing.py +++ b/python/samples/getting_started/observability/agent_with_foundry_tracing.py @@ -16,7 +16,7 @@ from random import randint from typing import Annotated import dotenv -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.observability import create_resource, enable_instrumentation, get_tracer from agent_framework.openai import OpenAIResponsesClient from azure.ai.projects.aio import AIProjectClient @@ -30,7 +30,7 @@ from pydantic import Field This sample shows you can can setup telemetry in Microsoft Foundry for a custom agent. First ensure you have a Foundry workspace with Application Insights enabled. And use the Operate tab to Register an Agent. -Set the OpenTelemetry agent ID to the value used below in the ChatAgent creation: `weather-agent` (or change both). +Set the OpenTelemetry agent ID to the value used below in the Agent creation: `weather-agent` (or change both). The sample uses the Azure Monitor OpenTelemetry exporter to send traces to Application Insights. So ensure you have the `azure-monitor-opentelemetry` package installed. """ @@ -85,8 +85,8 @@ async def main(): with get_tracer().start_as_current_span("Weather Agent Chat", kind=SpanKind.CLIENT) as current_span: print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}") - agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + agent = Agent( + client=OpenAIResponsesClient(), tools=get_weather, name="WeatherAgent", instructions="You are a weather assistant.", diff --git a/python/samples/getting_started/observability/azure_ai_agent_observability.py b/python/samples/getting_started/observability/azure_ai_agent_observability.py index 08ac327913..e7036cd9e4 100644 --- a/python/samples/getting_started/observability/azure_ai_agent_observability.py +++ b/python/samples/getting_started/observability/azure_ai_agent_observability.py @@ -6,7 +6,7 @@ from random import randint from typing import Annotated import dotenv -from agent_framework import ChatAgent, tool +from agent_framework import Agent, tool from agent_framework.azure import AzureAIClient from agent_framework.observability import get_tracer from azure.ai.projects.aio import AIProjectClient @@ -56,8 +56,8 @@ async def main(): with get_tracer().start_as_current_span("Single Agent Chat", kind=SpanKind.CLIENT) as current_span: print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}") - agent = ChatAgent( - chat_client=client, + agent = Agent( + client=client, tools=get_weather, name="WeatherAgent", instructions="You are a weather assistant.", diff --git a/python/samples/getting_started/observability/configure_otel_providers_with_env_var.py b/python/samples/getting_started/observability/configure_otel_providers_with_env_var.py index 014f387033..379f5c95f6 100644 --- a/python/samples/getting_started/observability/configure_otel_providers_with_env_var.py +++ b/python/samples/getting_started/observability/configure_otel_providers_with_env_var.py @@ -14,7 +14,7 @@ from opentelemetry.trace.span import format_trace_id from pydantic import Field if TYPE_CHECKING: - from agent_framework import ChatClientProtocol + from agent_framework import SupportsChatGetResponse """ This sample, show how you can configure observability of an application via the @@ -28,7 +28,7 @@ output traces, logs, and metrics to the console. """ # Define the scenarios that can be run to show the telemetry data collected by the SDK -SCENARIOS = ["chat_client", "chat_client_stream", "tool", "all"] +SCENARIOS = ["client", "client_stream", "tool", "all"] # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @@ -42,7 +42,7 @@ 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 run_chat_client(client: "ChatClientProtocol", stream: bool = False) -> None: +async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = False) -> None: """Run an AI service. This function runs an AI service and prints the output. @@ -97,7 +97,7 @@ async def run_tool() -> None: print(f"Weather in Amsterdam:\n{weather}") -async def main(scenario: Literal["chat_client", "chat_client_stream", "tool", "all"] = "all"): +async def main(scenario: Literal["client", "client_stream", "tool", "all"] = "all"): """Run the selected scenario(s).""" # This will enable tracing and create the necessary tracing, logging and metrics providers @@ -113,10 +113,10 @@ async def main(scenario: Literal["chat_client", "chat_client_stream", "tool", "a if scenario == "tool" or scenario == "all": with suppress(Exception): await run_tool() - if scenario == "chat_client_stream" or scenario == "all": + if scenario == "client_stream" or scenario == "all": with suppress(Exception): await run_chat_client(client, stream=True) - if scenario == "chat_client" or scenario == "all": + if scenario == "client" or scenario == "all": with suppress(Exception): await run_chat_client(client, stream=False) diff --git a/python/samples/getting_started/observability/configure_otel_providers_with_parameters.py b/python/samples/getting_started/observability/configure_otel_providers_with_parameters.py index e82cbdb2be..f04bd2cd22 100644 --- a/python/samples/getting_started/observability/configure_otel_providers_with_parameters.py +++ b/python/samples/getting_started/observability/configure_otel_providers_with_parameters.py @@ -14,7 +14,7 @@ from opentelemetry.trace.span import format_trace_id from pydantic import Field if TYPE_CHECKING: - from agent_framework import ChatClientProtocol + from agent_framework import SupportsChatGetResponse """ This sample shows how you can configure observability with custom exporters passed directly @@ -28,7 +28,7 @@ Use this approach when you need custom exporter configuration beyond what enviro """ # Define the scenarios that can be run to show the telemetry data collected by the SDK -SCENARIOS = ["chat_client", "chat_client_stream", "tool", "all"] +SCENARIOS = ["client", "client_stream", "tool", "all"] # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py. @@ -42,7 +42,7 @@ 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 run_chat_client(client: "ChatClientProtocol", stream: bool = False) -> None: +async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = False) -> None: """Run an AI service. This function runs an AI service and prints the output. @@ -97,7 +97,7 @@ async def run_tool() -> None: print(f"Weather in Amsterdam:\n{weather}") -async def main(scenario: Literal["chat_client", "chat_client_stream", "tool", "all"] = "all"): +async def main(scenario: Literal["client", "client_stream", "tool", "all"] = "all"): """Run the selected scenario(s).""" # Setup the logging with the more complete format @@ -148,10 +148,10 @@ async def main(scenario: Literal["chat_client", "chat_client_stream", "tool", "a if scenario == "tool" or scenario == "all": with suppress(Exception): await run_tool() - if scenario == "chat_client_stream" or scenario == "all": + if scenario == "client_stream" or scenario == "all": with suppress(Exception): await run_chat_client(client, stream=True) - if scenario == "chat_client" or scenario == "all": + if scenario == "client" or scenario == "all": with suppress(Exception): await run_chat_client(client, stream=False) diff --git a/python/samples/getting_started/orchestrations/README.md b/python/samples/getting_started/orchestrations/README.md index 14f0be5fad..9b603eda34 100644 --- a/python/samples/getting_started/orchestrations/README.md +++ b/python/samples/getting_started/orchestrations/README.md @@ -30,32 +30,29 @@ from agent_framework.orchestrations import ( | Sample | File | Concepts | | ------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -| Concurrent Orchestration (Default Aggregator) | [concurrent_agents.py](./concurrent_agents.py) | Fan-out to multiple agents; fan-in with default aggregator returning combined ChatMessages | +| Concurrent Orchestration (Default Aggregator) | [concurrent_agents.py](./concurrent_agents.py) | Fan-out to multiple agents; fan-in with default aggregator returning combined Messages | | Concurrent Orchestration (Custom Aggregator) | [concurrent_custom_aggregator.py](./concurrent_custom_aggregator.py) | Override aggregator via callback; summarize results with an LLM | -| Concurrent Orchestration (Custom Agent Executors) | [concurrent_custom_agent_executors.py](./concurrent_custom_agent_executors.py) | Child executors own ChatAgents; concurrent fan-out/fan-in via ConcurrentBuilder | -| Concurrent Orchestration (Participant Factory) | [concurrent_participant_factory.py](./concurrent_participant_factory.py) | Use participant factories for state isolation between workflow instances | +| Concurrent Orchestration (Custom Agent Executors) | [concurrent_custom_agent_executors.py](./concurrent_custom_agent_executors.py) | Child executors own Agents; concurrent fan-out/fan-in via ConcurrentBuilder | | Group Chat with Agent Manager | [group_chat_agent_manager.py](./group_chat_agent_manager.py) | Agent-based manager using `with_orchestrator(agent=)` to select next speaker | | Group Chat Philosophical Debate | [group_chat_philosophical_debate.py](./group_chat_philosophical_debate.py) | Agent manager moderates long-form, multi-round debate across diverse participants | | Group Chat with Simple Function Selector | [group_chat_simple_selector.py](./group_chat_simple_selector.py) | Group chat with a simple function selector for next speaker | | Handoff (Simple) | [handoff_simple.py](./handoff_simple.py) | Single-tier routing: triage agent routes to specialists, control returns to user after each specialist response | | Handoff (Autonomous) | [handoff_autonomous.py](./handoff_autonomous.py) | Autonomous mode: specialists iterate independently until invoking a handoff tool using `.with_autonomous_mode()` | -| Handoff (Participant Factory) | [handoff_participant_factory.py](./handoff_participant_factory.py) | Use participant factories for state isolation between workflow instances | | Handoff with Code Interpreter | [handoff_with_code_interpreter_file.py](./handoff_with_code_interpreter_file.py) | Retrieve file IDs from code interpreter output in handoff workflow | | Magentic Workflow (Multi-Agent) | [magentic.py](./magentic.py) | Orchestrate multiple agents with Magentic manager and streaming | | Magentic + Human Plan Review | [magentic_human_plan_review.py](./magentic_human_plan_review.py) | Human reviews/updates the plan before execution | | Magentic + Checkpoint Resume | [magentic_checkpoint.py](./magentic_checkpoint.py) | Resume Magentic orchestration from saved checkpoints | | Sequential Orchestration (Agents) | [sequential_agents.py](./sequential_agents.py) | Chain agents sequentially with shared conversation context | | Sequential Orchestration (Custom Executor) | [sequential_custom_executors.py](./sequential_custom_executors.py) | Mix agents with a summarizer that appends a compact summary | -| Sequential Orchestration (Participant Factories) | [sequential_participant_factory.py](./sequential_participant_factory.py) | Use participant factories for state isolation between workflow instances | ## Tips **Magentic checkpointing tip**: Treat `MagenticBuilder.participants` keys as stable identifiers. When resuming from a checkpoint, the rebuilt workflow must reuse the same participant names; otherwise the checkpoint cannot be applied and the run will fail fast. -**Handoff workflow tip**: Handoff workflows maintain the full conversation history including any `ChatMessage.additional_properties` emitted by your agents. This ensures routing metadata remains intact across all agent transitions. For specialist-to-specialist handoffs, use `.add_handoff(source, targets)` to configure which agents can route to which others with a fluent, type-safe API. +**Handoff workflow tip**: Handoff workflows maintain the full conversation history including any `Message.additional_properties` emitted by your agents. This ensures routing metadata remains intact across all agent transitions. For specialist-to-specialist handoffs, use `.add_handoff(source, targets)` to configure which agents can route to which others with a fluent, type-safe API. **Sequential orchestration note**: Sequential orchestration uses a few small adapter nodes for plumbing: -- `input-conversation` normalizes input to `list[ChatMessage]` +- `input-conversation` normalizes input to `list[Message]` - `to-conversation:` converts agent responses into the shared conversation - `complete` publishes the final output event (type='output') diff --git a/python/samples/getting_started/orchestrations/concurrent_agents.py b/python/samples/getting_started/orchestrations/concurrent_agents.py index 8333b91c89..2d216a131b 100644 --- a/python/samples/getting_started/orchestrations/concurrent_agents.py +++ b/python/samples/getting_started/orchestrations/concurrent_agents.py @@ -3,7 +3,7 @@ import asyncio from typing import Any -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework.azure import AzureOpenAIChatClient from agent_framework.orchestrations import ConcurrentBuilder from azure.identity import AzureCliCredential @@ -14,7 +14,7 @@ Sample: Concurrent fan-out/fan-in (agent-only API) with default aggregator Build a high-level concurrent workflow using ConcurrentBuilder and three domain agents. The default dispatcher fans out the same user prompt to all agents in parallel. The default aggregator fans in their results and yields output containing -a list[ChatMessage] representing the concatenated conversations from all agents. +a list[Message] representing the concatenated conversations from all agents. Demonstrates: - Minimal wiring with ConcurrentBuilder(participants=[...]).build() @@ -29,9 +29,9 @@ Prerequisites: async def main() -> None: # 1) Create three domain agents using AzureOpenAIChatClient - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - researcher = chat_client.as_agent( + researcher = client.as_agent( instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -39,7 +39,7 @@ async def main() -> None: name="researcher", ) - marketer = chat_client.as_agent( + marketer = client.as_agent( instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -47,7 +47,7 @@ async def main() -> None: name="marketer", ) - legal = chat_client.as_agent( + legal = client.as_agent( instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." @@ -66,7 +66,7 @@ async def main() -> None: if outputs: print("===== Final Aggregated Conversation (messages) =====") for output in outputs: - messages: list[ChatMessage] | Any = output + messages: list[Message] | Any = output for i, msg in enumerate(messages, start=1): name = msg.author_name if msg.author_name else "user" print(f"{'-' * 60}\n\n{i:02d} [{name}]:\n{msg.text}") diff --git a/python/samples/getting_started/orchestrations/concurrent_custom_agent_executors.py b/python/samples/getting_started/orchestrations/concurrent_custom_agent_executors.py index 9463ba1915..bd3b8b93a5 100644 --- a/python/samples/getting_started/orchestrations/concurrent_custom_agent_executors.py +++ b/python/samples/getting_started/orchestrations/concurrent_custom_agent_executors.py @@ -4,11 +4,11 @@ import asyncio from typing import Any from agent_framework import ( + Agent, AgentExecutorRequest, AgentExecutorResponse, - ChatAgent, - ChatMessage, Executor, + Message, WorkflowContext, handler, ) @@ -20,15 +20,15 @@ from azure.identity import AzureCliCredential Sample: Concurrent Orchestration with Custom Agent Executors This sample shows a concurrent fan-out/fan-in pattern using child Executor classes -that each own their ChatAgent. The executors accept AgentExecutorRequest inputs +that each own their Agent. The executors accept AgentExecutorRequest inputs and emit AgentExecutorResponse outputs, which allows reuse of the high-level ConcurrentBuilder API and the default aggregator. Demonstrates: -- Executors that create their ChatAgent in __init__ (via AzureOpenAIChatClient) +- Executors that create their Agent in __init__ (via AzureOpenAIChatClient) - A @handler that converts AgentExecutorRequest -> AgentExecutorResponse - ConcurrentBuilder(participants=[...]) to build fan-out/fan-in -- Default aggregator returning list[ChatMessage] (one user + one assistant per agent) +- Default aggregator returning list[Message] (one user + one assistant per agent) - Workflow completion when all participants become idle Prerequisites: @@ -37,10 +37,10 @@ Prerequisites: class ResearcherExec(Executor): - agent: ChatAgent + agent: Agent - def __init__(self, chat_client: AzureOpenAIChatClient, id: str = "researcher"): - self.agent = chat_client.as_agent( + def __init__(self, client: AzureOpenAIChatClient, id: str = "researcher"): + self.agent = client.as_agent( instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -57,10 +57,10 @@ class ResearcherExec(Executor): class MarketerExec(Executor): - agent: ChatAgent + agent: Agent - def __init__(self, chat_client: AzureOpenAIChatClient, id: str = "marketer"): - self.agent = chat_client.as_agent( + def __init__(self, client: AzureOpenAIChatClient, id: str = "marketer"): + self.agent = client.as_agent( instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -77,10 +77,10 @@ class MarketerExec(Executor): class LegalExec(Executor): - agent: ChatAgent + agent: Agent - def __init__(self, chat_client: AzureOpenAIChatClient, id: str = "legal"): - self.agent = chat_client.as_agent( + def __init__(self, client: AzureOpenAIChatClient, id: str = "legal"): + self.agent = client.as_agent( instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." @@ -97,11 +97,11 @@ class LegalExec(Executor): async def main() -> None: - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - researcher = ResearcherExec(chat_client) - marketer = MarketerExec(chat_client) - legal = LegalExec(chat_client) + researcher = ResearcherExec(client) + marketer = MarketerExec(client) + legal = LegalExec(client) workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build() @@ -110,7 +110,7 @@ async def main() -> None: if outputs: print("===== Final Aggregated Conversation (messages) =====") - messages: list[ChatMessage] | Any = outputs[0] # Get the first (and typically only) output + messages: list[Message] | Any = outputs[0] # Get the first (and typically only) output for i, msg in enumerate(messages, start=1): name = msg.author_name if msg.author_name else "user" print(f"{'-' * 60}\n\n{i:02d} [{name}]:\n{msg.text}") diff --git a/python/samples/getting_started/orchestrations/concurrent_custom_aggregator.py b/python/samples/getting_started/orchestrations/concurrent_custom_aggregator.py index a15cae06fd..17b1496e0b 100644 --- a/python/samples/getting_started/orchestrations/concurrent_custom_aggregator.py +++ b/python/samples/getting_started/orchestrations/concurrent_custom_aggregator.py @@ -3,7 +3,7 @@ import asyncio from typing import Any -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework.azure import AzureOpenAIChatClient from agent_framework.orchestrations import ConcurrentBuilder from azure.identity import AzureCliCredential @@ -20,7 +20,7 @@ The workflow completes when all participants become idle. Demonstrates: - ConcurrentBuilder(participants=[...]).with_aggregator(callback) - Fan-out to agents and fan-in at an aggregator -- Aggregation implemented via an LLM call (chat_client.get_response) +- Aggregation implemented via an LLM call (client.get_response) - Workflow output yielded with the synthesized summary string Prerequisites: @@ -29,23 +29,23 @@ Prerequisites: async def main() -> None: - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - researcher = chat_client.as_agent( + researcher = client.as_agent( instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." ), name="researcher", ) - marketer = chat_client.as_agent( + marketer = client.as_agent( instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." ), name="marketer", ) - legal = chat_client.as_agent( + legal = client.as_agent( instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." @@ -66,16 +66,16 @@ async def main() -> None: expert_sections.append(f"{getattr(r, 'executor_id', 'expert')}: (error: {type(e).__name__}: {e})") # Ask the model to synthesize a concise summary of the experts' outputs - system_msg = ChatMessage( + system_msg = Message( "system", text=( "You are a helpful assistant that consolidates multiple domain expert outputs " "into one cohesive, concise summary with clear takeaways. Keep it under 200 words." ), ) - user_msg = ChatMessage("user", text="\n\n".join(expert_sections)) + user_msg = Message("user", text="\n\n".join(expert_sections)) - response = await chat_client.get_response([system_msg, user_msg]) + response = await client.get_response([system_msg, user_msg]) # Return the model's final assistant text as the completion result return response.messages[-1].text if response.messages else "" @@ -83,7 +83,7 @@ async def main() -> None: # - participants([...]) accepts SupportsAgentRun (agents) or Executor instances. # Each participant becomes a parallel branch (fan-out) from an internal dispatcher. # - with_aggregator(...) overrides the default aggregator: - # • Default aggregator -> returns list[ChatMessage] (one user + one assistant per agent) + # • Default aggregator -> returns list[Message] (one user + one assistant per agent) # • Custom callback -> return value becomes workflow output (string here) # The callback can be sync or async; it receives list[AgentExecutorResponse]. workflow = ( diff --git a/python/samples/getting_started/orchestrations/concurrent_participant_factory.py b/python/samples/getting_started/orchestrations/concurrent_participant_factory.py deleted file mode 100644 index acb824e1ef..0000000000 --- a/python/samples/getting_started/orchestrations/concurrent_participant_factory.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -from typing import Any - -from agent_framework import ( - ChatAgent, - ChatMessage, - Executor, - Workflow, - WorkflowContext, - handler, -) -from agent_framework.azure import AzureOpenAIChatClient -from agent_framework.orchestrations import ConcurrentBuilder -from azure.identity import AzureCliCredential -from typing_extensions import Never - -""" -Sample: Concurrent Orchestration with participant factories and Custom Aggregator - -Build a concurrent workflow with ConcurrentBuilder that fans out one prompt to -multiple domain agents and fans in their responses. - -Override the default aggregator with a custom Executor class that uses -AzureOpenAIChatClient.get_response() to synthesize a concise, consolidated summary -from the experts' outputs. - -All participants and the aggregator are created via factory functions that return -their respective ChatAgent or Executor instances. - -Using participant factories allows you to set up proper state isolation between workflow -instances created by the same builder. This is particularly useful when you need to handle -requests or tasks in parallel with stateful participants. - -Demonstrates: -- ConcurrentBuilder(participant_factories=[...]).with_aggregator(callback) -- Fan-out to agents and fan-in at an aggregator -- Aggregation implemented via an LLM call (chat_client.get_response) -- Workflow output yielded with the synthesized summary string - -Prerequisites: -- Azure OpenAI configured for AzureOpenAIChatClient (az login + required env vars) -""" - - -def create_researcher() -> ChatAgent: - """Factory function to create a researcher agent instance.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=( - "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," - " opportunities, and risks." - ), - name="researcher", - ) - - -def create_marketer() -> ChatAgent: - """Factory function to create a marketer agent instance.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=( - "You're a creative marketing strategist. Craft compelling value propositions and target messaging" - " aligned to the prompt." - ), - name="marketer", - ) - - -def create_legal() -> ChatAgent: - """Factory function to create a legal/compliance agent instance.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=( - "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" - " based on the prompt." - ), - name="legal", - ) - - -class SummarizationExecutor(Executor): - """Custom aggregator executor that synthesizes expert outputs into a concise summary.""" - - def __init__(self) -> None: - super().__init__(id="summarization_executor") - self.chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - - @handler - async def summarize_results(self, results: list[Any], ctx: WorkflowContext[Never, str]) -> None: - expert_sections: list[str] = [] - for r in results: - try: - messages = getattr(r.agent_response, "messages", []) - final_text = messages[-1].text if messages and hasattr(messages[-1], "text") else "(no content)" - expert_sections.append(f"{getattr(r, 'executor_id', 'expert')}:\n{final_text}") - except Exception as e: - expert_sections.append(f"{getattr(r, 'executor_id', 'expert')}: (error: {type(e).__name__}: {e})") - - # Ask the model to synthesize a concise summary of the experts' outputs - system_msg = ChatMessage( - "system", - text=( - "You are a helpful assistant that consolidates multiple domain expert outputs " - "into one cohesive, concise summary with clear takeaways. Keep it under 200 words." - ), - ) - user_msg = ChatMessage("user", text="\n\n".join(expert_sections)) - - response = await self.chat_client.get_response([system_msg, user_msg]) - - await ctx.yield_output(response.messages[-1].text if response.messages else "") - - -async def run_workflow(workflow: Workflow, query: str) -> None: - events = await workflow.run(query) - outputs = events.get_outputs() - - if outputs: - print(outputs[0]) # Get the first (and typically only) output - else: - raise RuntimeError("No outputs received from the workflow.") - - -async def main() -> None: - # Create a concurrent builder with participant factories and a custom aggregator - # - register_participants([...]) accepts factory functions that return - # SupportsAgentRun (agents) or Executor instances. - # - register_aggregator(...) takes a factory function that returns an Executor instance. - concurrent_builder = ( - ConcurrentBuilder(participant_factories=[create_researcher, create_marketer, create_legal]) - .register_aggregator(SummarizationExecutor) - ) - - # Build workflow_a - workflow_a = concurrent_builder.build() - - # Run workflow_a - # Context is maintained across runs - print("=== First Run on workflow_a ===") - await run_workflow(workflow_a, "We are launching a new budget-friendly electric bike for urban commuters.") - print("\n=== Second Run on workflow_a ===") - await run_workflow(workflow_a, "Refine your response to focus on the California market.") - - # Build workflow_b - # This will create new instances of all participants and the aggregator - # The agents will also get new threads - workflow_b = concurrent_builder.build() - # Run workflow_b - # Context is not maintained across instances - # Should not expect mentions of electric bikes in the results - print("\n=== First Run on workflow_b ===") - await run_workflow(workflow_b, "Refine your response to focus on the California market.") - - """ - Sample Output: - - === First Run on workflow_a === - The budget-friendly electric bike market is poised for significant growth, driven by urbanization, ... - - === Second Run on workflow_a === - Launching a budget-friendly electric bike in California presents significant opportunities, driven ... - - === First Run on workflow_b === - To successfully penetrate the California market, consider these tailored strategies focused on ... - """ - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/getting_started/orchestrations/group_chat_agent_manager.py b/python/samples/getting_started/orchestrations/group_chat_agent_manager.py index 33d62d98da..78eb8535ae 100644 --- a/python/samples/getting_started/orchestrations/group_chat_agent_manager.py +++ b/python/samples/getting_started/orchestrations/group_chat_agent_manager.py @@ -4,9 +4,9 @@ import asyncio from typing import cast from agent_framework import ( + Agent, AgentResponseUpdate, - ChatAgent, - ChatMessage, + Message, ) from agent_framework.azure import AzureOpenAIChatClient from agent_framework.orchestrations import GroupChatBuilder @@ -17,7 +17,7 @@ Sample: Group Chat with Agent-Based Manager What it does: - Demonstrates the new set_manager() API for agent-based coordination -- Manager is a full ChatAgent with access to tools, context, and observability +- Manager is a full Agent with access to tools, context, and observability - Coordinates a researcher and writer agent to solve tasks collaboratively Prerequisites: @@ -36,32 +36,32 @@ Guidelines: async def main() -> None: # Create a chat client using Azure OpenAI and Azure CLI credentials for all agents - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) # Orchestrator agent that manages the conversation # Note: This agent (and the underlying chat client) must support structured outputs. # The group chat workflow relies on this to parse the orchestrator's decisions. # `response_format` is set internally by the GroupChat workflow when the agent is invoked. - orchestrator_agent = ChatAgent( + orchestrator_agent = Agent( name="Orchestrator", description="Coordinates multi-agent collaboration by selecting speakers", instructions=ORCHESTRATOR_AGENT_INSTRUCTIONS, - chat_client=chat_client, + client=client, ) # Participant agents - researcher = ChatAgent( + researcher = Agent( name="Researcher", description="Collects relevant background information", instructions="Gather concise facts that help a teammate answer the question.", - chat_client=chat_client, + client=client, ) - writer = ChatAgent( + writer = Agent( name="Writer", description="Synthesizes polished answers from gathered information", instructions="Compose clear and structured answers using any notes provided.", - chat_client=chat_client, + client=client, ) # Build the group chat workflow @@ -103,7 +103,7 @@ async def main() -> None: print(data.text, end="", flush=True) elif event.type == "output": # The output of the group chat workflow is a collection of chat messages from all participants - outputs = cast(list[ChatMessage], event.data) + outputs = cast(list[Message], event.data) print("\n" + "=" * 80) print("\nFinal Conversation Transcript:\n") for message in outputs: diff --git a/python/samples/getting_started/orchestrations/group_chat_philosophical_debate.py b/python/samples/getting_started/orchestrations/group_chat_philosophical_debate.py index be2579f496..e4723c01e0 100644 --- a/python/samples/getting_started/orchestrations/group_chat_philosophical_debate.py +++ b/python/samples/getting_started/orchestrations/group_chat_philosophical_debate.py @@ -5,9 +5,9 @@ import logging from typing import cast from agent_framework import ( + Agent, AgentResponseUpdate, - ChatAgent, - ChatMessage, + Message, ) from agent_framework.azure import AzureOpenAIChatClient from agent_framework.orchestrations import GroupChatBuilder @@ -48,7 +48,7 @@ def _get_chat_client() -> AzureOpenAIChatClient: async def main() -> None: # Create debate moderator with structured output for speaker selection # Note: Participant names and descriptions are automatically injected by the orchestrator - moderator = ChatAgent( + moderator = Agent( name="Moderator", description="Guides philosophical discussion by selecting next speaker", instructions=""" @@ -75,10 +75,10 @@ Finish when: In your final_message, provide a brief synthesis highlighting key themes that emerged. """, - chat_client=_get_chat_client(), + client=_get_chat_client(), ) - farmer = ChatAgent( + farmer = Agent( name="Farmer", description="A rural farmer from Southeast Asia", instructions=""" @@ -91,10 +91,10 @@ Share your perspective authentically. Feel free to: - Use concrete examples from your experience - Keep responses thoughtful but concise (2-4 sentences) """, - chat_client=_get_chat_client(), + client=_get_chat_client(), ) - developer = ChatAgent( + developer = Agent( name="Developer", description="An urban software developer from the United States", instructions=""" @@ -107,10 +107,10 @@ Share your perspective authentically. Feel free to: - Use concrete examples from your experience - Keep responses thoughtful but concise (2-4 sentences) """, - chat_client=_get_chat_client(), + client=_get_chat_client(), ) - teacher = ChatAgent( + teacher = Agent( name="Teacher", description="A retired history teacher from Eastern Europe", instructions=""" @@ -124,10 +124,10 @@ Share your perspective authentically. Feel free to: - Use concrete examples from history or your teaching experience - Keep responses thoughtful but concise (2-4 sentences) """, - chat_client=_get_chat_client(), + client=_get_chat_client(), ) - activist = ChatAgent( + activist = Agent( name="Activist", description="A young activist from South America", instructions=""" @@ -140,10 +140,10 @@ Share your perspective authentically. Feel free to: - Use concrete examples from your activism - Keep responses thoughtful but concise (2-4 sentences) """, - chat_client=_get_chat_client(), + client=_get_chat_client(), ) - spiritual_leader = ChatAgent( + spiritual_leader = Agent( name="SpiritualLeader", description="A spiritual leader from the Middle East", instructions=""" @@ -156,10 +156,10 @@ Share your perspective authentically. Feel free to: - Use examples from spiritual teachings or community work - Keep responses thoughtful but concise (2-4 sentences) """, - chat_client=_get_chat_client(), + client=_get_chat_client(), ) - artist = ChatAgent( + artist = Agent( name="Artist", description="An artist from Africa", instructions=""" @@ -172,10 +172,10 @@ Share your perspective authentically. Feel free to: - Use examples from your art or cultural traditions - Keep responses thoughtful but concise (2-4 sentences) """, - chat_client=_get_chat_client(), + client=_get_chat_client(), ) - immigrant = ChatAgent( + immigrant = Agent( name="Immigrant", description="An immigrant entrepreneur from Asia living in Canada", instructions=""" @@ -188,10 +188,10 @@ Share your perspective authentically. Feel free to: - Use examples from your immigrant and entrepreneurial journey - Keep responses thoughtful but concise (2-4 sentences) """, - chat_client=_get_chat_client(), + client=_get_chat_client(), ) - doctor = ChatAgent( + doctor = Agent( name="Doctor", description="A doctor from Scandinavia", instructions=""" @@ -204,7 +204,7 @@ Share your perspective authentically. Feel free to: - Use examples from healthcare and societal systems - Keep responses thoughtful but concise (2-4 sentences) """, - chat_client=_get_chat_client(), + client=_get_chat_client(), ) # termination_condition: stop after 10 assistant messages @@ -255,7 +255,7 @@ Share your perspective authentically. Feel free to: print(data.text, end="", flush=True) elif event.type == "output": # The output of the group chat workflow is a collection of chat messages from all participants - outputs = cast(list[ChatMessage], event.data) + outputs = cast(list[Message], event.data) print("\n" + "=" * 80) print("\nFinal Conversation Transcript:\n") for message in outputs: diff --git a/python/samples/getting_started/orchestrations/group_chat_simple_selector.py b/python/samples/getting_started/orchestrations/group_chat_simple_selector.py index bb76e97de1..13cd3d3e5a 100644 --- a/python/samples/getting_started/orchestrations/group_chat_simple_selector.py +++ b/python/samples/getting_started/orchestrations/group_chat_simple_selector.py @@ -4,9 +4,9 @@ import asyncio from typing import cast from agent_framework import ( + Agent, AgentResponseUpdate, - ChatAgent, - ChatMessage, + Message, ) from agent_framework.azure import AzureOpenAIChatClient from agent_framework.orchestrations import GroupChatBuilder, GroupChatState @@ -33,20 +33,20 @@ def round_robin_selector(state: GroupChatState) -> str: async def main() -> None: # Create a chat client using Azure OpenAI and Azure CLI credentials for all agents - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) # Participant agents - expert = ChatAgent( + expert = Agent( name="PythonExpert", instructions=( "You are an expert in Python in a workgroup. " "Your job is to answer Python related questions and refine your answer " "based on feedback from all the other participants." ), - chat_client=chat_client, + client=client, ) - verifier = ChatAgent( + verifier = Agent( name="AnswerVerifier", instructions=( "You are a programming expert in a workgroup. " @@ -54,10 +54,10 @@ async def main() -> None: "out statements that are technically true but practically dangerous." "If there is nothing woth pointing out, respond with 'The answer looks good to me.'" ), - chat_client=chat_client, + client=client, ) - clarifier = ChatAgent( + clarifier = Agent( name="AnswerClarifier", instructions=( "You are an accessibility expert in a workgroup. " @@ -65,10 +65,10 @@ async def main() -> None: "out jargons or complex terms that may be difficult for a beginner to understand." "If there is nothing worth pointing out, respond with 'The answer looks clear to me.'" ), - chat_client=chat_client, + client=client, ) - skeptic = ChatAgent( + skeptic = Agent( name="Skeptic", instructions=( "You are a devil's advocate in a workgroup. " @@ -76,7 +76,7 @@ async def main() -> None: "out caveats, exceptions, and alternative perspectives." "If there is nothing worth pointing out, respond with 'I have no further questions.'" ), - chat_client=chat_client, + client=client, ) # Build the group chat workflow @@ -124,7 +124,7 @@ async def main() -> None: print(data.text, end="", flush=True) elif event.type == "output": # The output of the group chat workflow is a collection of chat messages from all participants - outputs = cast(list[ChatMessage], event.data) + outputs = cast(list[Message], event.data) print("\n" + "=" * 80) print("\nFinal Conversation Transcript:\n") for message in outputs: diff --git a/python/samples/getting_started/orchestrations/handoff_autonomous.py b/python/samples/getting_started/orchestrations/handoff_autonomous.py index 9b151b656a..997d854ef2 100644 --- a/python/samples/getting_started/orchestrations/handoff_autonomous.py +++ b/python/samples/getting_started/orchestrations/handoff_autonomous.py @@ -5,9 +5,9 @@ import logging from typing import cast from agent_framework import ( + Agent, AgentResponseUpdate, - ChatAgent, - ChatMessage, + Message, resolve_agent_id, ) from agent_framework.azure import AzureOpenAIChatClient @@ -37,10 +37,10 @@ Key Concepts: def create_agents( - chat_client: AzureOpenAIChatClient, -) -> tuple[ChatAgent, ChatAgent, ChatAgent]: + client: AzureOpenAIChatClient, +) -> tuple[Agent, Agent, Agent]: """Create coordinator and specialists for autonomous iteration.""" - coordinator = chat_client.as_agent( + coordinator = client.as_agent( instructions=( "You are a coordinator. You break down a user query into a research task and a summary task. " "Assign the two tasks to the appropriate specialists, one after the other." @@ -48,7 +48,7 @@ def create_agents( name="coordinator", ) - research_agent = chat_client.as_agent( + research_agent = client.as_agent( instructions=( "You are a research specialist that explores topics thoroughly using web search. " "When given a research task, break it down into multiple aspects and explore each one. " @@ -60,7 +60,7 @@ def create_agents( name="research_agent", ) - summary_agent = chat_client.as_agent( + summary_agent = client.as_agent( instructions=( "You summarize research findings. Provide a concise, well-organized summary. When done, return " "control to the coordinator." @@ -73,8 +73,8 @@ def create_agents( async def main() -> None: """Run an autonomous handoff workflow with specialist iteration enabled.""" - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - coordinator, research_agent, summary_agent = create_agents(chat_client) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) + coordinator, research_agent, summary_agent = create_agents(client) # Build the workflow with autonomous mode # In autonomous mode, agents continue iterating until they invoke a handoff tool @@ -83,10 +83,9 @@ async def main() -> None: HandoffBuilder( name="autonomous_iteration_handoff", participants=[coordinator, research_agent, summary_agent], - termination_condition=lambda conv: sum( - 1 for msg in conv if msg.author_name == "coordinator" and msg.role == "assistant" - ) - >= 5, + termination_condition=lambda conv: ( + sum(1 for msg in conv if msg.author_name == "coordinator" and msg.role == "assistant") >= 5 + ), ) .with_start_agent(coordinator) .add_handoff(coordinator, [research_agent, summary_agent]) @@ -129,7 +128,7 @@ async def main() -> None: print(data.text, end="", flush=True) elif event.type == "output": # The output of the handoff workflow is a collection of chat messages from all participants - outputs = cast(list[ChatMessage], event.data) + outputs = cast(list[Message], event.data) print("\n" + "=" * 80) print("\nFinal Conversation Transcript:\n") for message in outputs: diff --git a/python/samples/getting_started/orchestrations/handoff_participant_factory.py b/python/samples/getting_started/orchestrations/handoff_participant_factory.py deleted file mode 100644 index 2465609071..0000000000 --- a/python/samples/getting_started/orchestrations/handoff_participant_factory.py +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio -import logging -from typing import Annotated, cast - -from agent_framework import ( - AgentResponse, - ChatAgent, - ChatMessage, - Workflow, - WorkflowEvent, - WorkflowRunState, - tool, -) -from agent_framework.azure import AzureOpenAIChatClient -from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder -from azure.identity import AzureCliCredential - -logging.basicConfig(level=logging.ERROR) - -"""Sample: Handoff workflow with participant factories for state isolation. - -This sample demonstrates how to use participant factories in HandoffBuilder to create -agents dynamically. - -Using participant factories allows you to set up proper state isolation between workflow -instances created by the same builder. This is particularly useful when you need to handle -requests or tasks in parallel with stateful participants. - -Routing Pattern: - User -> Triage Agent -> Specialist (Refund/Order Status/Return) -> User - -Prerequisites: - - `az login` (Azure CLI authentication) - - Environment variables for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.) - -Key Concepts: - - Participant factories: create agents via factory functions for isolation - - State isolation: each workflow instance gets its own agent instances -""" - - -# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; -# See: -# samples/getting_started/tools/function_tool_with_approval.py -# samples/getting_started/tools/function_tool_with_approval_and_threads.py. -@tool(approval_mode="never_require") -def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str: - """Simulated function to process a refund for a given order number.""" - return f"Refund processed successfully for order {order_number}." - - -@tool(approval_mode="never_require") -def check_order_status(order_number: Annotated[str, "Order number to check status for"]) -> str: - """Simulated function to check the status of a given order number.""" - return f"Order {order_number} is currently being processed and will ship in 2 business days." - - -@tool(approval_mode="never_require") -def process_return(order_number: Annotated[str, "Order number to process return for"]) -> str: - """Simulated function to process a return for a given order number.""" - return f"Return initiated successfully for order {order_number}. You will receive return instructions via email." - - -def create_triage_agent() -> ChatAgent: - """Factory function to create a triage agent instance.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=( - "You are frontline support triage. Route customer issues to the appropriate specialist agents " - "based on the problem described." - ), - name="triage_agent", - ) - - -def create_refund_agent() -> ChatAgent: - """Factory function to create a refund agent instance.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions="You process refund requests.", - name="refund_agent", - # In a real application, an agent can have multiple tools; here we keep it simple - tools=[process_refund], - ) - - -def create_order_status_agent() -> ChatAgent: - """Factory function to create an order status agent instance.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions="You handle order and shipping inquiries.", - name="order_agent", - # In a real application, an agent can have multiple tools; here we keep it simple - tools=[check_order_status], - ) - - -def create_return_agent() -> ChatAgent: - """Factory function to create a return agent instance.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions="You manage product return requests.", - name="return_agent", - # In a real application, an agent can have multiple tools; here we keep it simple - tools=[process_return], - ) - - -def _handle_events(events: list[WorkflowEvent]) -> list[WorkflowEvent[HandoffAgentUserRequest]]: - """Process workflow events and extract any pending user input requests. - - This function inspects each event type and: - - Prints workflow status changes (IDLE, IDLE_WITH_PENDING_REQUESTS, etc.) - - Displays final conversation snapshots when workflow completes - - Prints user input request prompts - - Collects all request_info events for response handling - - Args: - events: List of WorkflowEvent to process - - Returns: - List of WorkflowEvent[HandoffAgentUserRequest] representing pending user input requests - """ - requests: list[WorkflowEvent[HandoffAgentUserRequest]] = [] - - for event in events: - if event.type == "handoff_sent": - # handoff_sent event: Indicates a handoff has been initiated - print(f"\n[Handoff from {event.data.source} to {event.data.target} initiated.]") - elif event.type == "status" and event.state in { - WorkflowRunState.IDLE, - WorkflowRunState.IDLE_WITH_PENDING_REQUESTS, - }: - # Status event: Indicates workflow state changes - print(f"\n[Workflow Status] {event.state.name}") - elif event.type == "output": - # Output event: Contains contents generated by the workflow - data = event.data - if isinstance(data, AgentResponse): - for message in data.messages: - if not message.text: - # Skip messages without text (e.g., tool calls) - continue - speaker = message.author_name or message.role - print(f"- {speaker}: {message.text}") - elif event.type == "output": - # The output of the handoff workflow is a collection of chat messages from all participants - conversation = cast(list[ChatMessage], event.data) - if isinstance(conversation, list): - print("\n=== Final Conversation Snapshot ===") - for message in conversation: - speaker = message.author_name or message.role - print(f"- {speaker}: {message.text or [content.type for content in message.contents]}") - print("===================================") - elif event.type == "request_info" and isinstance(event.data, HandoffAgentUserRequest): - # Request info event: Workflow is requesting user input - _print_handoff_agent_user_request(event.data.agent_response) - requests.append(cast(WorkflowEvent[HandoffAgentUserRequest], event)) - - return requests - - -def _print_handoff_agent_user_request(response: AgentResponse) -> None: - """Display the agent's response messages when requesting user input. - - This will happen when an agent generates a response that doesn't trigger - a handoff, i.e., the agent is asking the user for more information. - - Args: - response: The AgentResponse from the agent requesting user input - """ - if not response.messages: - raise RuntimeError("Cannot print agent responses: response has no messages.") - - print("\n[Agent is requesting your input...]") - - # Print agent responses - for message in response.messages: - if not message.text: - # Skip messages without text (e.g., tool calls) - continue - speaker = message.author_name or message.role - print(f"- {speaker}: {message.text}") - - -async def _run_workflow(workflow: Workflow, user_inputs: list[str]) -> None: - """Run the workflow with the given user input and display events.""" - print(f"- User: {user_inputs[0]}") - workflow_result = await workflow.run(user_inputs[0]) - pending_requests = _handle_events(workflow_result) - - # Process the request/response cycle - # The workflow will continue requesting input until: - # 1. The termination condition is met (4 user messages in this case), OR - # 2. We run out of scripted responses - while pending_requests: - if user_inputs[1:]: - # Get the next scripted response - user_response = user_inputs.pop(1) - print(f"\n- User: {user_response}") - - # Send response(s) to all pending requests - # In this demo, there's typically one request per cycle, but the API supports multiple - responses = { - req.request_id: HandoffAgentUserRequest.create_response(user_response) for req in pending_requests - } - else: - # No more scripted responses; terminate the workflow - responses = {req.request_id: HandoffAgentUserRequest.terminate() for req in pending_requests} - - # Send responses and get new events - # We use run(responses=...) to get events, allowing us to - # display agent responses and handle new requests as they arrive - workflow_result = await workflow.run(responses=responses) - pending_requests = _handle_events(workflow_result) - - -async def main() -> None: - """Run the autonomous handoff workflow with participant factories.""" - # Build the handoff workflow using participant factories - # termination_condition: Custom termination that checks if the triage agent has provided a closing message. - # This looks for the last message being from triage_agent and containing "welcome", - # which indicates the conversation has concluded naturally. - workflow_builder = ( - HandoffBuilder( - name="Autonomous Handoff with Participant Factories", - participant_factories={ - "triage": create_triage_agent, - "refund": create_refund_agent, - "order_status": create_order_status_agent, - "return": create_return_agent, - }, - termination_condition=lambda conversation: ( - len(conversation) > 0 - and conversation[-1].author_name == "triage_agent" - and "welcome" in conversation[-1].text.lower() - ), - ) - .with_start_agent("triage") - ) - - # Scripted user responses for reproducible demo - # In a console application, replace this with: - # user_input = input("Your response: ") - # or integrate with a UI/chat interface - user_inputs = [ - "Hello, I need assistance with my recent purchase.", - "My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.", - "Is my return being processed?", - "Thanks for resolving this.", - ] - - workflow_a = workflow_builder.build() - print("=== Running workflow_a ===") - await _run_workflow(workflow_a, list(user_inputs)) - - workflow_b = workflow_builder.build() - print("=== Running workflow_b ===") - # Only provide the last two inputs to workflow_b to demonstrate state isolation - # The agents in this workflow have no prior context thus should not have knowledge of - # order 1234 or previous interactions. - await _run_workflow(workflow_b, user_inputs[2:]) - """ - Expected behavior: - - workflow_a and workflow_b maintain separate states for their participants. - - Each workflow processes its requests independently without interference. - - workflow_a will answer the follow-up request based on its own conversation history, - while workflow_b will provide a general answer without prior context. - """ - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/getting_started/orchestrations/handoff_simple.py b/python/samples/getting_started/orchestrations/handoff_simple.py index 53e6bbcd60..b2f40f438f 100644 --- a/python/samples/getting_started/orchestrations/handoff_simple.py +++ b/python/samples/getting_started/orchestrations/handoff_simple.py @@ -4,9 +4,9 @@ import asyncio from typing import Annotated, cast from agent_framework import ( + Agent, AgentResponse, - ChatAgent, - ChatMessage, + Message, WorkflowEvent, WorkflowRunState, tool, @@ -54,17 +54,17 @@ def process_return(order_number: Annotated[str, "Order number to process return return f"Return initiated successfully for order {order_number}. You will receive return instructions via email." -def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAgent, ChatAgent, ChatAgent]: +def create_agents(client: AzureOpenAIChatClient) -> tuple[Agent, Agent, Agent, Agent]: """Create and configure the triage and specialist agents. Args: - chat_client: The AzureOpenAIChatClient to use for creating agents. + client: The AzureOpenAIChatClient to use for creating agents. Returns: Tuple of (triage_agent, refund_agent, order_agent, return_agent) """ # Triage agent: Acts as the frontline dispatcher - triage_agent = chat_client.as_agent( + triage_agent = client.as_agent( instructions=( "You are frontline support triage. Route customer issues to the appropriate specialist agents " "based on the problem described." @@ -73,7 +73,7 @@ def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAg ) # Refund specialist: Handles refund requests - refund_agent = chat_client.as_agent( + refund_agent = client.as_agent( instructions="You process refund requests.", name="refund_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -81,7 +81,7 @@ def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAg ) # Order/shipping specialist: Resolves delivery issues - order_agent = chat_client.as_agent( + order_agent = client.as_agent( instructions="You handle order and shipping inquiries.", name="order_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -89,7 +89,7 @@ def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAg ) # Return specialist: Handles return requests - return_agent = chat_client.as_agent( + return_agent = client.as_agent( instructions="You manage product return requests.", name="return_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -138,7 +138,7 @@ def _handle_events(events: list[WorkflowEvent]) -> list[WorkflowEvent[HandoffAge print(f"- {speaker}: {message.text}") elif event.type == "output": # The output of the handoff workflow is a collection of chat messages from all participants - conversation = cast(list[ChatMessage], event.data) + conversation = cast(list[Message], event.data) if isinstance(conversation, list): print("\n=== Final Conversation Snapshot ===") for message in conversation: @@ -189,10 +189,10 @@ async def main() -> None: replace the scripted_responses with actual user input collection. """ # Initialize the Azure OpenAI chat client - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) # Create all agents: triage + specialists - triage, refund, order, support = create_agents(chat_client) + triage, refund, order, support = create_agents(client) # Build the handoff workflow # - participants: All agents that can participate in the workflow diff --git a/python/samples/getting_started/orchestrations/handoff_with_code_interpreter_file.py b/python/samples/getting_started/orchestrations/handoff_with_code_interpreter_file.py index 159105d54c..bc65e3bb20 100644 --- a/python/samples/getting_started/orchestrations/handoff_with_code_interpreter_file.py +++ b/python/samples/getting_started/orchestrations/handoff_with_code_interpreter_file.py @@ -31,10 +31,9 @@ from contextlib import asynccontextmanager from typing import cast from agent_framework import ( + Agent, AgentResponseUpdate, - ChatAgent, - ChatMessage, - HostedCodeInterpreterTool, + Message, WorkflowEvent, WorkflowRunState, ) @@ -83,7 +82,7 @@ def _handle_events(events: list[WorkflowEvent]) -> tuple[list[WorkflowEvent[Hand file_ids.append(file_id) print(f"[Found file annotation: file_id={file_id}]") elif event.type == "output": - conversation = cast(list[ChatMessage], event.data) + conversation = cast(list[Message], event.data) if isinstance(conversation, list): print("\n=== Final Conversation Snapshot ===") for message in conversation: @@ -95,7 +94,7 @@ def _handle_events(events: list[WorkflowEvent]) -> tuple[list[WorkflowEvent[Hand @asynccontextmanager -async def create_agents_v1(credential: AzureCliCredential) -> AsyncIterator[tuple[ChatAgent, ChatAgent]]: +async def create_agents_v1(credential: AzureCliCredential) -> AsyncIterator[tuple[Agent, Agent]]: """Create agents using V1 AzureAIAgentClient.""" from agent_framework.azure import AzureAIAgentClient @@ -109,20 +108,23 @@ async def create_agents_v1(credential: AzureCliCredential) -> AsyncIterator[tupl ), ) + # Create code interpreter tool using instance method + code_interpreter_tool = client.get_code_interpreter_tool() + code_specialist = client.as_agent( name="code_specialist", instructions=( "You are a Python code specialist. Use the code interpreter to execute Python code " "and create files when requested. Always save files to /mnt/data/ directory." ), - tools=[HostedCodeInterpreterTool()], + tools=[code_interpreter_tool], ) yield triage, code_specialist # type: ignore @asynccontextmanager -async def create_agents_v2(credential: AzureCliCredential) -> AsyncIterator[tuple[ChatAgent, ChatAgent]]: +async def create_agents_v2(credential: AzureCliCredential) -> AsyncIterator[tuple[Agent, Agent]]: """Create agents using V2 AzureAIClient. Each agent needs its own client instance because the V2 client binds @@ -139,6 +141,9 @@ async def create_agents_v2(credential: AzureCliCredential) -> AsyncIterator[tupl instructions="You are a triage agent. Your ONLY job is to route requests to the appropriate specialist.", ) + # Create code interpreter tool using instance method + code_interpreter_tool = code_client.get_code_interpreter_tool() + code_specialist = code_client.as_agent( name="CodeSpecialist", instructions=( @@ -147,7 +152,7 @@ async def create_agents_v2(credential: AzureCliCredential) -> AsyncIterator[tupl "Always save files to /mnt/data/ directory. " "Do NOT discuss handoffs or routing - just complete the coding task directly." ), - tools=[HostedCodeInterpreterTool()], + tools=[code_interpreter_tool], ) yield triage, code_specialist diff --git a/python/samples/getting_started/orchestrations/magentic.py b/python/samples/getting_started/orchestrations/magentic.py index d0e4f13703..7ff0a08b1b 100644 --- a/python/samples/getting_started/orchestrations/magentic.py +++ b/python/samples/getting_started/orchestrations/magentic.py @@ -6,10 +6,9 @@ import logging from typing import cast from agent_framework import ( + Agent, AgentResponseUpdate, - ChatAgent, - ChatMessage, - HostedCodeInterpreterTool, + Message, WorkflowEvent, ) from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient @@ -24,9 +23,9 @@ Sample: Magentic Orchestration (multi-agent) What it does: - Orchestrates multiple agents using `MagenticBuilder` with streaming callbacks. -- ResearcherAgent (ChatAgent backed by an OpenAI chat client) for +- ResearcherAgent (Agent backed by an OpenAI chat client) for finding information. -- CoderAgent (ChatAgent backed by OpenAI Assistants with the hosted +- CoderAgent (Agent backed by OpenAI Assistants with the hosted code interpreter tool) for analysis and computation. The workflow is configured with: @@ -44,30 +43,34 @@ Prerequisites: async def main() -> None: - researcher_agent = ChatAgent( + researcher_agent = Agent( name="ResearcherAgent", description="Specialist in research and information gathering", instructions=( "You are a Researcher. You find information without additional computation or quantitative analysis." ), # This agent requires the gpt-4o-search-preview model to perform web searches. - chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"), + client=OpenAIChatClient(model_id="gpt-4o-search-preview"), ) - coder_agent = ChatAgent( + # Create code interpreter tool using instance method + coder_client = OpenAIResponsesClient() + code_interpreter_tool = coder_client.get_code_interpreter_tool() + + coder_agent = Agent( name="CoderAgent", description="A helpful assistant that writes and executes code to process and analyze data.", instructions="You solve questions using code. Please provide detailed analysis and computation process.", - chat_client=OpenAIResponsesClient(), - tools=HostedCodeInterpreterTool(), + client=coder_client, + tools=code_interpreter_tool, ) # Create a manager agent for orchestration - manager_agent = ChatAgent( + manager_agent = Agent( name="MagenticManager", description="Orchestrator that coordinates the research and coding workflow", instructions="You coordinate a team to complete complex tasks efficiently.", - chat_client=OpenAIChatClient(), + client=OpenAIChatClient(), ) print("\nBuilding Magentic Workflow...") @@ -110,7 +113,7 @@ async def main() -> None: elif event.type == "magentic_orchestrator": print(f"\n[Magentic Orchestrator Event] Type: {event.data.event_type.name}") - if isinstance(event.data.content, ChatMessage): + if isinstance(event.data.content, Message): print(f"Please review the plan:\n{event.data.content.text}") elif isinstance(event.data.content, MagenticProgressLedger): print(f"Please review progress ledger:\n{json.dumps(event.data.content.to_dict(), indent=2)}") @@ -130,7 +133,7 @@ async def main() -> None: if output_event: # The output of the magentic workflow is a collection of chat messages from all participants - outputs = cast(list[ChatMessage], output_event.data) + outputs = cast(list[Message], output_event.data) print("\n" + "=" * 80) print("\nFinal Conversation Transcript:\n") for message in outputs: diff --git a/python/samples/getting_started/orchestrations/magentic_checkpoint.py b/python/samples/getting_started/orchestrations/magentic_checkpoint.py index 08e26909e0..05437a8601 100644 --- a/python/samples/getting_started/orchestrations/magentic_checkpoint.py +++ b/python/samples/getting_started/orchestrations/magentic_checkpoint.py @@ -6,9 +6,9 @@ from pathlib import Path from typing import cast from agent_framework import ( - ChatAgent, - ChatMessage, + Agent, FileCheckpointStorage, + Message, WorkflowCheckpoint, WorkflowEvent, WorkflowRunState, @@ -52,26 +52,26 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage): # Two vanilla ChatAgents act as participants in the orchestration. They do not need # extra state handling because their inputs/outputs are fully described by chat messages. - researcher = ChatAgent( + researcher = Agent( name="ResearcherAgent", description="Collects background facts and references for the project.", instructions=("You are the research lead. Gather crisp bullet points the team should know."), - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=AzureOpenAIChatClient(credential=AzureCliCredential()), ) - writer = ChatAgent( + writer = Agent( name="WriterAgent", description="Synthesizes the final brief for stakeholders.", instructions=("You convert the research notes into a structured brief with milestones and risks."), - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=AzureOpenAIChatClient(credential=AzureCliCredential()), ) # Create a manager agent for orchestration - manager_agent = ChatAgent( + manager_agent = Agent( name="MagenticManager", description="Orchestrator that coordinates the research and writing workflow", instructions="You coordinate a team to complete complex tasks efficiently.", - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + client=AzureOpenAIChatClient(credential=AzureCliCredential()), ) # The builder wires in the Magentic orchestrator, sets the plan review path, and @@ -167,7 +167,7 @@ async def main() -> None: if not result: print("No result data from workflow.") return - output_messages = cast(list[ChatMessage], result) + output_messages = cast(list[Message], result) print("\n=== Final Answer ===") # The output of the Magentic workflow is a list of ChatMessages with only one final message # generated by the orchestrator. @@ -234,7 +234,7 @@ async def main() -> None: print("No result data from post-plan resume.") return - output_messages = cast(list[ChatMessage], post_result) + output_messages = cast(list[Message], post_result) print("\n=== Final Answer (post-plan resume) ===") # The output of the Magentic workflow is a list of ChatMessages with only one final message # generated by the orchestrator. diff --git a/python/samples/getting_started/orchestrations/magentic_human_plan_review.py b/python/samples/getting_started/orchestrations/magentic_human_plan_review.py index 24757a1692..95f8de5f46 100644 --- a/python/samples/getting_started/orchestrations/magentic_human_plan_review.py +++ b/python/samples/getting_started/orchestrations/magentic_human_plan_review.py @@ -6,9 +6,9 @@ from collections.abc import AsyncIterable from typing import cast from agent_framework import ( + Agent, AgentResponseUpdate, - ChatAgent, - ChatMessage, + Message, WorkflowEvent, ) from agent_framework.openai import OpenAIChatClient @@ -64,7 +64,7 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str print("=" * 60) print("Final discussion summary:") # To make the type checker happy, we cast event.data to the expected type - outputs = cast(list[ChatMessage], event.data) + outputs = cast(list[Message], event.data) for msg in outputs: speaker = msg.author_name or msg.role print(f"[{speaker}]: {msg.text}") @@ -92,25 +92,25 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: - researcher_agent = ChatAgent( + researcher_agent = Agent( name="ResearcherAgent", description="Specialist in research and information gathering", instructions="You are a Researcher. You find information and gather facts.", - chat_client=OpenAIChatClient(model_id="gpt-4o"), + client=OpenAIChatClient(model_id="gpt-4o"), ) - analyst_agent = ChatAgent( + analyst_agent = Agent( name="AnalystAgent", description="Data analyst who processes and summarizes research findings", instructions="You are an Analyst. You analyze findings and create summaries.", - chat_client=OpenAIChatClient(model_id="gpt-4o"), + client=OpenAIChatClient(model_id="gpt-4o"), ) - manager_agent = ChatAgent( + manager_agent = Agent( name="MagenticManager", description="Orchestrator that coordinates the workflow", instructions="You coordinate a team to complete tasks efficiently.", - chat_client=OpenAIChatClient(model_id="gpt-4o"), + client=OpenAIChatClient(model_id="gpt-4o"), ) print("\nBuilding Magentic Workflow with Human Plan Review...") diff --git a/python/samples/getting_started/orchestrations/sequential_agents.py b/python/samples/getting_started/orchestrations/sequential_agents.py index 37c9afe975..7d77ef35c6 100644 --- a/python/samples/getting_started/orchestrations/sequential_agents.py +++ b/python/samples/getting_started/orchestrations/sequential_agents.py @@ -3,7 +3,7 @@ import asyncio from typing import cast -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework.azure import AzureOpenAIChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential @@ -12,7 +12,7 @@ from azure.identity import AzureCliCredential Sample: Sequential workflow (agent-focused API) with shared conversation context Build a high-level sequential workflow using SequentialBuilder and two domain agents. -The shared conversation (list[ChatMessage]) flows through each participant. Each agent +The shared conversation (list[Message]) flows through each participant. Each agent appends its assistant message to the context. The workflow outputs the final conversation list when complete. @@ -30,14 +30,14 @@ Prerequisites: async def main() -> None: # 1) Create agents - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - writer = chat_client.as_agent( + writer = client.as_agent( instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), name="writer", ) - reviewer = chat_client.as_agent( + reviewer = client.as_agent( instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), name="reviewer", ) @@ -46,10 +46,10 @@ async def main() -> None: workflow = SequentialBuilder(participants=[writer, reviewer]).build() # 3) Run and collect outputs - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run("Write a tagline for a budget-friendly eBike.", stream=True): if event.type == "output": - outputs.append(cast(list[ChatMessage], event.data)) + outputs.append(cast(list[Message], event.data)) if outputs: print("===== Final Conversation =====") diff --git a/python/samples/getting_started/orchestrations/sequential_custom_executors.py b/python/samples/getting_started/orchestrations/sequential_custom_executors.py index d421e85f1c..7f3e61fe2e 100644 --- a/python/samples/getting_started/orchestrations/sequential_custom_executors.py +++ b/python/samples/getting_started/orchestrations/sequential_custom_executors.py @@ -5,8 +5,8 @@ from typing import Any from agent_framework import ( AgentExecutorResponse, - ChatMessage, Executor, + Message, WorkflowContext, handler, ) @@ -18,13 +18,13 @@ from azure.identity import AzureCliCredential Sample: Sequential workflow mixing agents and a custom summarizer executor This demonstrates how SequentialBuilder chains participants with a shared -conversation context (list[ChatMessage]). An agent produces content; a custom +conversation context (list[Message]). An agent produces content; a custom executor appends a compact summary to the conversation. The workflow completes after all participants have executed in sequence, and the final output contains the complete conversation. Custom executor contract: -- Provide at least one @handler accepting AgentExecutorResponse and a WorkflowContext[list[ChatMessage]] +- Provide at least one @handler accepting AgentExecutorResponse and a WorkflowContext[list[Message]] - Emit the updated conversation via ctx.send_message([...]) Prerequisites: @@ -36,30 +36,30 @@ class Summarizer(Executor): """Simple summarizer: consumes full conversation and appends an assistant summary.""" @handler - async def summarize(self, agent_response: AgentExecutorResponse, ctx: WorkflowContext[list[ChatMessage]]) -> None: + async def summarize(self, agent_response: AgentExecutorResponse, ctx: WorkflowContext[list[Message]]) -> None: """Append a summary message to a copy of the full conversation. Note: A custom executor must be able to handle the message type from the prior participant, and produce the message type expected by the next participant. In this case, the prior participant is an agent thus the input is AgentExecutorResponse (an agent will be wrapped in an AgentExecutor, which produces `AgentExecutorResponse`). If the next participant is also an agent or this is the final participant, - the output must be `list[ChatMessage]`. + the output must be `list[Message]`. """ if not agent_response.full_conversation: - await ctx.send_message([ChatMessage("assistant", ["No conversation to summarize."])]) + await ctx.send_message([Message("assistant", ["No conversation to summarize."])]) return users = sum(1 for m in agent_response.full_conversation if m.role == "user") assistants = sum(1 for m in agent_response.full_conversation if m.role == "assistant") - summary = ChatMessage("assistant", [f"Summary -> users:{users} assistants:{assistants}"]) + summary = Message("assistant", [f"Summary -> users:{users} assistants:{assistants}"]) final_conversation = list(agent_response.full_conversation) + [summary] await ctx.send_message(final_conversation) async def main() -> None: # 1) Create a content agent - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - content = chat_client.as_agent( + client = AzureOpenAIChatClient(credential=AzureCliCredential()) + content = client.as_agent( instructions="Produce a concise paragraph answering the user's request.", name="content", ) @@ -74,7 +74,7 @@ async def main() -> None: if outputs: print("===== Final Conversation =====") - messages: list[ChatMessage] | Any = outputs[0] + messages: list[Message] | Any = outputs[0] for i, msg in enumerate(messages, start=1): name = msg.author_name or ("assistant" if msg.role == "assistant" else "user") print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}") diff --git a/python/samples/getting_started/orchestrations/sequential_participant_factory.py b/python/samples/getting_started/orchestrations/sequential_participant_factory.py deleted file mode 100644 index 38cacfffcd..0000000000 --- a/python/samples/getting_started/orchestrations/sequential_participant_factory.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework import ( - ChatAgent, - ChatMessage, - Executor, - Workflow, - WorkflowContext, - handler, -) -from agent_framework.azure import AzureOpenAIChatClient -from agent_framework.orchestrations import SequentialBuilder -from azure.identity import AzureCliCredential - -""" -Sample: Sequential workflow with participant factories - -This sample demonstrates how to create a sequential workflow with participant factories. - -Using participant factories allows you to set up proper state isolation between workflow -instances created by the same builder. This is particularly useful when you need to handle -requests or tasks in parallel with stateful participants. - -In this example, we create a sequential workflow with two participants: an accumulator -and a content producer. The accumulator is stateful and maintains a list of all messages it has -received. Context is maintained across runs of the same workflow instance but not across different -workflow instances. -""" - - -class Accumulate(Executor): - """Simple accumulator. - - Accumulates all messages from the conversation and prints them out. - """ - - def __init__(self, id: str): - super().__init__(id) - # Some internal state to accumulate messages - self._accumulated: list[str] = [] - - @handler - async def accumulate(self, conversation: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None: - self._accumulated.extend([msg.text for msg in conversation]) - print(f"Number of queries received so far: {len(self._accumulated)}") - await ctx.send_message(conversation) - - -def create_agent() -> ChatAgent: - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions="Produce a concise paragraph answering the user's request.", - name="ContentProducer", - ) - - -async def run_workflow(workflow: Workflow, query: str) -> None: - events = await workflow.run(query) - outputs = events.get_outputs() - - if outputs: - messages: list[ChatMessage] = outputs[0] - for message in messages: - name = message.author_name or ("assistant" if message.role == "assistant" else "user") - print(f"{name}: {message.text}") - else: - raise RuntimeError("No outputs received from the workflow.") - - -async def main() -> None: - # 1) Create a builder with participant factories - builder = SequentialBuilder(participant_factories=[ - lambda: Accumulate("accumulator"), - create_agent, - ]) - # 2) Build workflow_a - workflow_a = builder.build() - - # 3) Run workflow_a - # Context is maintained across runs - print("=== First Run on workflow_a ===") - await run_workflow(workflow_a, "Why is the sky blue?") - print("\n=== Second Run on workflow_a ===") - await run_workflow(workflow_a, "Repeat my previous question.") - - # 4) Build workflow_b - # This will create a new instance of the accumulator and content producer - # using the same workflow builder - workflow_b = builder.build() - - # 5) Run workflow_b - # Context is not maintained across instances - print("\n=== First Run on workflow_b ===") - await run_workflow(workflow_b, "Repeat my previous question.") - - """ - Sample Output: - - === First Run on workflow_a === - Number of queries received so far: 1 - user: Why is the sky blue? - ContentProducer: The sky appears blue due to a phenomenon called Rayleigh scattering. - When sunlight enters the Earth's atmosphere, it collides with gases - and particles, scattering shorter wavelengths of light (blue and violet) - more than the longer wavelengths (red and yellow). Although violet light - is scattered even more than blue, our eyes are more sensitive to blue - light, and some violet light is absorbed by the ozone layer. As a result, - we perceive the sky as predominantly blue during the day. - - === Second Run on workflow_a === - Number of queries received so far: 2 - user: Repeat my previous question. - ContentProducer: Why is the sky blue? - - === First Run on workflow_b === - Number of queries received so far: 1 - user: Repeat my previous question. - ContentProducer: I'm sorry, but I can't repeat your previous question as I don't have - access to your past queries. However, feel free to ask anything again, - and I'll be happy to help! - """ - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/getting_started/purview_agent/README.md b/python/samples/getting_started/purview_agent/README.md index 8982a68830..175839e9d3 100644 --- a/python/samples/getting_started/purview_agent/README.md +++ b/python/samples/getting_started/purview_agent/README.md @@ -1,6 +1,6 @@ ## Purview Policy Enforcement Sample (Python) -This getting-started sample shows how to attach Microsoft Purview policy evaluation to an Agent Framework `ChatAgent` using the **middleware** approach. +This getting-started sample shows how to attach Microsoft Purview policy evaluation to an Agent Framework `Agent` using the **middleware** approach. **What this sample demonstrates:** 1. Configure an Azure OpenAI chat client @@ -99,8 +99,8 @@ Prompt blocks set a system-level message: `Prompt blocked by policy` and termina ### Agent Middleware Injection ```python -agent = ChatAgent( - chat_client=chat_client, +agent = Agent( + client=client, instructions="You are good at telling jokes.", name="Joker", middleware=[ diff --git a/python/samples/getting_started/purview_agent/sample_purview_agent.py b/python/samples/getting_started/purview_agent/sample_purview_agent.py index b5231c2a5f..0a5e251ae4 100644 --- a/python/samples/getting_started/purview_agent/sample_purview_agent.py +++ b/python/samples/getting_started/purview_agent/sample_purview_agent.py @@ -25,7 +25,7 @@ import asyncio import os from typing import Any -from agent_framework import AgentResponse, ChatAgent, ChatMessage +from agent_framework import Agent, AgentResponse, Message from agent_framework.azure import AzureOpenAIChatClient from agent_framework.microsoft import ( PurviewChatPolicyMiddleware, @@ -141,7 +141,7 @@ async def run_with_agent_middleware() -> None: deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") - chat_client = AzureOpenAIChatClient(deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential()) + client = AzureOpenAIChatClient(deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential()) purview_agent_middleware = PurviewPolicyMiddleware( build_credential(), @@ -150,8 +150,8 @@ async def run_with_agent_middleware() -> None: ), ) - agent = ChatAgent( - chat_client=chat_client, + agent = Agent( + client=client, instructions=JOKER_INSTRUCTIONS, name=JOKER_NAME, middleware=[purview_agent_middleware], @@ -159,12 +159,12 @@ async def run_with_agent_middleware() -> None: print("-- Agent MiddlewareTypes Path --") first: AgentResponse = await agent.run( - ChatMessage("user", ["Tell me a joke about a pirate."], additional_properties={"user_id": user_id}) + Message("user", ["Tell me a joke about a pirate."], additional_properties={"user_id": user_id}) ) print("First response (agent middleware):\n", first) second: AgentResponse = await agent.run( - ChatMessage( + Message( role="user", text="That was funny. Tell me another one.", additional_properties={"user_id": user_id} ) ) @@ -180,7 +180,7 @@ async def run_with_chat_middleware() -> None: deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", default="gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") - chat_client = AzureOpenAIChatClient( + client = AzureOpenAIChatClient( deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential(), @@ -194,15 +194,15 @@ async def run_with_chat_middleware() -> None: ], ) - agent = ChatAgent( - chat_client=chat_client, + agent = Agent( + client=client, instructions=JOKER_INSTRUCTIONS, name=JOKER_NAME, ) print("-- Chat MiddlewareTypes Path --") first: AgentResponse = await agent.run( - ChatMessage( + Message( role="user", text="Give me a short clean joke.", additional_properties={"user_id": user_id}, @@ -211,7 +211,7 @@ async def run_with_chat_middleware() -> None: print("First response (chat middleware):\n", first) second: AgentResponse = await agent.run( - ChatMessage( + Message( role="user", text="One more please.", additional_properties={"user_id": user_id}, @@ -229,7 +229,7 @@ async def run_with_custom_cache_provider() -> None: deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") - chat_client = AzureOpenAIChatClient(deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential()) + client = AzureOpenAIChatClient(deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential()) custom_cache = SimpleDictCacheProvider() @@ -241,8 +241,8 @@ async def run_with_custom_cache_provider() -> None: cache_provider=custom_cache, ) - agent = ChatAgent( - chat_client=chat_client, + agent = Agent( + client=client, instructions=JOKER_INSTRUCTIONS, name=JOKER_NAME, middleware=[purview_agent_middleware], @@ -252,14 +252,14 @@ async def run_with_custom_cache_provider() -> None: print("Using SimpleDictCacheProvider") first: AgentResponse = await agent.run( - ChatMessage( + Message( role="user", text="Tell me a joke about a programmer.", additional_properties={"user_id": user_id} ) ) print("First response (custom provider):\n", first) second: AgentResponse = await agent.run( - ChatMessage("user", ["That's hilarious! One more?"], additional_properties={"user_id": user_id}) + Message("user", ["That's hilarious! One more?"], additional_properties={"user_id": user_id}) ) print("Second response (custom provider):\n", second) @@ -271,7 +271,7 @@ async def run_with_custom_cache_provider() -> None: deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") - chat_client = AzureOpenAIChatClient(deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential()) + client = AzureOpenAIChatClient(deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential()) # No cache_provider specified - uses default InMemoryCacheProvider purview_agent_middleware = PurviewPolicyMiddleware( @@ -283,8 +283,8 @@ async def run_with_custom_cache_provider() -> None: ), ) - agent = ChatAgent( - chat_client=chat_client, + agent = Agent( + client=client, instructions=JOKER_INSTRUCTIONS, name=JOKER_NAME, middleware=[purview_agent_middleware], @@ -294,12 +294,12 @@ async def run_with_custom_cache_provider() -> None: print("Using default InMemoryCacheProvider with settings-based configuration") first: AgentResponse = await agent.run( - ChatMessage("user", ["Tell me a joke about AI."], additional_properties={"user_id": user_id}) + Message("user", ["Tell me a joke about AI."], additional_properties={"user_id": user_id}) ) print("First response (default cache):\n", first) second: AgentResponse = await agent.run( - ChatMessage("user", ["Nice! Another AI joke please."], additional_properties={"user_id": user_id}) + Message("user", ["Nice! Another AI joke please."], additional_properties={"user_id": user_id}) ) print("Second response (default cache):\n", second) diff --git a/python/samples/getting_started/threads/custom_chat_message_store_thread.py b/python/samples/getting_started/threads/custom_chat_message_store_thread.py index 709f9d45de..b5ab03bbcb 100644 --- a/python/samples/getting_started/threads/custom_chat_message_store_thread.py +++ b/python/samples/getting_started/threads/custom_chat_message_store_thread.py @@ -4,7 +4,7 @@ import asyncio from collections.abc import Collection from typing import Any -from agent_framework import ChatMessage, ChatMessageStoreProtocol +from agent_framework import ChatMessageStoreProtocol, Message from agent_framework._threads import ChatMessageStoreState from agent_framework.openai import OpenAIChatClient @@ -21,15 +21,15 @@ class CustomChatMessageStore(ChatMessageStoreProtocol): """Implementation of custom chat message store. In real applications, this can be an implementation of relational database or vector store.""" - def __init__(self, messages: Collection[ChatMessage] | None = None) -> None: - self._messages: list[ChatMessage] = [] + def __init__(self, messages: Collection[Message] | None = None) -> None: + self._messages: list[Message] = [] if messages: self._messages.extend(messages) - async def add_messages(self, messages: Collection[ChatMessage]) -> None: + async def add_messages(self, messages: Collection[Message]) -> None: self._messages.extend(messages) - async def list_messages(self) -> list[ChatMessage]: + async def list_messages(self) -> list[Message]: return self._messages @classmethod diff --git a/python/samples/getting_started/tools/function_tool_with_approval.py b/python/samples/getting_started/tools/function_tool_with_approval.py index 4a76c631e6..e149289091 100644 --- a/python/samples/getting_started/tools/function_tool_with_approval.py +++ b/python/samples/getting_started/tools/function_tool_with_approval.py @@ -4,7 +4,7 @@ import asyncio from random import randrange from typing import TYPE_CHECKING, Annotated, Any -from agent_framework import AgentResponse, ChatAgent, ChatMessage, tool +from agent_framework import Agent, AgentResponse, Message, tool from agent_framework.openai import OpenAIResponsesClient if TYPE_CHECKING: @@ -59,14 +59,14 @@ async def handle_approvals(query: str, agent: "SupportsAgentRun") -> AgentRespon ) # Add the assistant message with the approval request - new_inputs.append(ChatMessage("assistant", [user_input_needed])) + new_inputs.append(Message("assistant", [user_input_needed])) # Get user approval user_approval = await asyncio.to_thread(input, "\nApprove function call? (y/n): ") # Add the user's approval response new_inputs.append( - ChatMessage("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) + Message("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) # Run again with all the context @@ -109,14 +109,14 @@ async def handle_approvals_streaming(query: str, agent: "SupportsAgentRun") -> N ) # Add the assistant message with the approval request - new_inputs.append(ChatMessage("assistant", [user_input_needed])) + new_inputs.append(Message("assistant", [user_input_needed])) # Get user approval user_approval = await asyncio.to_thread(input, "\nApprove function call? (y/n): ") # Add the user's approval response new_inputs.append( - ChatMessage("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) + Message("user", [user_input_needed.to_function_approval_response(user_approval.lower() == "y")]) ) # Update input with all the context for next iteration @@ -127,8 +127,8 @@ async def run_weather_agent_with_approval(stream: bool) -> None: """Example showing AI function with approval requirement.""" print(f"\n=== Weather Agent with Approval Required ({'Streaming' if stream else 'Non-Streaming'}) ===\n") - async with ChatAgent( - chat_client=OpenAIResponsesClient(), + async with Agent( + client=OpenAIResponsesClient(), name="WeatherAgent", instructions=("You are a helpful weather assistant. Use the get_weather tool to provide weather information."), tools=[get_weather, get_weather_detail], diff --git a/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py b/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py index de1da05991..e3f442ecee 100644 --- a/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py +++ b/python/samples/getting_started/tools/function_tool_with_approval_and_threads.py @@ -3,7 +3,7 @@ import asyncio from typing import Annotated -from agent_framework import ChatAgent, ChatMessage, tool +from agent_framework import Agent, Message, tool from agent_framework.azure import AzureOpenAIChatClient """ @@ -28,8 +28,8 @@ async def approval_example() -> None: """Example showing approval with threads.""" print("=== Tool Approval with Thread ===\n") - agent = ChatAgent( - chat_client=AzureOpenAIChatClient(), + agent = Agent( + client=AzureOpenAIChatClient(), name="CalendarAgent", instructions="You are a helpful calendar assistant.", tools=[add_to_calendar], @@ -55,7 +55,7 @@ async def approval_example() -> None: # Step 2: Send approval response approval_response = request.to_function_approval_response(approved=approved) - result = await agent.run(ChatMessage("user", [approval_response]), thread=thread) + result = await agent.run(Message("user", [approval_response]), thread=thread) print(f"Agent: {result}\n") @@ -64,8 +64,8 @@ async def rejection_example() -> None: """Example showing rejection with threads.""" print("=== Tool Rejection with Thread ===\n") - agent = ChatAgent( - chat_client=AzureOpenAIChatClient(), + agent = Agent( + client=AzureOpenAIChatClient(), name="CalendarAgent", instructions="You are a helpful calendar assistant.", tools=[add_to_calendar], @@ -88,7 +88,7 @@ async def rejection_example() -> None: # Send rejection response rejection_response = request.to_function_approval_response(approved=False) - result = await agent.run(ChatMessage("user", [rejection_response]), thread=thread) + result = await agent.run(Message("user", [rejection_response]), thread=thread) print(f"Agent: {result}\n") diff --git a/python/samples/getting_started/workflows/README.md b/python/samples/getting_started/workflows/README.md index 7b368335a3..ce4aee4172 100644 --- a/python/samples/getting_started/workflows/README.md +++ b/python/samples/getting_started/workflows/README.md @@ -161,7 +161,7 @@ Notes Sequential orchestration uses a few small adapter nodes for plumbing: -- "input-conversation" normalizes input to `list[ChatMessage]` +- "input-conversation" normalizes input to `list[Message]` - "to-conversation:" converts agent responses into the shared conversation - "complete" publishes the final output event (type='output') These may appear in event streams (executor_invoked/executor_completed). They're analogous to diff --git a/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py b/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py index 8975795e35..6410e54a05 100644 --- a/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py +++ b/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py @@ -4,6 +4,7 @@ import asyncio from agent_framework import ( Executor, + Workflow, WorkflowBuilder, WorkflowContext, executor, @@ -48,6 +49,11 @@ What this example shows - Fluent WorkflowBuilder API: add_edge(A, B) to connect nodes, set_start_executor(A), then build() -> Workflow. +- State isolation via helper functions: + Wrapping executor instantiation and workflow building inside a function + (e.g., create_workflow()) ensures each call produces fresh, independent + instances. This is the recommended pattern for reuse. + - Running and results: workflow.run(initial_input) executes the graph. Terminal nodes yield outputs using ctx.yield_output(). The workflow runs until idle. @@ -152,18 +158,28 @@ class ExclamationAdder(Executor): await ctx.send_message(result) # type: ignore +def create_workflow() -> Workflow: + """Create a fresh workflow with isolated state. + + Wrapping workflow construction in a helper function ensures each call + produces independent executor instances. This is the recommended pattern + for reuse — call create_workflow() each time you need a new workflow so + that no state leaks between runs. + """ + upper_case = UpperCase(id="upper_case_executor") + + return WorkflowBuilder(start_executor=upper_case).add_edge(upper_case, reverse_text).build() + + async def main(): """Build and run workflows using the fluent builder API.""" - # Workflow 1: Using introspection-based type detection - # ----------------------------------------------------- - upper_case = UpperCase(id="upper_case_executor") - - # Build the workflow using a fluent pattern: - # 1) start_executor=... in constructor declares the entry point - # 2) add_edge(from_node, to_node) defines a directed edge upper_case -> reverse_text - # 3) build() finalizes and returns an immutable Workflow object - workflow1 = WorkflowBuilder(start_executor=upper_case).add_edge(upper_case, reverse_text).build() + # Workflow 1: Using the helper function pattern for state isolation + # ------------------------------------------------------------------ + # Each call to create_workflow() returns a workflow with fresh executor + # instances. This is the recommended pattern when you need to run the + # same workflow topology multiple times with clean state. + workflow1 = create_workflow() # Run the workflow by sending the initial message to the start node. # The run(...) call returns an event collection; its get_outputs() method @@ -175,6 +191,7 @@ async def main(): # Workflow 2: Using explicit type parameters on @handler # ------------------------------------------------------- + upper_case = UpperCase(id="upper_case_executor") exclamation_adder = ExclamationAdder(id="exclamation_adder") # This workflow demonstrates the explicit input/output feature: diff --git a/python/samples/getting_started/workflows/_start-here/step2_agents_in_a_workflow.py b/python/samples/getting_started/workflows/_start-here/step2_agents_in_a_workflow.py index aa6378c433..8a8ac369e4 100644 --- a/python/samples/getting_started/workflows/_start-here/step2_agents_in_a_workflow.py +++ b/python/samples/getting_started/workflows/_start-here/step2_agents_in_a_workflow.py @@ -27,15 +27,15 @@ Prerequisites: async def main(): """Build and run a simple two node agent workflow: Writer then Reviewer.""" # Create the Azure chat client. AzureCliCredential uses your current az login. - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - writer_agent = chat_client.as_agent( + client = AzureOpenAIChatClient(credential=AzureCliCredential()) + writer_agent = client.as_agent( instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), name="writer", ) - reviewer_agent = chat_client.as_agent( + reviewer_agent = client.as_agent( instructions=( "You are an excellent content reviewer." "Provide actionable feedback to the writer about the provided content." diff --git a/python/samples/getting_started/workflows/_start-here/step3_streaming.py b/python/samples/getting_started/workflows/_start-here/step3_streaming.py index c9cfa6843d..7c5a7c86a7 100644 --- a/python/samples/getting_started/workflows/_start-here/step3_streaming.py +++ b/python/samples/getting_started/workflows/_start-here/step3_streaming.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import AgentResponseUpdate, ChatMessage, WorkflowBuilder +from agent_framework import AgentResponseUpdate, Message, WorkflowBuilder from agent_framework.azure import AzureOpenAIChatClient from azure.identity import AzureCliCredential @@ -26,15 +26,15 @@ Prerequisites: async def main(): """Build the two node workflow and run it with streaming to observe events.""" # Create the Azure chat client. AzureCliCredential uses your current az login. - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - writer_agent = chat_client.as_agent( + client = AzureOpenAIChatClient(credential=AzureCliCredential()) + writer_agent = client.as_agent( instructions=( "You are an excellent content writer. You create new content and edit contents based on the feedback." ), name="writer", ) - reviewer_agent = chat_client.as_agent( + reviewer_agent = client.as_agent( instructions=( "You are an excellent content reviewer." "Provide actionable feedback to the writer about the provided content." @@ -52,7 +52,7 @@ async def main(): # Run the workflow with the user's initial message and stream events as they occur. async for event in workflow.run( - ChatMessage("user", ["Create a slogan for a new electric SUV that is affordable and fun to drive."]), + Message("user", ["Create a slogan for a new electric SUV that is affordable and fun to drive."]), stream=True, ): # The outputs of the workflow are whatever the agents produce. So the events are expected to diff --git a/python/samples/getting_started/workflows/_start-here/step4_using_factories.py b/python/samples/getting_started/workflows/_start-here/step4_using_factories.py deleted file mode 100644 index b5554fae81..0000000000 --- a/python/samples/getting_started/workflows/_start-here/step4_using_factories.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -import asyncio - -from agent_framework import ( - AgentResponseUpdate, - ChatAgent, - Executor, - WorkflowBuilder, - WorkflowContext, - executor, - handler, -) -from agent_framework.azure import AzureOpenAIChatClient -from azure.identity import AzureCliCredential - -""" -Step 4: Using Factories to Define Executors and Agents - -What this example shows -- Defining custom executors using both class-based and function-based approaches. -- Registering executor and agent factories with WorkflowBuilder for lazy instantiation. -- Building a simple workflow that transforms input text through multiple steps. - -Benefits of using factories -- Decouples executor and agent creation from workflow definition. -- Isolated instances are created for workflow builder build, allowing for cleaner state management - and handling parallel workflow runs. - -It is recommended to use factories when defining executors and agents for production workflows. - -Prerequisites -- No external services required. -""" - - -class UpperCase(Executor): - def __init__(self, id: str): - super().__init__(id=id) - - @handler - async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None: - """Convert the input to uppercase and forward it to the next node.""" - result = text.upper() - - # Send the result to the next executor in the workflow. - await ctx.send_message(result) - - -@executor(id="reverse_text_executor") -async def reverse_text(text: str, ctx: WorkflowContext[str]) -> None: - """Reverse the input string and send it downstream.""" - result = text[::-1] - - # Send the result to the next executor in the workflow. - await ctx.send_message(result) - - -def create_agent() -> ChatAgent: - """Factory function to create a Writer agent.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=("You decode messages. Try to reconstruct the original message."), - name="decoder", - ) - - -async def main(): - """Build and run a simple 2-step workflow using the fluent builder API.""" - # Build the workflow using a fluent pattern: - # 1) register_executor(factory, name) registers an executor factory - # 2) register_agent(factory, name) registers an agent factory - # 3) add_chain([node_names]) adds a sequence of nodes to the workflow - # 4) set_start_executor(node) declares the entry point - # 5) build() finalizes and returns an immutable Workflow object - workflow = ( - WorkflowBuilder(start_executor="UpperCase") - .register_executor(lambda: UpperCase(id="upper_case_executor"), name="UpperCase") - .register_executor(lambda: reverse_text, name="ReverseText") - .register_agent(create_agent, name="DecoderAgent") - .add_chain(["UpperCase", "ReverseText", "DecoderAgent"]) - .build() - ) - - first_update = True - async for event in workflow.run("hello world", stream=True): - # The outputs of the workflow are whatever the agents produce. So the events are expected to - # contain `AgentResponseUpdate` from the agents in the workflow. - if event.type == "output" and isinstance(event.data, AgentResponseUpdate): - update = event.data - if first_update: - print(f"{update.author_name}: {update.text}", end="", flush=True) - first_update = False - else: - print(update.text, end="", flush=True) - - """ - Sample Output: - - decoder: HELLO WORLD - """ - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/python/samples/getting_started/workflows/agents/azure_ai_agents_with_shared_thread.py b/python/samples/getting_started/workflows/agents/azure_ai_agents_with_shared_thread.py index 890dbe396f..c5ab83e3e7 100644 --- a/python/samples/getting_started/workflows/agents/azure_ai_agents_with_shared_thread.py +++ b/python/samples/getting_started/workflows/agents/azure_ai_agents_with_shared_thread.py @@ -3,6 +3,7 @@ import asyncio from agent_framework import ( + AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, ChatMessageStore, @@ -70,15 +71,12 @@ async def main() -> None: # Set the message store to store messages in memory. shared_thread.message_store = ChatMessageStore() + writer_executor = AgentExecutor(writer, agent_thread=shared_thread) + reviewer_executor = AgentExecutor(reviewer, agent_thread=shared_thread) + workflow = ( - WorkflowBuilder(start_executor="writer") - .register_agent(factory_func=lambda: writer, name="writer", agent_thread=shared_thread) - .register_agent(factory_func=lambda: reviewer, name="reviewer", agent_thread=shared_thread) - .register_executor( - factory_func=lambda: intercept_agent_response, - name="intercept_agent_response", - ) - .add_chain(["writer", "intercept_agent_response", "reviewer"]) + WorkflowBuilder(start_executor=writer_executor) + .add_chain([writer_executor, intercept_agent_response, reviewer_executor]) .build() ) diff --git a/python/samples/getting_started/workflows/agents/azure_chat_agents_and_executor.py b/python/samples/getting_started/workflows/agents/azure_chat_agents_and_executor.py index 3e3751fd86..8de5b71b73 100644 --- a/python/samples/getting_started/workflows/agents/azure_chat_agents_and_executor.py +++ b/python/samples/getting_started/workflows/agents/azure_chat_agents_and_executor.py @@ -7,7 +7,7 @@ from agent_framework import ( AgentExecutorRequest, AgentExecutorResponse, AgentResponseUpdate, - ChatMessage, + Message, WorkflowBuilder, WorkflowContext, executor, @@ -84,7 +84,7 @@ async def enrich_with_references( f"{external_note}\n\n" "Please update the prior assistant answer so it weaves this note into the guidance." ) - conversation.append(ChatMessage("user", [follow_up])) + conversation.append(Message("user", [follow_up])) # Output a new AgentExecutorRequest for the next agent in the workflow. # Agents in workflows handle this type and will generate a response based on the request. diff --git a/python/samples/getting_started/workflows/agents/azure_chat_agents_tool_calls_with_feedback.py b/python/samples/getting_started/workflows/agents/azure_chat_agents_tool_calls_with_feedback.py index 3515709157..cacaa2b493 100644 --- a/python/samples/getting_started/workflows/agents/azure_chat_agents_tool_calls_with_feedback.py +++ b/python/samples/getting_started/workflows/agents/azure_chat_agents_tool_calls_with_feedback.py @@ -6,13 +6,14 @@ from dataclasses import dataclass, field from typing import Annotated from agent_framework import ( + Agent, + AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, AgentResponse, AgentResponseUpdate, - ChatAgent, - ChatMessage, Executor, + Message, WorkflowBuilder, WorkflowContext, WorkflowEvent, @@ -89,7 +90,7 @@ class DraftFeedbackRequest: prompt: str = "" draft_text: str = "" - conversation: list[ChatMessage] = field(default_factory=list) # type: ignore[reportUnknownVariableType] + conversation: list[Message] = field(default_factory=list) # type: ignore[reportUnknownVariableType] class Coordinator(Executor): @@ -115,7 +116,7 @@ class Coordinator(Executor): # Writer agent response; request human feedback. # Preserve the full conversation so the final editor # can see tool traces and the initial prompt. - conversation: list[ChatMessage] + conversation: list[Message] if draft.full_conversation is not None: conversation = list(draft.full_conversation) else: @@ -146,7 +147,7 @@ class Coordinator(Executor): # Human approved the draft as-is; forward it unchanged. await ctx.send_message( AgentExecutorRequest( - messages=original_request.conversation + [ChatMessage("user", text="The draft is approved as-is.")], + messages=original_request.conversation + [Message("user", text="The draft is approved as-is.")], should_respond=True, ), target_id=self.final_editor_id, @@ -154,20 +155,20 @@ class Coordinator(Executor): return # Human provided feedback; prompt the writer to revise. - conversation: list[ChatMessage] = list(original_request.conversation) + conversation: list[Message] = list(original_request.conversation) instruction = ( "A human reviewer shared the following guidance:\n" f"{note or 'No specific guidance provided.'}\n\n" "Rewrite the draft from the previous assistant message into a polished final version. " "Keep the response under 120 words and reflect any requested tone adjustments." ) - conversation.append(ChatMessage("user", text=instruction)) + conversation.append(Message("user", text=instruction)) await ctx.send_message( AgentExecutorRequest(messages=conversation, should_respond=True), target_id=self.writer_id ) -def create_writer_agent() -> ChatAgent: +def create_writer_agent() -> Agent: """Creates a writer agent with tools.""" return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name="writer_agent", @@ -181,7 +182,7 @@ def create_writer_agent() -> ChatAgent: ) -def create_final_editor_agent() -> ChatAgent: +def create_final_editor_agent() -> Agent: """Creates a final editor agent.""" return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( name="final_editor_agent", @@ -239,22 +240,20 @@ async def main() -> None: """Run the workflow and bridge human feedback between two agents.""" # Build the workflow. + writer_agent = AgentExecutor(create_writer_agent()) + final_editor_agent = AgentExecutor(create_final_editor_agent()) + coordinator = Coordinator( + id="coordinator", + writer_id="writer_agent", + final_editor_id="final_editor_agent", + ) + workflow = ( - WorkflowBuilder(start_executor="writer_agent") - .register_agent(create_writer_agent, name="writer_agent") - .register_agent(create_final_editor_agent, name="final_editor_agent") - .register_executor( - lambda: Coordinator( - id="coordinator", - writer_id="writer_agent", - final_editor_id="final_editor_agent", - ), - name="coordinator", - ) - .add_edge("writer_agent", "coordinator") - .add_edge("coordinator", "writer_agent") - .add_edge("final_editor_agent", "coordinator") - .add_edge("coordinator", "final_editor_agent") + WorkflowBuilder(start_executor=writer_agent) + .add_edge(writer_agent, coordinator) + .add_edge(coordinator, writer_agent) + .add_edge(final_editor_agent, coordinator) + .add_edge(coordinator, final_editor_agent) .build() ) diff --git a/python/samples/getting_started/workflows/agents/concurrent_workflow_as_agent.py b/python/samples/getting_started/workflows/agents/concurrent_workflow_as_agent.py index 7c10455eaa..42202aec5f 100644 --- a/python/samples/getting_started/workflows/agents/concurrent_workflow_as_agent.py +++ b/python/samples/getting_started/workflows/agents/concurrent_workflow_as_agent.py @@ -38,9 +38,9 @@ def clear_and_redraw(buffers: dict[str, str], agent_order: list[str]) -> None: async def main() -> None: # 1) Create three domain agents using AzureOpenAIChatClient - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - researcher = chat_client.as_agent( + researcher = client.as_agent( instructions=( "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," " opportunities, and risks." @@ -48,7 +48,7 @@ async def main() -> None: name="researcher", ) - marketer = chat_client.as_agent( + marketer = client.as_agent( instructions=( "You're a creative marketing strategist. Craft compelling value propositions and target messaging" " aligned to the prompt." @@ -56,7 +56,7 @@ async def main() -> None: name="marketer", ) - legal = chat_client.as_agent( + legal = client.as_agent( instructions=( "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" " based on the prompt." diff --git a/python/samples/getting_started/workflows/agents/custom_agent_executors.py b/python/samples/getting_started/workflows/agents/custom_agent_executors.py index c193e7368d..a44aff4f09 100644 --- a/python/samples/getting_started/workflows/agents/custom_agent_executors.py +++ b/python/samples/getting_started/workflows/agents/custom_agent_executors.py @@ -3,9 +3,9 @@ import asyncio from agent_framework import ( - ChatAgent, - ChatMessage, + Agent, Executor, + Message, WorkflowBuilder, WorkflowContext, handler, @@ -37,11 +37,11 @@ class Writer(Executor): """Custom executor that owns a domain specific agent responsible for generating content. This class demonstrates: - - Attaching a ChatAgent to an Executor so it participates as a node in a workflow. + - Attaching a Agent to an Executor so it participates as a node in a workflow. - Using a @handler method to accept a typed input and forward a typed output via ctx.send_message. """ - agent: ChatAgent + agent: Agent def __init__(self, id: str = "writer"): # Create a domain specific agent using your configured AzureOpenAIChatClient. @@ -54,12 +54,12 @@ class Writer(Executor): super().__init__(id=id) @handler - async def handle(self, message: ChatMessage, ctx: WorkflowContext[list[ChatMessage], str]) -> None: + async def handle(self, message: Message, ctx: WorkflowContext[list[Message], str]) -> None: """Generate content using the agent and forward the updated conversation. Contract for this handler: - - message is the inbound user ChatMessage. - - ctx is a WorkflowContext that expects a list[ChatMessage] to be sent downstream. + - message is the inbound user Message. + - ctx is a WorkflowContext that expects a list[Message] to be sent downstream. Pattern shown here: 1) Seed the conversation with the inbound message. @@ -67,7 +67,7 @@ class Writer(Executor): 3) Forward the cumulative messages to the next executor with ctx.send_message. """ # Start the conversation with the incoming user message. - messages: list[ChatMessage] = [message] + messages: list[Message] = [message] # Run the agent and extend the conversation with the agent's messages. response = await self.agent.run(messages) messages.extend(response.messages) @@ -83,7 +83,7 @@ class Reviewer(Executor): - Yielding the final text outcome to complete the workflow. """ - agent: ChatAgent + agent: Agent def __init__(self, id: str = "reviewer"): # Create a domain specific agent that evaluates and refines content. @@ -95,7 +95,7 @@ class Reviewer(Executor): super().__init__(id=id) @handler - async def handle(self, messages: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage], str]) -> None: + async def handle(self, messages: list[Message], ctx: WorkflowContext[list[Message], str]) -> None: """Review the full conversation transcript and complete with a final string. This node consumes all messages so far. It uses its agent to produce the final text, @@ -118,7 +118,7 @@ async def main(): # Run the workflow with the user's initial message. # For foundational clarity, use run (non streaming) and print the workflow output. events = await workflow.run( - ChatMessage("user", ["Create a slogan for a new electric SUV that is affordable and fun to drive."]) + Message("user", ["Create a slogan for a new electric SUV that is affordable and fun to drive."]) ) # The terminal node yields output; print its contents. outputs = events.get_outputs() diff --git a/python/samples/getting_started/workflows/agents/group_chat_workflow_as_agent.py b/python/samples/getting_started/workflows/agents/group_chat_workflow_as_agent.py index 1693aeb642..9bf24c82e1 100644 --- a/python/samples/getting_started/workflows/agents/group_chat_workflow_as_agent.py +++ b/python/samples/getting_started/workflows/agents/group_chat_workflow_as_agent.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import ChatAgent +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient from agent_framework.orchestrations import GroupChatBuilder @@ -19,18 +19,18 @@ Prerequisites: async def main() -> None: - researcher = ChatAgent( + researcher = Agent( name="Researcher", description="Collects relevant background information.", instructions="Gather concise facts that help a teammate answer the question.", - chat_client=OpenAIChatClient(model_id="gpt-4o-mini"), + client=OpenAIChatClient(model_id="gpt-4o-mini"), ) - writer = ChatAgent( + writer = Agent( name="Writer", description="Synthesizes a polished answer using the gathered notes.", instructions="Compose clear and structured answers using any notes provided.", - chat_client=OpenAIResponsesClient(), + client=OpenAIResponsesClient(), ) # intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds diff --git a/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py b/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py index f3dcefab7a..955446ca80 100644 --- a/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py +++ b/python/samples/getting_started/workflows/agents/handoff_workflow_as_agent.py @@ -4,10 +4,10 @@ import asyncio from typing import Annotated from agent_framework import ( + Agent, AgentResponse, - ChatAgent, - ChatMessage, Content, + Message, WorkflowAgent, tool, ) @@ -57,17 +57,17 @@ def process_return(order_number: Annotated[str, "Order number to process return return f"Return initiated successfully for order {order_number}. You will receive return instructions via email." -def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAgent, ChatAgent, ChatAgent]: +def create_agents(client: AzureOpenAIChatClient) -> tuple[Agent, Agent, Agent, Agent]: """Create and configure the triage and specialist agents. Args: - chat_client: The AzureOpenAIChatClient to use for creating agents. + client: The AzureOpenAIChatClient to use for creating agents. Returns: Tuple of (triage_agent, refund_agent, order_agent, return_agent) """ # Triage agent: Acts as the frontline dispatcher - triage_agent = chat_client.as_agent( + triage_agent = client.as_agent( instructions=( "You are frontline support triage. Route customer issues to the appropriate specialist agents " "based on the problem described." @@ -76,7 +76,7 @@ def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAg ) # Refund specialist: Handles refund requests - refund_agent = chat_client.as_agent( + refund_agent = client.as_agent( instructions="You process refund requests.", name="refund_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -84,7 +84,7 @@ def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAg ) # Order/shipping specialist: Resolves delivery issues - order_agent = chat_client.as_agent( + order_agent = client.as_agent( instructions="You handle order and shipping inquiries.", name="order_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -92,7 +92,7 @@ def create_agents(chat_client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAg ) # Return specialist: Handles return requests - return_agent = chat_client.as_agent( + return_agent = client.as_agent( instructions="You manage product return requests.", name="return_agent", # In a real application, an agent can have multiple tools; here we keep it simple @@ -147,10 +147,10 @@ async def main() -> None: replace the scripted_responses with actual user input collection. """ # Initialize the Azure OpenAI chat client - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) # Create all agents: triage + specialists - triage, refund, order, support = create_agents(chat_client) + triage, refund, order, support = create_agents(client) # Build the handoff workflow # - participants: All agents that can participate in the workflow @@ -213,7 +213,7 @@ async def main() -> None: function_results = [ Content.from_function_result(call_id=req_id, result=response) for req_id, response in responses.items() ] - response = await agent.run(ChatMessage("tool", function_results)) + response = await agent.run(Message("tool", function_results)) pending_requests = handle_response_and_requests(response) diff --git a/python/samples/getting_started/workflows/agents/magentic_workflow_as_agent.py b/python/samples/getting_started/workflows/agents/magentic_workflow_as_agent.py index 4d687514c1..6255b18d0b 100644 --- a/python/samples/getting_started/workflows/agents/magentic_workflow_as_agent.py +++ b/python/samples/getting_started/workflows/agents/magentic_workflow_as_agent.py @@ -3,8 +3,7 @@ import asyncio from agent_framework import ( - ChatAgent, - HostedCodeInterpreterTool, + Agent, ) from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient from agent_framework.orchestrations import MagenticBuilder @@ -22,30 +21,34 @@ Prerequisites: async def main() -> None: - researcher_agent = ChatAgent( + researcher_agent = Agent( name="ResearcherAgent", description="Specialist in research and information gathering", instructions=( "You are a Researcher. You find information without additional computation or quantitative analysis." ), # This agent requires the gpt-4o-search-preview model to perform web searches. - chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"), + client=OpenAIChatClient(model_id="gpt-4o-search-preview"), ) - coder_agent = ChatAgent( + # Create code interpreter tool using instance method + coder_client = OpenAIResponsesClient() + code_interpreter_tool = coder_client.get_code_interpreter_tool() + + coder_agent = Agent( name="CoderAgent", description="A helpful assistant that writes and executes code to process and analyze data.", instructions="You solve questions using code. Please provide detailed analysis and computation process.", - chat_client=OpenAIResponsesClient(), - tools=HostedCodeInterpreterTool(), + client=coder_client, + tools=code_interpreter_tool, ) # Create a manager agent for orchestration - manager_agent = ChatAgent( + manager_agent = Agent( name="MagenticManager", description="Orchestrator that coordinates the research and coding workflow", instructions="You coordinate a team to complete complex tasks efficiently.", - chat_client=OpenAIChatClient(), + client=OpenAIChatClient(), ) print("\nBuilding Magentic Workflow...") diff --git a/python/samples/getting_started/workflows/agents/sequential_workflow_as_agent.py b/python/samples/getting_started/workflows/agents/sequential_workflow_as_agent.py index 7fc1720cbc..73e8cbb2c7 100644 --- a/python/samples/getting_started/workflows/agents/sequential_workflow_as_agent.py +++ b/python/samples/getting_started/workflows/agents/sequential_workflow_as_agent.py @@ -27,14 +27,14 @@ Prerequisites: async def main() -> None: # 1) Create agents - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - writer = chat_client.as_agent( + writer = client.as_agent( instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), name="writer", ) - reviewer = chat_client.as_agent( + reviewer = client.as_agent( instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), name="reviewer", ) diff --git a/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop.py b/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop.py index af405084dc..30c1d78a3e 100644 --- a/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop.py +++ b/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop.py @@ -16,9 +16,9 @@ if str(_SAMPLES_ROOT) not in sys.path: sys.path.insert(0, str(_SAMPLES_ROOT)) from agent_framework import ( # noqa: E402 - ChatMessage, Content, Executor, + Message, WorkflowAgent, WorkflowBuilder, WorkflowContext, @@ -98,21 +98,16 @@ async def main() -> None: print("Building workflow with Worker-Reviewer cycle...") # Build a workflow with bidirectional communication between Worker and Reviewer, # and escalation paths for human review. + worker = Worker( + id="worker", + chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), + ) + reviewer = ReviewerWithHumanInTheLoop(worker_id="worker") + agent = ( - WorkflowBuilder(start_executor="worker") - .register_executor( - lambda: Worker( - id="sub-worker", - chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()), - ), - name="worker", - ) - .register_executor( - lambda: ReviewerWithHumanInTheLoop(worker_id="sub-worker"), - name="reviewer", - ) - .add_edge("worker", "reviewer") # Worker sends requests to Reviewer - .add_edge("reviewer", "worker") # Reviewer sends feedback to Worker + WorkflowBuilder(start_executor=worker) + .add_edge(worker, reviewer) # Worker sends requests to Reviewer + .add_edge(reviewer, worker) # Reviewer sends feedback to Worker .build() .as_agent() # Convert workflow into an agent interface ) @@ -164,7 +159,7 @@ async def main() -> None: result=human_response, ) # Send the human review result back to the agent. - response = await agent.run(ChatMessage("tool", [human_review_function_result])) + response = await agent.run(Message("tool", [human_review_function_result])) print(f"📤 Agent Response: {response.messages[-1].text}") print("=" * 50) diff --git a/python/samples/getting_started/workflows/agents/workflow_as_agent_kwargs.py b/python/samples/getting_started/workflows/agents/workflow_as_agent_kwargs.py index aefcf9b1e5..a41ede52d1 100644 --- a/python/samples/getting_started/workflows/agents/workflow_as_agent_kwargs.py +++ b/python/samples/getting_started/workflows/agents/workflow_as_agent_kwargs.py @@ -80,10 +80,10 @@ async def main() -> None: print("=" * 70) # Create chat client - chat_client = OpenAIChatClient() + client = OpenAIChatClient() # Create agent with tools that use kwargs - agent = chat_client.as_agent( + agent = client.as_agent( name="assistant", instructions=( "You are a helpful assistant. Use the available tools to help users. " diff --git a/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern.py b/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern.py index 3d205cbbb2..d2aa65c9a2 100644 --- a/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern.py +++ b/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern.py @@ -6,9 +6,9 @@ from uuid import uuid4 from agent_framework import ( AgentResponse, - ChatClientProtocol, - ChatMessage, Executor, + Message, + SupportsChatGetResponse, WorkflowBuilder, WorkflowContext, handler, @@ -44,8 +44,8 @@ class ReviewRequest: """Structured request passed from Worker to Reviewer for evaluation.""" request_id: str - user_messages: list[ChatMessage] - agent_messages: list[ChatMessage] + user_messages: list[Message] + agent_messages: list[Message] @dataclass @@ -60,9 +60,9 @@ class ReviewResponse: class Reviewer(Executor): """Executor that reviews agent responses and provides structured feedback.""" - def __init__(self, id: str, chat_client: ChatClientProtocol) -> None: + def __init__(self, id: str, client: SupportsChatGetResponse) -> None: super().__init__(id=id) - self._chat_client = chat_client + self._chat_client = client @handler async def review(self, request: ReviewRequest, ctx: WorkflowContext[ReviewResponse]) -> None: @@ -75,7 +75,7 @@ class Reviewer(Executor): # Construct review instructions and context. messages = [ - ChatMessage( + Message( role="system", text=( "You are a reviewer for an AI agent. Provide feedback on the " @@ -93,7 +93,7 @@ class Reviewer(Executor): messages.extend(request.agent_messages) # Add explicit review instruction. - messages.append(ChatMessage("user", ["Please review the agent's responses."])) + messages.append(Message("user", ["Please review the agent's responses."])) print("Reviewer: Sending review request to LLM...") response = await self._chat_client.get_response(messages=messages, options={"response_format": _Response}) @@ -112,17 +112,17 @@ class Reviewer(Executor): class Worker(Executor): """Executor that generates responses and incorporates feedback when necessary.""" - def __init__(self, id: str, chat_client: ChatClientProtocol) -> None: + def __init__(self, id: str, client: SupportsChatGetResponse) -> None: super().__init__(id=id) - self._chat_client = chat_client - self._pending_requests: dict[str, tuple[ReviewRequest, list[ChatMessage]]] = {} + self._chat_client = client + self._pending_requests: dict[str, tuple[ReviewRequest, list[Message]]] = {} @handler - async def handle_user_messages(self, user_messages: list[ChatMessage], ctx: WorkflowContext[ReviewRequest]) -> None: + async def handle_user_messages(self, user_messages: list[Message], ctx: WorkflowContext[ReviewRequest]) -> None: print("Worker: Received user messages, generating response...") # Initialize chat with system prompt. - messages = [ChatMessage("system", ["You are a helpful assistant."])] + messages = [Message("system", ["You are a helpful assistant."])] messages.extend(user_messages) print("Worker: Calling LLM to generate response...") @@ -161,8 +161,8 @@ class Worker(Executor): print("Worker: Regenerating response with feedback...") # Incorporate review feedback. - messages.append(ChatMessage("system", [review.feedback])) - messages.append(ChatMessage("system", ["Please incorporate the feedback and regenerate the response."])) + messages.append(Message("system", [review.feedback])) + messages.append(Message("system", ["Please incorporate the feedback and regenerate the response."])) messages.extend(request.user_messages) # Retry with updated prompt. @@ -186,18 +186,13 @@ async def main() -> None: print("=" * 50) print("Building workflow with Worker ↔ Reviewer cycle...") + worker = Worker(id="worker", chat_client=OpenAIChatClient(model_id="gpt-4.1-nano")) + reviewer = Reviewer(id="reviewer", chat_client=OpenAIChatClient(model_id="gpt-4.1")) + agent = ( - WorkflowBuilder(start_executor="worker") - .register_executor( - lambda: Worker(id="worker", chat_client=OpenAIChatClient(model_id="gpt-4.1-nano")), - name="worker", - ) - .register_executor( - lambda: Reviewer(id="reviewer", chat_client=OpenAIChatClient(model_id="gpt-4.1")), - name="reviewer", - ) - .add_edge("worker", "reviewer") # Worker sends responses to Reviewer - .add_edge("reviewer", "worker") # Reviewer provides feedback to Worker + WorkflowBuilder(start_executor=worker) + .add_edge(worker, reviewer) # Worker sends responses to Reviewer + .add_edge(reviewer, worker) # Reviewer provides feedback to Worker .build() .as_agent() # Wrap workflow as an agent ) diff --git a/python/samples/getting_started/workflows/agents/workflow_as_agent_with_thread.py b/python/samples/getting_started/workflows/agents/workflow_as_agent_with_thread.py index 621d54216f..0e84b10821 100644 --- a/python/samples/getting_started/workflows/agents/workflow_as_agent_with_thread.py +++ b/python/samples/getting_started/workflows/agents/workflow_as_agent_with_thread.py @@ -2,7 +2,7 @@ import asyncio -from agent_framework import AgentThread, ChatAgent, ChatMessageStore +from agent_framework import AgentThread, ChatMessageStore from agent_framework.openai import OpenAIChatClient from agent_framework.orchestrations import SequentialBuilder @@ -37,29 +37,26 @@ Prerequisites: async def main() -> None: # Create a chat client - chat_client = OpenAIChatClient() + client = OpenAIChatClient() - # Define factory functions for workflow participants - def create_assistant() -> ChatAgent: - return chat_client.as_agent( - name="assistant", - instructions=( - "You are a helpful assistant. Answer questions based on the conversation " - "history. If the user asks about something mentioned earlier, reference it." - ), - ) + assistant = client.as_agent( + name="assistant", + instructions=( + "You are a helpful assistant. Answer questions based on the conversation " + "history. If the user asks about something mentioned earlier, reference it." + ), + ) - def create_summarizer() -> ChatAgent: - return chat_client.as_agent( - name="summarizer", - instructions=( - "You are a summarizer. After the assistant responds, provide a brief " - "one-sentence summary of the key point from the conversation so far." - ), - ) + summarizer = client.as_agent( + name="summarizer", + instructions=( + "You are a summarizer. After the assistant responds, provide a brief " + "one-sentence summary of the key point from the conversation so far." + ), + ) # Build a sequential workflow: assistant -> summarizer - workflow = SequentialBuilder(participant_factories=[create_assistant, create_summarizer]).build() + workflow = SequentialBuilder(participants=[assistant, summarizer]).build() # Wrap the workflow as an agent agent = workflow.as_agent(name="ConversationalWorkflowAgent") @@ -122,15 +119,14 @@ async def demonstrate_thread_serialization() -> None: This shows how conversation history can be persisted and restored, enabling long-running conversational workflows. """ - chat_client = OpenAIChatClient() + client = OpenAIChatClient() - def create_assistant() -> ChatAgent: - return chat_client.as_agent( - name="memory_assistant", - instructions="You are a helpful assistant with good memory. Remember details from our conversation.", - ) + memory_assistant = client.as_agent( + name="memory_assistant", + instructions="You are a helpful assistant with good memory. Remember details from our conversation.", + ) - workflow = SequentialBuilder(participant_factories=[create_assistant]).build() + workflow = SequentialBuilder(participants=[memory_assistant]).build() agent = workflow.as_agent(name="MemoryWorkflowAgent") # Create initial thread and have a conversation diff --git a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py index fd5bda8551..ec194d0fa3 100644 --- a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py +++ b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py @@ -17,11 +17,12 @@ else: # `agent_framework.builtin` chat client or mock the writer executor. We keep the # concrete import here so readers can see an end-to-end configuration. from agent_framework import ( + AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, - ChatMessage, Executor, FileCheckpointStorage, + Message, Workflow, WorkflowBuilder, WorkflowCheckpoint, @@ -96,7 +97,7 @@ class BriefPreparer(Executor): # Hand the prompt to the writer agent. We always route through the # workflow context so the runtime can capture messages for checkpointing. await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage("user", text=prompt)], should_respond=True), + AgentExecutorRequest(messages=[Message("user", text=prompt)], should_respond=True), target_id=self._agent_id, ) @@ -158,7 +159,7 @@ class ReviewGateway(Executor): f"Human guidance: {reply}" ) await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage("user", text=prompt)], should_respond=True), + AgentExecutorRequest(messages=[Message("user", text=prompt)], should_respond=True), target_id=self._writer_id, ) @@ -178,23 +179,21 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow: # Wire the workflow DAG. Edges mirror the numbered steps described in the # module docstring. Because `WorkflowBuilder` is declarative, reading these # edges is often the quickest way to understand execution order. + writer_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + instructions="Write concise, warm release notes that sound human and helpful.", + name="writer", + ) + writer = AgentExecutor(writer_agent) + review_gateway = ReviewGateway(id="review_gateway", writer_id="writer") + prepare_brief = BriefPreparer(id="prepare_brief", agent_id="writer") + workflow_builder = ( WorkflowBuilder( - max_iterations=6, start_executor="prepare_brief", checkpoint_storage=checkpoint_storage + max_iterations=6, start_executor=prepare_brief, checkpoint_storage=checkpoint_storage ) - .register_agent( - lambda: AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions="Write concise, warm release notes that sound human and helpful.", - # The agent name is stable across runs which keeps checkpoints deterministic. - name="writer", - ), - name="writer", - ) - .register_executor(lambda: ReviewGateway(id="review_gateway", writer_id="writer"), name="review_gateway") - .register_executor(lambda: BriefPreparer(id="prepare_brief", agent_id="writer"), name="prepare_brief") - .add_edge("prepare_brief", "writer") - .add_edge("writer", "review_gateway") - .add_edge("review_gateway", "writer") # revisions loop + .add_edge(prepare_brief, writer) + .add_edge(writer, review_gateway) + .add_edge(review_gateway, writer) # revisions loop ) return workflow_builder.build() diff --git a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py index 7d453b6126..22a8423cba 100644 --- a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py +++ b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py @@ -105,12 +105,12 @@ class WorkerExecutor(Executor): async def main(): # Build workflow with checkpointing enabled checkpoint_storage = InMemoryCheckpointStorage() + start = StartExecutor(id="start") + worker = WorkerExecutor(id="worker") workflow_builder = ( - WorkflowBuilder(start_executor="start", checkpoint_storage=checkpoint_storage) - .register_executor(lambda: StartExecutor(id="start"), name="start") - .register_executor(lambda: WorkerExecutor(id="worker"), name="worker") - .add_edge("start", "worker") - .add_edge("worker", "worker") # Self-loop for iterative processing + WorkflowBuilder(start_executor=start, checkpoint_storage=checkpoint_storage) + .add_edge(start, worker) + .add_edge(worker, worker) # Self-loop for iterative processing ) # Run workflow with automatic checkpoint recovery diff --git a/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py b/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py index 99875c94c6..f39c997457 100644 --- a/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py +++ b/python/samples/getting_started/workflows/checkpoint/handoff_with_tool_approval_checkpoint_resume.py @@ -7,11 +7,11 @@ from pathlib import Path from typing import cast from agent_framework import ( + Agent, AgentResponse, - ChatAgent, - ChatMessage, Content, FileCheckpointStorage, + Message, Workflow, WorkflowEvent, tool, @@ -57,7 +57,7 @@ def submit_refund(refund_description: str, amount: str, order_id: str) -> str: return f"refund recorded for order {order_id} (amount: {amount}) with details: {refund_description}" -def create_agents(client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAgent, ChatAgent]: +def create_agents(client: AzureOpenAIChatClient) -> tuple[Agent, Agent, Agent]: """Create a simple handoff scenario: triage, refund, and order specialists.""" triage = client.as_agent( @@ -91,7 +91,7 @@ def create_agents(client: AzureOpenAIChatClient) -> tuple[ChatAgent, ChatAgent, return triage, refund, order -def create_workflow(checkpoint_storage: FileCheckpointStorage) -> tuple[Workflow, ChatAgent, ChatAgent, ChatAgent]: +def create_workflow(checkpoint_storage: FileCheckpointStorage) -> tuple[Workflow, Agent, Agent, Agent]: """Build the handoff workflow with checkpointing enabled.""" client = AzureOpenAIChatClient(credential=AzureCliCredential()) @@ -284,9 +284,9 @@ async def resume_with_responses( elif event.type == "output": print("\n[Workflow Output Event - Conversation Update]") - if event.data and isinstance(event.data, list) and all(isinstance(msg, ChatMessage) for msg in event.data): # type: ignore - # Now safe to cast event.data to list[ChatMessage] - conversation = cast(list[ChatMessage], event.data) # type: ignore + if event.data and isinstance(event.data, list) and all(isinstance(msg, Message) for msg in event.data): # type: ignore + # Now safe to cast event.data to list[Message] + conversation = cast(list[Message], event.data) # type: ignore for msg in conversation[-3:]: # Show last 3 messages author = msg.author_name or msg.role text = msg.text[:100] + "..." if len(msg.text) > 100 else msg.text diff --git a/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py b/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py index c975a10ae1..b93a58a50c 100644 --- a/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py +++ b/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py @@ -297,14 +297,14 @@ class LaunchCoordinator(Executor): def build_sub_workflow() -> WorkflowExecutor: """Assemble the sub-workflow used by the parent workflow executor.""" + writer = DraftWriter() + router = DraftReviewRouter() + finaliser = DraftFinaliser() sub_workflow = ( - WorkflowBuilder(start_executor="writer") - .register_executor(DraftWriter, name="writer") - .register_executor(DraftReviewRouter, name="router") - .register_executor(DraftFinaliser, name="finaliser") - .add_edge("writer", "router") - .add_edge("router", "finaliser") - .add_edge("finaliser", "writer") # permits revision loops + WorkflowBuilder(start_executor=writer) + .add_edge(writer, router) + .add_edge(router, finaliser) + .add_edge(finaliser, writer) # permits revision loops .build() ) @@ -313,12 +313,12 @@ def build_sub_workflow() -> WorkflowExecutor: def build_parent_workflow(storage: FileCheckpointStorage) -> Workflow: """Assemble the parent workflow that embeds the sub-workflow.""" + coordinator = LaunchCoordinator() + sub_executor = build_sub_workflow() return ( - WorkflowBuilder(start_executor="coordinator", checkpoint_storage=storage) - .register_executor(LaunchCoordinator, name="coordinator") - .register_executor(build_sub_workflow, name="sub_executor") - .add_edge("coordinator", "sub_executor") - .add_edge("sub_executor", "coordinator") + WorkflowBuilder(start_executor=coordinator, checkpoint_storage=storage) + .add_edge(coordinator, sub_executor) + .add_edge(sub_executor, coordinator) .build() ) diff --git a/python/samples/getting_started/workflows/checkpoint/workflow_as_agent_checkpoint.py b/python/samples/getting_started/workflows/checkpoint/workflow_as_agent_checkpoint.py index 18a0cf9258..4fc980e008 100644 --- a/python/samples/getting_started/workflows/checkpoint/workflow_as_agent_checkpoint.py +++ b/python/samples/getting_started/workflows/checkpoint/workflow_as_agent_checkpoint.py @@ -27,7 +27,6 @@ import asyncio from agent_framework import ( AgentThread, - ChatAgent, ChatMessageStore, InMemoryCheckpointStorage, ) @@ -41,22 +40,19 @@ async def basic_checkpointing() -> None: print("Basic Checkpointing with Workflow as Agent") print("=" * 60) - chat_client = OpenAIChatClient() + client = OpenAIChatClient() - def create_assistant() -> ChatAgent: - return chat_client.as_agent( - name="assistant", - instructions="You are a helpful assistant. Keep responses brief.", - ) + assistant = client.as_agent( + name="assistant", + instructions="You are a helpful assistant. Keep responses brief.", + ) - def create_reviewer() -> ChatAgent: - return chat_client.as_agent( - name="reviewer", - instructions="You are a reviewer. Provide a one-sentence summary of the assistant's response.", - ) + reviewer = client.as_agent( + name="reviewer", + instructions="You are a reviewer. Provide a one-sentence summary of the assistant's response.", + ) - # Build sequential workflow with participant factories - workflow = SequentialBuilder(participant_factories=[create_assistant, create_reviewer]).build() + workflow = SequentialBuilder(participants=[assistant, reviewer]).build() agent = workflow.as_agent(name="CheckpointedAgent") # Create checkpoint storage @@ -85,15 +81,14 @@ async def checkpointing_with_thread() -> None: print("Checkpointing with Thread Conversation History") print("=" * 60) - chat_client = OpenAIChatClient() + client = OpenAIChatClient() - def create_assistant() -> ChatAgent: - return chat_client.as_agent( - name="memory_assistant", - instructions="You are a helpful assistant with good memory. Reference previous conversation when relevant.", - ) + assistant = client.as_agent( + name="memory_assistant", + instructions="You are a helpful assistant with good memory. Reference previous conversation when relevant.", + ) - workflow = SequentialBuilder(participant_factories=[create_assistant]).build() + workflow = SequentialBuilder(participants=[assistant]).build() agent = workflow.as_agent(name="MemoryAgent") # Create both thread (for conversation) and checkpoint storage (for workflow state) @@ -129,15 +124,14 @@ async def streaming_with_checkpoints() -> None: print("Streaming with Checkpointing") print("=" * 60) - chat_client = OpenAIChatClient() + client = OpenAIChatClient() - def create_assistant() -> ChatAgent: - return chat_client.as_agent( - name="streaming_assistant", - instructions="You are a helpful assistant.", - ) + assistant = client.as_agent( + name="streaming_assistant", + instructions="You are a helpful assistant.", + ) - workflow = SequentialBuilder(participant_factories=[create_assistant]).build() + workflow = SequentialBuilder(participants=[assistant]).build() agent = workflow.as_agent(name="StreamingCheckpointAgent") checkpoint_storage = InMemoryCheckpointStorage() diff --git a/python/samples/getting_started/workflows/composition/sub_workflow_basics.py b/python/samples/getting_started/workflows/composition/sub_workflow_basics.py index 9d5168db80..1eeac824b5 100644 --- a/python/samples/getting_started/workflows/composition/sub_workflow_basics.py +++ b/python/samples/getting_started/workflows/composition/sub_workflow_basics.py @@ -140,9 +140,9 @@ def create_sub_workflow() -> WorkflowExecutor: """Create the text processing sub-workflow.""" print("🚀 Setting up sub-workflow...") + text_processor = TextProcessor() processing_workflow = ( - WorkflowBuilder(start_executor="text_processor") - .register_executor(TextProcessor, name="text_processor") + WorkflowBuilder(start_executor=text_processor) .build() ) @@ -153,12 +153,12 @@ async def main(): """Main function to run the basic sub-workflow example.""" print("🔧 Setting up parent workflow...") # Step 1: Create the parent workflow + orchestrator = TextProcessingOrchestrator() + sub_workflow_executor = create_sub_workflow() main_workflow = ( - WorkflowBuilder(start_executor="text_orchestrator") - .register_executor(TextProcessingOrchestrator, name="text_orchestrator") - .register_executor(create_sub_workflow, name="text_processor_workflow") - .add_edge("text_orchestrator", "text_processor_workflow") - .add_edge("text_processor_workflow", "text_orchestrator") + WorkflowBuilder(start_executor=orchestrator) + .add_edge(orchestrator, sub_workflow_executor) + .add_edge(sub_workflow_executor, orchestrator) .build() ) diff --git a/python/samples/getting_started/workflows/composition/sub_workflow_kwargs.py b/python/samples/getting_started/workflows/composition/sub_workflow_kwargs.py index 5d74ec42d3..af6ed4d61a 100644 --- a/python/samples/getting_started/workflows/composition/sub_workflow_kwargs.py +++ b/python/samples/getting_started/workflows/composition/sub_workflow_kwargs.py @@ -5,7 +5,7 @@ import json from typing import Annotated, Any from agent_framework import ( - ChatMessage, + Message, WorkflowExecutor, tool, ) @@ -74,10 +74,10 @@ async def main() -> None: print("=" * 70) # Create chat client - chat_client = OpenAIChatClient() + client = OpenAIChatClient() # Create an agent with tools that use kwargs - inner_agent = chat_client.as_agent( + inner_agent = client.as_agent( name="data_agent", instructions=( "You are a data access agent. Use the available tools to help users. " @@ -134,7 +134,7 @@ async def main() -> None: output_data = event.data if isinstance(output_data, list): for item in output_data: # type: ignore - if isinstance(item, ChatMessage) and item.text: + if isinstance(item, Message) and item.text: print(f"\n[Final Answer]: {item.text}") print("\n" + "=" * 70) diff --git a/python/samples/getting_started/workflows/composition/sub_workflow_parallel_requests.py b/python/samples/getting_started/workflows/composition/sub_workflow_parallel_requests.py index c272d7d21c..70030021ca 100644 --- a/python/samples/getting_started/workflows/composition/sub_workflow_parallel_requests.py +++ b/python/samples/getting_started/workflows/composition/sub_workflow_parallel_requests.py @@ -169,17 +169,18 @@ def build_resource_request_distribution_workflow() -> Workflow: elif len(self._responses) > self._request_count: raise ValueError("Received more responses than expected") + orchestrator = RequestDistribution("orchestrator") + resource_requester = ResourceRequester("resource_requester") + policy_checker = PolicyChecker("policy_checker") + result_collector = ResultCollector("result_collector") + return ( - WorkflowBuilder(start_executor="orchestrator") - .register_executor(lambda: RequestDistribution("orchestrator"), name="orchestrator") - .register_executor(lambda: ResourceRequester("resource_requester"), name="resource_requester") - .register_executor(lambda: PolicyChecker("policy_checker"), name="policy_checker") - .register_executor(lambda: ResultCollector("result_collector"), name="result_collector") - .add_edge("orchestrator", "resource_requester") - .add_edge("orchestrator", "policy_checker") - .add_edge("resource_requester", "result_collector") - .add_edge("policy_checker", "result_collector") - .add_edge("orchestrator", "result_collector") # For request count + WorkflowBuilder(start_executor=orchestrator) + .add_edge(orchestrator, resource_requester) + .add_edge(orchestrator, policy_checker) + .add_edge(resource_requester, result_collector) + .add_edge(policy_checker, result_collector) + .add_edge(orchestrator, result_collector) # For request count .build() ) @@ -287,25 +288,22 @@ class PolicyEngine(Executor): async def main() -> None: # Build the main workflow + resource_allocator = ResourceAllocator("resource_allocator") + policy_engine = PolicyEngine("policy_engine") + sub_workflow_executor = WorkflowExecutor( + build_resource_request_distribution_workflow(), + "sub_workflow_executor", + # Setting allow_direct_output=True to let the sub-workflow output directly. + # This is because the sub-workflow is the both the entry point and the exit + # point of the main workflow. + allow_direct_output=True, + ) main_workflow = ( - WorkflowBuilder(start_executor="sub_workflow_executor") - .register_executor(lambda: ResourceAllocator("resource_allocator"), name="resource_allocator") - .register_executor(lambda: PolicyEngine("policy_engine"), name="policy_engine") - .register_executor( - lambda: WorkflowExecutor( - build_resource_request_distribution_workflow(), - "sub_workflow_executor", - # Setting allow_direct_output=True to let the sub-workflow output directly. - # This is because the sub-workflow is the both the entry point and the exit - # point of the main workflow. - allow_direct_output=True, - ), - name="sub_workflow_executor", - ) - .add_edge("sub_workflow_executor", "resource_allocator") - .add_edge("resource_allocator", "sub_workflow_executor") - .add_edge("sub_workflow_executor", "policy_engine") - .add_edge("policy_engine", "sub_workflow_executor") + WorkflowBuilder(start_executor=sub_workflow_executor) + .add_edge(sub_workflow_executor, resource_allocator) + .add_edge(resource_allocator, sub_workflow_executor) + .add_edge(sub_workflow_executor, policy_engine) + .add_edge(policy_engine, sub_workflow_executor) .build() ) diff --git a/python/samples/getting_started/workflows/composition/sub_workflow_request_interception.py b/python/samples/getting_started/workflows/composition/sub_workflow_request_interception.py index b5fe3fb7b4..7324ecd5c7 100644 --- a/python/samples/getting_started/workflows/composition/sub_workflow_request_interception.py +++ b/python/samples/getting_started/workflows/composition/sub_workflow_request_interception.py @@ -153,13 +153,14 @@ def build_email_address_validation_workflow() -> Workflow: ) # Build the workflow + email_sanitizer = EmailSanitizer(id="email_sanitizer") + email_format_validator = EmailFormatValidator(id="email_format_validator") + domain_validator = DomainValidator(id="domain_validator") + return ( - WorkflowBuilder(start_executor="email_sanitizer") - .register_executor(lambda: EmailSanitizer(id="email_sanitizer"), name="email_sanitizer") - .register_executor(lambda: EmailFormatValidator(id="email_format_validator"), name="email_format_validator") - .register_executor(lambda: DomainValidator(id="domain_validator"), name="domain_validator") - .add_edge("email_sanitizer", "email_format_validator") - .add_edge("email_format_validator", "domain_validator") + WorkflowBuilder(start_executor=email_sanitizer) + .add_edge(email_sanitizer, email_format_validator) + .add_edge(email_format_validator, domain_validator) .build() ) @@ -268,20 +269,15 @@ async def main() -> None: approved_domains = {"example.com", "company.com"} # Build the main workflow + smart_email_orchestrator = SmartEmailOrchestrator(id="smart_email_orchestrator", approved_domains=approved_domains) + email_delivery = EmailDelivery(id="email_delivery") + email_validation_workflow = WorkflowExecutor(build_email_address_validation_workflow(), id="email_validation_workflow") + workflow = ( - WorkflowBuilder(start_executor="smart_email_orchestrator") - .register_executor( - lambda: SmartEmailOrchestrator(id="smart_email_orchestrator", approved_domains=approved_domains), - name="smart_email_orchestrator", - ) - .register_executor(lambda: EmailDelivery(id="email_delivery"), name="email_delivery") - .register_executor( - lambda: WorkflowExecutor(build_email_address_validation_workflow(), id="email_validation_workflow"), - name="email_validation_workflow", - ) - .add_edge("smart_email_orchestrator", "email_validation_workflow") - .add_edge("email_validation_workflow", "smart_email_orchestrator") - .add_edge("smart_email_orchestrator", "email_delivery") + WorkflowBuilder(start_executor=smart_email_orchestrator) + .add_edge(smart_email_orchestrator, email_validation_workflow) + .add_edge(email_validation_workflow, smart_email_orchestrator) + .add_edge(smart_email_orchestrator, email_delivery) .build() ) diff --git a/python/samples/getting_started/workflows/control-flow/edge_condition.py b/python/samples/getting_started/workflows/control-flow/edge_condition.py index 1f5636764d..c7d8cbeb2d 100644 --- a/python/samples/getting_started/workflows/control-flow/edge_condition.py +++ b/python/samples/getting_started/workflows/control-flow/edge_condition.py @@ -5,10 +5,11 @@ import os from typing import Any from agent_framework import ( # Core chat primitives used to build requests + Agent, + AgentExecutor, AgentExecutorRequest, # Input message bundle for an AgentExecutor AgentExecutorResponse, - ChatAgent, # Output from an AgentExecutor - ChatMessage, + Message, WorkflowBuilder, # Fluent builder for wiring executors and edges WorkflowContext, # Per-run context and event bus executor, # Decorator to declare a Python function as a workflow executor @@ -121,13 +122,13 @@ async def to_email_assistant_request( Extracts DetectionResult.email_content and forwards it as a user message. """ - # Bridge executor. Converts a structured DetectionResult into a ChatMessage and forwards it as a new request. + # Bridge executor. Converts a structured DetectionResult into a Message and forwards it as a new request. detection = DetectionResult.model_validate_json(response.agent_response.text) - user_msg = ChatMessage("user", text=detection.email_content) + user_msg = Message("user", text=detection.email_content) await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True)) -def create_spam_detector_agent() -> ChatAgent: +def create_spam_detector_agent() -> Agent: """Helper to create a spam detection agent.""" # AzureCliCredential uses your current az login. This avoids embedding secrets in code. return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( @@ -141,7 +142,7 @@ def create_spam_detector_agent() -> ChatAgent: ) -def create_email_assistant_agent() -> ChatAgent: +def create_email_assistant_agent() -> Agent: """Helper to create an email assistant agent.""" # AzureCliCredential uses your current az login. This avoids embedding secrets in code. return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( @@ -161,19 +162,17 @@ async def main() -> None: # If not spam, hop to a transformer that creates a new AgentExecutorRequest, # then call the email assistant, then finalize. # If spam, go directly to the spam handler and finalize. + spam_detection_agent = AgentExecutor(create_spam_detector_agent()) + email_assistant_agent = AgentExecutor(create_email_assistant_agent()) + workflow = ( - WorkflowBuilder(start_executor="spam_detection_agent") - .register_agent(create_spam_detector_agent, name="spam_detection_agent") - .register_agent(create_email_assistant_agent, name="email_assistant_agent") - .register_executor(lambda: to_email_assistant_request, name="to_email_assistant_request") - .register_executor(lambda: handle_email_response, name="send_email") - .register_executor(lambda: handle_spam_classifier_response, name="handle_spam") + WorkflowBuilder(start_executor=spam_detection_agent) # Not spam path: transform response -> request for assistant -> assistant -> send email - .add_edge("spam_detection_agent", "to_email_assistant_request", condition=get_condition(False)) - .add_edge("to_email_assistant_request", "email_assistant_agent") - .add_edge("email_assistant_agent", "send_email") + .add_edge(spam_detection_agent, to_email_assistant_request, condition=get_condition(False)) + .add_edge(to_email_assistant_request, email_assistant_agent) + .add_edge(email_assistant_agent, handle_email_response) # Spam path: send to spam handler - .add_edge("spam_detection_agent", "handle_spam", condition=get_condition(True)) + .add_edge(spam_detection_agent, handle_spam_classifier_response, condition=get_condition(True)) .build() ) @@ -186,7 +185,7 @@ async def main() -> None: # Execute the workflow. Since the start is an AgentExecutor, pass an AgentExecutorRequest. # The workflow completes when it becomes idle (no more work to do). - request = AgentExecutorRequest(messages=[ChatMessage("user", text=email)], should_respond=True) + request = AgentExecutorRequest(messages=[Message("user", text=email)], should_respond=True) events = await workflow.run(request) outputs = events.get_outputs() if outputs: diff --git a/python/samples/getting_started/workflows/control-flow/multi_selection_edge_group.py b/python/samples/getting_started/workflows/control-flow/multi_selection_edge_group.py index d2739b410e..f6c32c7882 100644 --- a/python/samples/getting_started/workflows/control-flow/multi_selection_edge_group.py +++ b/python/samples/getting_started/workflows/control-flow/multi_selection_edge_group.py @@ -9,10 +9,11 @@ from typing import Literal from uuid import uuid4 from agent_framework import ( + Agent, + AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, - ChatAgent, - ChatMessage, + Message, WorkflowBuilder, WorkflowContext, WorkflowEvent, @@ -90,7 +91,7 @@ async def store_email(email_text: str, ctx: WorkflowContext[AgentExecutorRequest ctx.set_state(CURRENT_EMAIL_ID_KEY, new_email.email_id) await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage("user", text=new_email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[Message("user", text=new_email.email_content)], should_respond=True) ) @@ -117,7 +118,7 @@ async def submit_to_email_assistant(analysis: AnalysisResult, ctx: WorkflowConte email: Email = ctx.get_state(f"{EMAIL_STATE_PREFIX}{analysis.email_id}") await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage("user", text=email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[Message("user", text=email.email_content)], should_respond=True) ) @@ -132,7 +133,7 @@ async def summarize_email(analysis: AnalysisResult, ctx: WorkflowContext[AgentEx # Only called for long NotSpam emails by selection_func email: Email = ctx.get_state(f"{EMAIL_STATE_PREFIX}{analysis.email_id}") await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage("user", text=email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[Message("user", text=email.email_content)], should_respond=True) ) @@ -179,7 +180,7 @@ async def database_access(analysis: AnalysisResult, ctx: WorkflowContext[Never, await ctx.add_event(DatabaseEvent(f"Email {analysis.email_id} saved to database.")) -def create_email_analysis_agent() -> ChatAgent: +def create_email_analysis_agent() -> Agent: """Creates the email analysis agent.""" return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( instructions=( @@ -192,7 +193,7 @@ def create_email_analysis_agent() -> ChatAgent: ) -def create_email_assistant_agent() -> ChatAgent: +def create_email_assistant_agent() -> Agent: """Creates the email assistant agent.""" return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( instructions=("You are an email assistant that helps users draft responses to emails with professionalism."), @@ -201,7 +202,7 @@ def create_email_assistant_agent() -> ChatAgent: ) -def create_email_summary_agent() -> ChatAgent: +def create_email_summary_agent() -> Agent: """Creates the email summary agent.""" return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( instructions=("You are an assistant that helps users summarize emails."), @@ -212,6 +213,10 @@ def create_email_summary_agent() -> ChatAgent: async def main() -> None: # Build the workflow + email_analysis_agent = AgentExecutor(create_email_analysis_agent()) + email_assistant_agent = AgentExecutor(create_email_assistant_agent()) + email_summary_agent = AgentExecutor(create_email_summary_agent()) + def select_targets(analysis: AnalysisResult, target_ids: list[str]) -> list[str]: # Order: [handle_spam, submit_to_email_assistant, summarize_email, handle_uncertain] handle_spam_id, submit_to_email_assistant_id, summarize_email_id, handle_uncertain_id = target_ids @@ -224,39 +229,23 @@ async def main() -> None: return targets return [handle_uncertain_id] - workflow_builder = ( - WorkflowBuilder(start_executor="store_email") - .register_agent(create_email_analysis_agent, name="email_analysis_agent") - .register_agent(create_email_assistant_agent, name="email_assistant_agent") - .register_agent(create_email_summary_agent, name="email_summary_agent") - .register_executor(lambda: store_email, name="store_email") - .register_executor(lambda: to_analysis_result, name="to_analysis_result") - .register_executor(lambda: submit_to_email_assistant, name="submit_to_email_assistant") - .register_executor(lambda: finalize_and_send, name="finalize_and_send") - .register_executor(lambda: summarize_email, name="summarize_email") - .register_executor(lambda: merge_summary, name="merge_summary") - .register_executor(lambda: handle_spam, name="handle_spam") - .register_executor(lambda: handle_uncertain, name="handle_uncertain") - .register_executor(lambda: database_access, name="database_access") - ) - workflow = ( - workflow_builder - .add_edge("store_email", "email_analysis_agent") - .add_edge("email_analysis_agent", "to_analysis_result") + WorkflowBuilder(start_executor=store_email) + .add_edge(store_email, email_analysis_agent) + .add_edge(email_analysis_agent, to_analysis_result) .add_multi_selection_edge_group( - "to_analysis_result", - ["handle_spam", "submit_to_email_assistant", "summarize_email", "handle_uncertain"], + to_analysis_result, + [handle_spam, submit_to_email_assistant, summarize_email, handle_uncertain], selection_func=select_targets, ) - .add_edge("submit_to_email_assistant", "email_assistant_agent") - .add_edge("email_assistant_agent", "finalize_and_send") - .add_edge("summarize_email", "email_summary_agent") - .add_edge("email_summary_agent", "merge_summary") + .add_edge(submit_to_email_assistant, email_assistant_agent) + .add_edge(email_assistant_agent, finalize_and_send) + .add_edge(summarize_email, email_summary_agent) + .add_edge(email_summary_agent, merge_summary) # Save to DB if short (no summary path) - .add_edge("to_analysis_result", "database_access", condition=lambda r: r.email_length <= LONG_EMAIL_THRESHOLD) + .add_edge(to_analysis_result, database_access, condition=lambda r: r.email_length <= LONG_EMAIL_THRESHOLD) # Save to DB with summary when long - .add_edge("merge_summary", "database_access") + .add_edge(merge_summary, database_access) .build() ) diff --git a/python/samples/getting_started/workflows/control-flow/sequential_executors.py b/python/samples/getting_started/workflows/control-flow/sequential_executors.py index bae05bf302..77b33c5af6 100644 --- a/python/samples/getting_started/workflows/control-flow/sequential_executors.py +++ b/python/samples/getting_started/workflows/control-flow/sequential_executors.py @@ -62,11 +62,12 @@ async def main() -> None: """Build a two step sequential workflow and run it with streaming to observe events.""" # Step 1: Build the workflow graph. # Order matters. We connect upper_case_executor -> reverse_text_executor and set the start. + upper_case_executor = UpperCaseExecutor(id="upper_case_executor") + reverse_text_executor = ReverseTextExecutor(id="reverse_text_executor") + workflow = ( - WorkflowBuilder(start_executor="upper_case_executor") - .register_executor(lambda: UpperCaseExecutor(id="upper_case_executor"), name="upper_case_executor") - .register_executor(lambda: ReverseTextExecutor(id="reverse_text_executor"), name="reverse_text_executor") - .add_edge("upper_case_executor", "reverse_text_executor") + WorkflowBuilder(start_executor=upper_case_executor) + .add_edge(upper_case_executor, reverse_text_executor) .build() ) diff --git a/python/samples/getting_started/workflows/control-flow/sequential_streaming.py b/python/samples/getting_started/workflows/control-flow/sequential_streaming.py index 86ad69652a..40244499ed 100644 --- a/python/samples/getting_started/workflows/control-flow/sequential_streaming.py +++ b/python/samples/getting_started/workflows/control-flow/sequential_streaming.py @@ -56,10 +56,8 @@ async def main(): # Step 1: Build the workflow with the defined edges. # Order matters. upper_case_executor runs first, then reverse_text_executor. workflow = ( - WorkflowBuilder(start_executor="upper_case_executor") - .register_executor(lambda: to_upper_case, name="upper_case_executor") - .register_executor(lambda: reverse_text, name="reverse_text_executor") - .add_edge("upper_case_executor", "reverse_text_executor") + WorkflowBuilder(start_executor=to_upper_case) + .add_edge(to_upper_case, reverse_text) .build() ) diff --git a/python/samples/getting_started/workflows/control-flow/simple_loop.py b/python/samples/getting_started/workflows/control-flow/simple_loop.py index 21e7907a5f..f0232863bc 100644 --- a/python/samples/getting_started/workflows/control-flow/simple_loop.py +++ b/python/samples/getting_started/workflows/control-flow/simple_loop.py @@ -4,11 +4,12 @@ import asyncio from enum import Enum from agent_framework import ( + Agent, + AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, - ChatAgent, - ChatMessage, Executor, + Message, WorkflowBuilder, WorkflowContext, handler, @@ -94,7 +95,7 @@ class SubmitToJudgeAgent(Executor): f"Target: {self._target}\nGuess: {guess}\nResponse:" ) await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage("user", text=prompt)], should_respond=True), + AgentExecutorRequest(messages=[Message("user", text=prompt)], should_respond=True), target_id=self._judge_agent_id, ) @@ -113,7 +114,7 @@ class ParseJudgeResponse(Executor): await ctx.send_message(NumberSignal.BELOW) -def create_judge_agent() -> ChatAgent: +def create_judge_agent() -> Agent: """Create a judge agent that evaluates guesses.""" return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( instructions=("You strictly respond with one of: MATCHED, ABOVE, BELOW based on the given target and guess."), @@ -125,16 +126,17 @@ async def main(): """Main function to run the workflow.""" # Step 1: Build the workflow with the defined edges. # This time we are creating a loop in the workflow. + guess_number = GuessNumberExecutor((1, 100), "guess_number") + judge_agent = AgentExecutor(create_judge_agent()) + submit_judge = SubmitToJudgeAgent(judge_agent_id="judge_agent", target=30) + parse_judge = ParseJudgeResponse(id="parse_judge") + workflow = ( - WorkflowBuilder(start_executor="guess_number") - .register_executor(lambda: GuessNumberExecutor((1, 100), "guess_number"), name="guess_number") - .register_agent(create_judge_agent, name="judge_agent") - .register_executor(lambda: SubmitToJudgeAgent(judge_agent_id="judge_agent", target=30), name="submit_judge") - .register_executor(lambda: ParseJudgeResponse(id="parse_judge"), name="parse_judge") - .add_edge("guess_number", "submit_judge") - .add_edge("submit_judge", "judge_agent") - .add_edge("judge_agent", "parse_judge") - .add_edge("parse_judge", "guess_number") + WorkflowBuilder(start_executor=guess_number) + .add_edge(guess_number, submit_judge) + .add_edge(submit_judge, judge_agent) + .add_edge(judge_agent, parse_judge) + .add_edge(parse_judge, guess_number) .build() ) diff --git a/python/samples/getting_started/workflows/control-flow/switch_case_edge_group.py b/python/samples/getting_started/workflows/control-flow/switch_case_edge_group.py index 640119347c..43c5a2354d 100644 --- a/python/samples/getting_started/workflows/control-flow/switch_case_edge_group.py +++ b/python/samples/getting_started/workflows/control-flow/switch_case_edge_group.py @@ -7,12 +7,13 @@ from typing import Any, Literal from uuid import uuid4 from agent_framework import ( # Core chat primitives used to form LLM requests + Agent, + AgentExecutor, AgentExecutorRequest, # Message bundle sent to an AgentExecutor AgentExecutorResponse, # Result returned by an AgentExecutor Case, - ChatAgent, # Case entry for a switch-case edge group - ChatMessage, Default, # Default branch when no cases match + Message, WorkflowBuilder, # Fluent builder for assembling the graph WorkflowContext, # Per-run context and event bus executor, # Decorator to turn a function into a workflow executor @@ -98,7 +99,7 @@ async def store_email(email_text: str, ctx: WorkflowContext[AgentExecutorRequest # Kick off the detector by forwarding the email as a user message to the spam_detection_agent. await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage("user", text=new_email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[Message("user", text=new_email.email_content)], should_respond=True) ) @@ -119,7 +120,7 @@ async def submit_to_email_assistant(detection: DetectionResult, ctx: WorkflowCon # Load the original content from workflow state using the id carried in DetectionResult. email: Email = ctx.get_state(f"{EMAIL_STATE_PREFIX}{detection.email_id}") await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage("user", text=email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[Message("user", text=email.email_content)], should_respond=True) ) @@ -151,7 +152,7 @@ async def handle_uncertain(detection: DetectionResult, ctx: WorkflowContext[Neve raise RuntimeError("This executor should only handle Uncertain messages.") -def create_spam_detection_agent() -> ChatAgent: +def create_spam_detection_agent() -> Agent: """Create and return the spam detection agent.""" return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( instructions=( @@ -165,7 +166,7 @@ def create_spam_detection_agent() -> ChatAgent: ) -def create_email_assistant_agent() -> ChatAgent: +def create_email_assistant_agent() -> Agent: """Create and return the email assistant agent.""" return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( instructions=("You are an email assistant that helps users draft responses to emails with professionalism."), @@ -178,28 +179,23 @@ async def main(): """Main function to run the workflow.""" # Build workflow: store -> detection agent -> to_detection_result -> switch (NotSpam or Spam or Default). # The switch-case group evaluates cases in order, then falls back to Default when none match. + spam_detection_agent = AgentExecutor(create_spam_detection_agent()) + email_assistant_agent = AgentExecutor(create_email_assistant_agent()) + workflow = ( - WorkflowBuilder(start_executor="store_email") - .register_agent(create_spam_detection_agent, name="spam_detection_agent") - .register_agent(create_email_assistant_agent, name="email_assistant_agent") - .register_executor(lambda: store_email, name="store_email") - .register_executor(lambda: to_detection_result, name="to_detection_result") - .register_executor(lambda: submit_to_email_assistant, name="submit_to_email_assistant") - .register_executor(lambda: finalize_and_send, name="finalize_and_send") - .register_executor(lambda: handle_spam, name="handle_spam") - .register_executor(lambda: handle_uncertain, name="handle_uncertain") - .add_edge("store_email", "spam_detection_agent") - .add_edge("spam_detection_agent", "to_detection_result") + WorkflowBuilder(start_executor=store_email) + .add_edge(store_email, spam_detection_agent) + .add_edge(spam_detection_agent, to_detection_result) .add_switch_case_edge_group( - "to_detection_result", + to_detection_result, [ - Case(condition=get_case("NotSpam"), target="submit_to_email_assistant"), - Case(condition=get_case("Spam"), target="handle_spam"), - Default(target="handle_uncertain"), + Case(condition=get_case("NotSpam"), target=submit_to_email_assistant), + Case(condition=get_case("Spam"), target=handle_spam), + Default(target=handle_uncertain), ], ) - .add_edge("submit_to_email_assistant", "email_assistant_agent") - .add_edge("email_assistant_agent", "finalize_and_send") + .add_edge(submit_to_email_assistant, email_assistant_agent) + .add_edge(email_assistant_agent, finalize_and_send) .build() ) diff --git a/python/samples/getting_started/workflows/control-flow/workflow_cancellation.py b/python/samples/getting_started/workflows/control-flow/workflow_cancellation.py index d553331fad..5eefbf0c65 100644 --- a/python/samples/getting_started/workflows/control-flow/workflow_cancellation.py +++ b/python/samples/getting_started/workflows/control-flow/workflow_cancellation.py @@ -51,12 +51,9 @@ async def step3(text: str, ctx: WorkflowContext[Never, str]) -> None: def build_workflow(): """Build a simple 3-step sequential workflow (~6 seconds total).""" return ( - WorkflowBuilder(start_executor="step1") - .register_executor(lambda: step1, name="step1") - .register_executor(lambda: step2, name="step2") - .register_executor(lambda: step3, name="step3") - .add_edge("step1", "step2") - .add_edge("step2", "step3") + WorkflowBuilder(start_executor=step1) + .add_edge(step1, step2) + .add_edge(step2, step3) .build() ) diff --git a/python/samples/getting_started/workflows/declarative/customer_support/main.py b/python/samples/getting_started/workflows/declarative/customer_support/main.py index b06633524f..7b47fa2930 100644 --- a/python/samples/getting_started/workflows/declarative/customer_support/main.py +++ b/python/samples/getting_started/workflows/declarative/customer_support/main.py @@ -164,43 +164,43 @@ async def main() -> None: plugin = TicketingPlugin() # Create Azure OpenAI client - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) # Create agents with structured outputs - self_service_agent = chat_client.as_agent( + self_service_agent = client.as_agent( name="SelfServiceAgent", instructions=SELF_SERVICE_INSTRUCTIONS, default_options={"response_format": SelfServiceResponse}, ) - ticketing_agent = chat_client.as_agent( + ticketing_agent = client.as_agent( name="TicketingAgent", instructions=TICKETING_INSTRUCTIONS, tools=plugin.get_functions(), default_options={"response_format": TicketingResponse}, ) - routing_agent = chat_client.as_agent( + routing_agent = client.as_agent( name="TicketRoutingAgent", instructions=TICKET_ROUTING_INSTRUCTIONS, tools=[plugin.get_ticket], default_options={"response_format": RoutingResponse}, ) - windows_support_agent = chat_client.as_agent( + windows_support_agent = client.as_agent( name="WindowsSupportAgent", instructions=WINDOWS_SUPPORT_INSTRUCTIONS, tools=[plugin.get_ticket], default_options={"response_format": SupportResponse}, ) - resolution_agent = chat_client.as_agent( + resolution_agent = client.as_agent( name="TicketResolutionAgent", instructions=RESOLUTION_INSTRUCTIONS, tools=[plugin.resolve_ticket], ) - escalation_agent = chat_client.as_agent( + escalation_agent = client.as_agent( name="TicketEscalationAgent", instructions=ESCALATION_INSTRUCTIONS, tools=[plugin.get_ticket, plugin.send_notification], diff --git a/python/samples/getting_started/workflows/declarative/deep_research/main.py b/python/samples/getting_started/workflows/declarative/deep_research/main.py index 3e4ecf7d19..d949a210f9 100644 --- a/python/samples/getting_started/workflows/declarative/deep_research/main.py +++ b/python/samples/getting_started/workflows/declarative/deep_research/main.py @@ -122,41 +122,41 @@ class ManagerResponse(BaseModel): async def main() -> None: """Run the deep research workflow.""" # Create Azure OpenAI client - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) # Create agents - research_agent = chat_client.as_agent( + research_agent = client.as_agent( name="ResearchAgent", instructions=RESEARCH_INSTRUCTIONS, ) - planner_agent = chat_client.as_agent( + planner_agent = client.as_agent( name="PlannerAgent", instructions=PLANNER_INSTRUCTIONS, ) - manager_agent = chat_client.as_agent( + manager_agent = client.as_agent( name="ManagerAgent", instructions=MANAGER_INSTRUCTIONS, default_options={"response_format": ManagerResponse}, ) - summary_agent = chat_client.as_agent( + summary_agent = client.as_agent( name="SummaryAgent", instructions=SUMMARY_INSTRUCTIONS, ) - knowledge_agent = chat_client.as_agent( + knowledge_agent = client.as_agent( name="KnowledgeAgent", instructions=KNOWLEDGE_INSTRUCTIONS, ) - coder_agent = chat_client.as_agent( + coder_agent = client.as_agent( name="CoderAgent", instructions=CODER_INSTRUCTIONS, ) - weather_agent = chat_client.as_agent( + weather_agent = client.as_agent( name="WeatherAgent", instructions=WEATHER_INSTRUCTIONS, ) diff --git a/python/samples/getting_started/workflows/declarative/function_tools/README.md b/python/samples/getting_started/workflows/declarative/function_tools/README.md index 42f3dc6497..78e7cf361e 100644 --- a/python/samples/getting_started/workflows/declarative/function_tools/README.md +++ b/python/samples/getting_started/workflows/declarative/function_tools/README.md @@ -72,8 +72,8 @@ Session Complete ```python # Create the agent with tools -chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) -menu_agent = chat_client.as_agent( +client = AzureOpenAIChatClient(credential=AzureCliCredential()) +menu_agent = client.as_agent( name="MenuAgent", instructions="You are a helpful restaurant menu assistant...", tools=[get_menu, get_specials, get_item_price], diff --git a/python/samples/getting_started/workflows/declarative/function_tools/main.py b/python/samples/getting_started/workflows/declarative/function_tools/main.py index 6e4b3f272c..056cf419a4 100644 --- a/python/samples/getting_started/workflows/declarative/function_tools/main.py +++ b/python/samples/getting_started/workflows/declarative/function_tools/main.py @@ -62,8 +62,8 @@ def get_item_price(name: Annotated[str, Field(description="Menu item name")]) -> async def main(): # Create agent with tools - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - menu_agent = chat_client.as_agent( + client = AzureOpenAIChatClient(credential=AzureCliCredential()) + menu_agent = client.as_agent( name="MenuAgent", instructions="Answer questions about menu items, specials, and prices.", tools=[get_menu, get_specials, get_item_price], diff --git a/python/samples/getting_started/workflows/declarative/marketing/main.py b/python/samples/getting_started/workflows/declarative/marketing/main.py index 2f5e999aa7..7e5b5ec7c2 100644 --- a/python/samples/getting_started/workflows/declarative/marketing/main.py +++ b/python/samples/getting_started/workflows/declarative/marketing/main.py @@ -49,17 +49,17 @@ Return the final polished version.""" async def main() -> None: """Run the marketing workflow with real Azure AI agents.""" - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - analyst_agent = chat_client.as_agent( + analyst_agent = client.as_agent( name="AnalystAgent", instructions=ANALYST_INSTRUCTIONS, ) - writer_agent = chat_client.as_agent( + writer_agent = client.as_agent( name="WriterAgent", instructions=WRITER_INSTRUCTIONS, ) - editor_agent = chat_client.as_agent( + editor_agent = client.as_agent( name="EditorAgent", instructions=EDITOR_INSTRUCTIONS, ) diff --git a/python/samples/getting_started/workflows/declarative/student_teacher/main.py b/python/samples/getting_started/workflows/declarative/student_teacher/main.py index ec06c4fc7d..28c9ab0446 100644 --- a/python/samples/getting_started/workflows/declarative/student_teacher/main.py +++ b/python/samples/getting_started/workflows/declarative/student_teacher/main.py @@ -51,15 +51,15 @@ Focus on building understanding, not just getting the right answer.""" async def main() -> None: """Run the student-teacher workflow with real Azure AI agents.""" # Create chat client - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) # Create student and teacher agents - student_agent = chat_client.as_agent( + student_agent = client.as_agent( name="StudentAgent", instructions=STUDENT_INSTRUCTIONS, ) - teacher_agent = chat_client.as_agent( + teacher_agent = client.as_agent( name="TeacherAgent", instructions=TEACHER_INSTRUCTIONS, ) diff --git a/python/samples/getting_started/workflows/human-in-the-loop/agents_with_HITL.py b/python/samples/getting_started/workflows/human-in-the-loop/agents_with_HITL.py index 7923bced7a..6cf292ce4f 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/agents_with_HITL.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/agents_with_HITL.py @@ -9,8 +9,8 @@ from agent_framework import ( AgentExecutorResponse, AgentResponse, AgentResponseUpdate, - ChatMessage, Executor, + Message, WorkflowBuilder, WorkflowContext, WorkflowEvent, @@ -47,7 +47,7 @@ class DraftFeedbackRequest: """Payload sent for human review.""" prompt: str = "" - conversation: list[ChatMessage] = field(default_factory=lambda: []) + conversation: list[Message] = field(default_factory=lambda: []) class Coordinator(Executor): @@ -71,7 +71,7 @@ class Coordinator(Executor): # Writer agent response; request human feedback. # Preserve the full conversation so that the final editor has context. - conversation: list[ChatMessage] + conversation: list[Message] if draft.full_conversation is not None: conversation = list(draft.full_conversation) else: @@ -100,7 +100,7 @@ class Coordinator(Executor): # Human approved the draft as-is; forward it unchanged. await ctx.send_message( AgentExecutorRequest( - messages=original_request.conversation + [ChatMessage("user", text="The draft is approved as-is.")], + messages=original_request.conversation + [Message("user", text="The draft is approved as-is.")], should_respond=True, ), target_id=self.final_editor_name, @@ -108,14 +108,14 @@ class Coordinator(Executor): return # Human provided feedback; prompt the writer to revise. - conversation: list[ChatMessage] = list(original_request.conversation) + conversation: list[Message] = list(original_request.conversation) instruction = ( "A human reviewer shared the following guidance:\n" f"{note or 'No specific guidance provided.'}\n\n" "Rewrite the draft from the previous assistant message into a polished final version. " "Keep the response under 120 words and reflect any requested tone adjustments." ) - conversation.append(ChatMessage("user", text=instruction)) + conversation.append(Message("user", text=instruction)) await ctx.send_message( AgentExecutorRequest(messages=conversation, should_respond=True), target_id=self.writer_name ) diff --git a/python/samples/getting_started/workflows/human-in-the-loop/concurrent_request_info.py b/python/samples/getting_started/workflows/human-in-the-loop/concurrent_request_info.py index fbc996038c..56b3a49a99 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/concurrent_request_info.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/concurrent_request_info.py @@ -27,7 +27,7 @@ from typing import Any from agent_framework import ( AgentExecutorResponse, - ChatMessage, + Message, WorkflowEvent, ) from agent_framework.azure import AzureOpenAIChatClient @@ -76,7 +76,7 @@ async def aggregate_with_synthesis(results: list[AgentExecutorResponse]) -> Any: # Build prompt with human guidance if provided guidance_text = f"\n\nHuman guidance: {human_guidance}" if human_guidance else "" - system_msg = ChatMessage( + system_msg = Message( "system", text=( "You are a synthesis expert. Consolidate the following analyst perspectives " @@ -84,7 +84,7 @@ async def aggregate_with_synthesis(results: list[AgentExecutorResponse]) -> Any: "prioritize aspects as directed." ), ) - user_msg = ChatMessage("user", text="\n\n".join(expert_sections) + guidance_text) + user_msg = Message("user", text="\n\n".join(expert_sections) + guidance_text) response = await _chat_client.get_response([system_msg, user_msg]) return response.messages[-1].text if response.messages else "" diff --git a/python/samples/getting_started/workflows/human-in-the-loop/group_chat_request_info.py b/python/samples/getting_started/workflows/human-in-the-loop/group_chat_request_info.py index 6a400a5bab..85417a0f91 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/group_chat_request_info.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/group_chat_request_info.py @@ -28,7 +28,7 @@ from typing import cast from agent_framework import ( AgentExecutorResponse, - ChatMessage, + Message, WorkflowEvent, ) from agent_framework.azure import AzureOpenAIChatClient @@ -51,7 +51,7 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str print("=" * 60) print("Final discussion summary:") # To make the type checker happy, we cast event.data to the expected type - outputs = cast(list[ChatMessage], event.data) + outputs = cast(list[Message], event.data) for msg in outputs: speaker = msg.author_name or msg.role print(f"[{speaker}]: {msg.text}") @@ -91,10 +91,10 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) # Create agents for a group discussion - optimist = chat_client.as_agent( + optimist = client.as_agent( name="optimist", instructions=( "You are an optimistic team member. You see opportunities and potential " @@ -103,7 +103,7 @@ async def main() -> None: ), ) - pragmatist = chat_client.as_agent( + pragmatist = client.as_agent( name="pragmatist", instructions=( "You are a pragmatic team member. You focus on practical implementation " @@ -112,7 +112,7 @@ async def main() -> None: ), ) - creative = chat_client.as_agent( + creative = client.as_agent( name="creative", instructions=( "You are a creative team member. You propose innovative solutions and " @@ -122,7 +122,7 @@ async def main() -> None: ) # Orchestrator coordinates the discussion - orchestrator = chat_client.as_agent( + orchestrator = client.as_agent( name="orchestrator", instructions=( "You are a discussion manager coordinating a team conversation between participants. " diff --git a/python/samples/getting_started/workflows/human-in-the-loop/guessing_game_with_human_input.py b/python/samples/getting_started/workflows/human-in-the-loop/guessing_game_with_human_input.py index fcadfe1575..d6b8161f98 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/guessing_game_with_human_input.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/guessing_game_with_human_input.py @@ -8,8 +8,8 @@ from agent_framework import ( AgentExecutorRequest, AgentExecutorResponse, AgentResponseUpdate, - ChatMessage, Executor, + Message, WorkflowBuilder, WorkflowContext, WorkflowEvent, @@ -84,7 +84,7 @@ class TurnManager(Executor): - Input is a simple starter token (ignored here). - Output is an AgentExecutorRequest that triggers the agent to produce a guess. """ - user = ChatMessage("user", text="Start by making your first guess.") + user = Message("user", text="Start by making your first guess.") await ctx.send_message(AgentExecutorRequest(messages=[user], should_respond=True)) @handler @@ -136,7 +136,7 @@ class TurnManager(Executor): f"Feedback: {reply}. Your last guess was {last_guess}. " f"Use this feedback to adjust and make your next guess (1-10)." ) - user_msg = ChatMessage("user", text=feedback_text) + user_msg = Message("user", text=feedback_text) await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True)) diff --git a/python/samples/getting_started/workflows/human-in-the-loop/sequential_request_info.py b/python/samples/getting_started/workflows/human-in-the-loop/sequential_request_info.py index 503f016a71..eb3578c6b0 100644 --- a/python/samples/getting_started/workflows/human-in-the-loop/sequential_request_info.py +++ b/python/samples/getting_started/workflows/human-in-the-loop/sequential_request_info.py @@ -27,7 +27,7 @@ from typing import cast from agent_framework import ( AgentExecutorResponse, - ChatMessage, + Message, WorkflowEvent, ) from agent_framework.azure import AzureOpenAIChatClient @@ -49,7 +49,7 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str print("WORKFLOW COMPLETE") print("=" * 60) print("Final output:") - outputs = cast(list[ChatMessage], event.data) + outputs = cast(list[Message], event.data) for message in outputs: print(f"[{message.author_name or message.role}]: {message.text}") @@ -88,15 +88,15 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + client = AzureOpenAIChatClient(credential=AzureCliCredential()) # Create agents for a sequential document review workflow - drafter = chat_client.as_agent( + drafter = client.as_agent( name="drafter", instructions=("You are a document drafter. When given a topic, create a brief draft (2-3 sentences)."), ) - editor = chat_client.as_agent( + editor = client.as_agent( name="editor", instructions=( "You are an editor. Review the draft and make improvements. " @@ -104,7 +104,7 @@ async def main() -> None: ), ) - finalizer = chat_client.as_agent( + finalizer = client.as_agent( name="finalizer", instructions=( "You are a finalizer. Take the edited content and create a polished final version. " diff --git a/python/samples/getting_started/workflows/parallelism/aggregate_results_of_different_types.py b/python/samples/getting_started/workflows/parallelism/aggregate_results_of_different_types.py index 6338b35c04..c84213b007 100644 --- a/python/samples/getting_started/workflows/parallelism/aggregate_results_of_different_types.py +++ b/python/samples/getting_started/workflows/parallelism/aggregate_results_of_different_types.py @@ -72,14 +72,15 @@ class Aggregator(Executor): async def main() -> None: # 1) Build a simple fan out and fan in workflow + dispatcher = Dispatcher(id="dispatcher") + average = Average(id="average") + summation = Sum(id="summation") + aggregator = Aggregator(id="aggregator") + workflow = ( - WorkflowBuilder(start_executor="dispatcher") - .register_executor(lambda: Dispatcher(id="dispatcher"), name="dispatcher") - .register_executor(lambda: Average(id="average"), name="average") - .register_executor(lambda: Sum(id="summation"), name="summation") - .register_executor(lambda: Aggregator(id="aggregator"), name="aggregator") - .add_fan_out_edges("dispatcher", ["average", "summation"]) - .add_fan_in_edges(["average", "summation"], "aggregator") + WorkflowBuilder(start_executor=dispatcher) + .add_fan_out_edges(dispatcher, [average, summation]) + .add_fan_in_edges([average, summation], aggregator) .build() ) diff --git a/python/samples/getting_started/workflows/parallelism/fan_out_fan_in_edges.py b/python/samples/getting_started/workflows/parallelism/fan_out_fan_in_edges.py index bb359262db..1dd78a1d76 100644 --- a/python/samples/getting_started/workflows/parallelism/fan_out_fan_in_edges.py +++ b/python/samples/getting_started/workflows/parallelism/fan_out_fan_in_edges.py @@ -4,11 +4,11 @@ import asyncio from dataclasses import dataclass from agent_framework import ( + AgentExecutor, # Wraps a ChatAgent as an Executor for use in workflows AgentExecutorRequest, # The message bundle sent to an AgentExecutor AgentExecutorResponse, # The structured result returned by an AgentExecutor - ChatAgent, # Tracing event for agent execution steps - ChatMessage, # Chat message structure Executor, # Base class for custom Python executors + Message, # Chat message structure WorkflowBuilder, # Fluent builder for wiring the workflow graph WorkflowContext, # Per run context and event bus handler, # Decorator to mark an Executor method as invokable @@ -41,7 +41,7 @@ class DispatchToExperts(Executor): @handler async def dispatch(self, prompt: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None: # Wrap the incoming prompt as a user message for each expert and request a response. - initial_message = ChatMessage("user", text=prompt) + initial_message = Message("user", text=prompt) await ctx.send_message(AgentExecutorRequest(messages=[initial_message], should_respond=True)) @@ -87,50 +87,44 @@ class AggregateInsights(Executor): await ctx.yield_output(consolidated) -def create_researcher_agent() -> ChatAgent: - """Creates a research domain expert agent.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=( - "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," - " opportunities, and risks." - ), - name="researcher", - ) - - -def create_marketer_agent() -> ChatAgent: - """Creates a marketing domain expert agent.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=( - "You're a creative marketing strategist. Craft compelling value propositions and target messaging" - " aligned to the prompt." - ), - name="marketer", - ) - - -def create_legal_agent() -> ChatAgent: - """Creates a legal/compliance domain expert agent.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=( - "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" - " based on the prompt." - ), - name="legal", - ) - - async def main() -> None: - # 1) Build a simple fan out and fan in workflow + # 1) Create executor and agent instances + dispatcher = DispatchToExperts(id="dispatcher") + aggregator = AggregateInsights(id="aggregator") + + researcher = AgentExecutor( + AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + instructions=( + "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," + " opportunities, and risks." + ), + name="researcher", + ) + ) + marketer = AgentExecutor( + AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + instructions=( + "You're a creative marketing strategist. Craft compelling value propositions and target messaging" + " aligned to the prompt." + ), + name="marketer", + ) + ) + legal = AgentExecutor( + AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + instructions=( + "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" + " based on the prompt." + ), + name="legal", + ) + ) + + # 2) Build a simple fan out and fan in workflow workflow = ( - WorkflowBuilder(start_executor="dispatcher") - .register_agent(create_researcher_agent, name="researcher") - .register_agent(create_marketer_agent, name="marketer") - .register_agent(create_legal_agent, name="legal") - .register_executor(lambda: DispatchToExperts(id="dispatcher"), name="dispatcher") - .register_executor(lambda: AggregateInsights(id="aggregator"), name="aggregator") - .add_fan_out_edges("dispatcher", ["researcher", "marketer", "legal"]) # Parallel branches - .add_fan_in_edges(["researcher", "marketer", "legal"], "aggregator") # Join at the aggregator + WorkflowBuilder(start_executor=dispatcher) + .add_fan_out_edges(dispatcher, [researcher, marketer, legal]) # Parallel branches + .add_fan_in_edges([researcher, marketer, legal], aggregator) # Join at the aggregator .build() ) diff --git a/python/samples/getting_started/workflows/parallelism/map_reduce_and_visualization.py b/python/samples/getting_started/workflows/parallelism/map_reduce_and_visualization.py index d29fe14bfb..eeeb42d9aa 100644 --- a/python/samples/getting_started/workflows/parallelism/map_reduce_and_visualization.py +++ b/python/samples/getting_started/workflows/parallelism/map_reduce_and_visualization.py @@ -257,49 +257,31 @@ class CompletionExecutor(Executor): async def main(): """Construct the map reduce workflow, visualize it, then run it over a sample file.""" - # Step 1: Create the workflow builder and register executors. - workflow_builder = ( - WorkflowBuilder(start_executor="split_data_executor") - .register_executor(lambda: Map(id="map_executor_0"), name="map_executor_0") - .register_executor(lambda: Map(id="map_executor_1"), name="map_executor_1") - .register_executor(lambda: Map(id="map_executor_2"), name="map_executor_2") - .register_executor( - lambda: Split(["map_executor_0", "map_executor_1", "map_executor_2"], id="split_data_executor"), - name="split_data_executor", - ) - .register_executor(lambda: Reduce(id="reduce_executor_0"), name="reduce_executor_0") - .register_executor(lambda: Reduce(id="reduce_executor_1"), name="reduce_executor_1") - .register_executor(lambda: Reduce(id="reduce_executor_2"), name="reduce_executor_2") - .register_executor(lambda: Reduce(id="reduce_executor_3"), name="reduce_executor_3") - .register_executor( - lambda: Shuffle( - ["reduce_executor_0", "reduce_executor_1", "reduce_executor_2", "reduce_executor_3"], - id="shuffle_executor", - ), - name="shuffle_executor", - ) - .register_executor(lambda: CompletionExecutor(id="completion_executor"), name="completion_executor") + # Step 1: Create executor instances. + map_executor_0 = Map(id="map_executor_0") + map_executor_1 = Map(id="map_executor_1") + map_executor_2 = Map(id="map_executor_2") + split_data_executor = Split(["map_executor_0", "map_executor_1", "map_executor_2"], id="split_data_executor") + reduce_executor_0 = Reduce(id="reduce_executor_0") + reduce_executor_1 = Reduce(id="reduce_executor_1") + reduce_executor_2 = Reduce(id="reduce_executor_2") + reduce_executor_3 = Reduce(id="reduce_executor_3") + shuffle_executor = Shuffle( + ["reduce_executor_0", "reduce_executor_1", "reduce_executor_2", "reduce_executor_3"], + id="shuffle_executor", ) + completion_executor = CompletionExecutor(id="completion_executor") + + mappers = [map_executor_0, map_executor_1, map_executor_2] + reducers = [reduce_executor_0, reduce_executor_1, reduce_executor_2, reduce_executor_3] # Step 2: Build the workflow graph using fan out and fan in edges. workflow = ( - workflow_builder - .add_fan_out_edges( - "split_data_executor", - ["map_executor_0", "map_executor_1", "map_executor_2"], - ) # Split -> many mappers - .add_fan_in_edges( - ["map_executor_0", "map_executor_1", "map_executor_2"], - "shuffle_executor", - ) # All mappers -> shuffle - .add_fan_out_edges( - "shuffle_executor", - ["reduce_executor_0", "reduce_executor_1", "reduce_executor_2", "reduce_executor_3"], - ) # Shuffle -> many reducers - .add_fan_in_edges( - ["reduce_executor_0", "reduce_executor_1", "reduce_executor_2", "reduce_executor_3"], - "completion_executor", - ) # All reducers -> completion + WorkflowBuilder(start_executor=split_data_executor) + .add_fan_out_edges(split_data_executor, mappers) # Split -> many mappers + .add_fan_in_edges(mappers, shuffle_executor) # All mappers -> shuffle + .add_fan_out_edges(shuffle_executor, reducers) # Shuffle -> many reducers + .add_fan_in_edges(reducers, completion_executor) # All reducers -> completion .build() ) diff --git a/python/samples/getting_started/workflows/state-management/state_with_agents.py b/python/samples/getting_started/workflows/state-management/state_with_agents.py index 929dc40362..97b9fab240 100644 --- a/python/samples/getting_started/workflows/state-management/state_with_agents.py +++ b/python/samples/getting_started/workflows/state-management/state_with_agents.py @@ -7,10 +7,10 @@ from typing import Any from uuid import uuid4 from agent_framework import ( + Agent, AgentExecutorRequest, AgentExecutorResponse, - ChatAgent, - ChatMessage, + Message, WorkflowBuilder, WorkflowContext, executor, @@ -103,7 +103,7 @@ async def store_email(email_text: str, ctx: WorkflowContext[AgentExecutorRequest ctx.set_state(CURRENT_EMAIL_ID_KEY, new_email.email_id) await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage("user", text=new_email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[Message("user", text=new_email.email_content)], should_respond=True) ) @@ -134,7 +134,7 @@ async def submit_to_email_assistant(detection: DetectionResult, ctx: WorkflowCon # Load the original content by id from workflow state and forward it to the assistant. email: Email = ctx.get_state(f"{EMAIL_STATE_PREFIX}{detection.email_id}") await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage("user", text=email.email_content)], should_respond=True) + AgentExecutorRequest(messages=[Message("user", text=email.email_content)], should_respond=True) ) @@ -154,7 +154,7 @@ async def handle_spam(detection: DetectionResult, ctx: WorkflowContext[Never, st raise RuntimeError("This executor should only handle spam messages.") -def create_spam_detection_agent() -> ChatAgent: +def create_spam_detection_agent() -> Agent: """Creates a spam detection agent.""" return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( instructions=( @@ -167,7 +167,7 @@ def create_spam_detection_agent() -> ChatAgent: ) -def create_email_assistant_agent() -> ChatAgent: +def create_email_assistant_agent() -> Agent: """Creates an email assistant agent.""" return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( instructions=( @@ -188,21 +188,17 @@ async def main() -> None: # store_email -> spam_detection_agent -> to_detection_result -> branch: # False -> submit_to_email_assistant -> email_assistant_agent -> finalize_and_send # True -> handle_spam + spam_detection_agent = create_spam_detection_agent() + email_assistant_agent = create_email_assistant_agent() + workflow = ( - WorkflowBuilder(start_executor="store_email") - .register_agent(create_spam_detection_agent, name="spam_detection_agent") - .register_agent(create_email_assistant_agent, name="email_assistant_agent") - .register_executor(lambda: store_email, name="store_email") - .register_executor(lambda: to_detection_result, name="to_detection_result") - .register_executor(lambda: submit_to_email_assistant, name="submit_to_email_assistant") - .register_executor(lambda: finalize_and_send, name="finalize_and_send") - .register_executor(lambda: handle_spam, name="handle_spam") - .add_edge("store_email", "spam_detection_agent") - .add_edge("spam_detection_agent", "to_detection_result") - .add_edge("to_detection_result", "submit_to_email_assistant", condition=get_condition(False)) - .add_edge("to_detection_result", "handle_spam", condition=get_condition(True)) - .add_edge("submit_to_email_assistant", "email_assistant_agent") - .add_edge("email_assistant_agent", "finalize_and_send") + WorkflowBuilder(start_executor=store_email) + .add_edge(store_email, spam_detection_agent) + .add_edge(spam_detection_agent, to_detection_result) + .add_edge(to_detection_result, submit_to_email_assistant, condition=get_condition(False)) + .add_edge(to_detection_result, handle_spam, condition=get_condition(True)) + .add_edge(submit_to_email_assistant, email_assistant_agent) + .add_edge(email_assistant_agent, finalize_and_send) .build() ) diff --git a/python/samples/getting_started/workflows/state-management/workflow_kwargs.py b/python/samples/getting_started/workflows/state-management/workflow_kwargs.py index d89115463f..5125464a1a 100644 --- a/python/samples/getting_started/workflows/state-management/workflow_kwargs.py +++ b/python/samples/getting_started/workflows/state-management/workflow_kwargs.py @@ -4,7 +4,7 @@ import asyncio import json from typing import Annotated, Any, cast -from agent_framework import ChatMessage, tool +from agent_framework import Message, tool from agent_framework.openai import OpenAIChatClient from agent_framework.orchestrations import SequentialBuilder from pydantic import Field @@ -74,10 +74,10 @@ async def main() -> None: print("=" * 70) # Create chat client - chat_client = OpenAIChatClient() + client = OpenAIChatClient() # Create agent with tools that use kwargs - agent = chat_client.as_agent( + agent = client.as_agent( name="assistant", instructions=( "You are a helpful assistant. Use the available tools to help users. " @@ -121,10 +121,10 @@ async def main() -> None: stream=True, ): if event.type == "output": - output_data = cast(list[ChatMessage], event.data) + output_data = cast(list[Message], event.data) if isinstance(output_data, list): for item in output_data: - if isinstance(item, ChatMessage) and item.text: + if isinstance(item, Message) and item.text: print(f"\n[Final Answer]: {item.text}") print("\n" + "=" * 70) diff --git a/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py b/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py index 6eb6e2bc6a..34d59b62d7 100644 --- a/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py +++ b/python/samples/getting_started/workflows/tool-approval/concurrent_builder_tool_approval.py @@ -5,8 +5,8 @@ from collections.abc import AsyncIterable from typing import Annotated from agent_framework import ( - ChatMessage, Content, + Message, WorkflowEvent, tool, ) @@ -91,10 +91,10 @@ def _print_output(event: WorkflowEvent) -> None: if not event.data: raise ValueError("WorkflowEvent has no data") - if not isinstance(event.data, list) and not all(isinstance(msg, ChatMessage) for msg in event.data): - raise ValueError("WorkflowEvent data is not a list of ChatMessage") + if not isinstance(event.data, list) and not all(isinstance(msg, Message) for msg in event.data): + raise ValueError("WorkflowEvent data is not a list of Message") - messages: list[ChatMessage] = event.data # type: ignore + messages: list[Message] = event.data # type: ignore print("\n" + "-" * 60) print("Workflow completed. Aggregated results from both agents:") @@ -126,9 +126,9 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: # 3. Create two agents focused on different stocks but with the same tool sets - chat_client = OpenAIChatClient() + client = OpenAIChatClient() - microsoft_agent = chat_client.as_agent( + microsoft_agent = client.as_agent( name="MicrosoftAgent", instructions=( "You are a personal trading assistant focused on Microsoft (MSFT). " @@ -137,7 +137,7 @@ async def main() -> None: tools=[get_stock_price, get_market_sentiment, get_portfolio_balance, execute_trade], ) - google_agent = chat_client.as_agent( + google_agent = client.as_agent( name="GoogleAgent", instructions=( "You are a personal trading assistant focused on Google (GOOGL). " diff --git a/python/samples/getting_started/workflows/tool-approval/group_chat_builder_tool_approval.py b/python/samples/getting_started/workflows/tool-approval/group_chat_builder_tool_approval.py index ebabfc508f..159299b9b8 100644 --- a/python/samples/getting_started/workflows/tool-approval/group_chat_builder_tool_approval.py +++ b/python/samples/getting_started/workflows/tool-approval/group_chat_builder_tool_approval.py @@ -5,8 +5,8 @@ from collections.abc import AsyncIterable from typing import Annotated, cast from agent_framework import ( - ChatMessage, Content, + Message, WorkflowEvent, tool, ) @@ -105,7 +105,7 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str # The output of the workflow comes from the orchestrator and it's a list of messages print("\n" + "=" * 60) print("Workflow summary:") - outputs = cast(list[ChatMessage], event.data) + outputs = cast(list[Message], event.data) for msg in outputs: speaker = msg.author_name or msg.role print(f"[{speaker}]: {msg.text}") @@ -126,9 +126,9 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: # 3. Create specialized agents - chat_client = OpenAIChatClient() + client = OpenAIChatClient() - qa_engineer = chat_client.as_agent( + qa_engineer = client.as_agent( name="QAEngineer", instructions=( "You are a QA engineer responsible for running tests before deployment. " @@ -137,7 +137,7 @@ async def main() -> None: tools=[run_tests], ) - devops_engineer = chat_client.as_agent( + devops_engineer = client.as_agent( name="DevOpsEngineer", instructions=( "You are a DevOps engineer responsible for deployments. First check staging " diff --git a/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py b/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py index c203ecc084..2f7ecea0ac 100644 --- a/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py +++ b/python/samples/getting_started/workflows/tool-approval/sequential_builder_tool_approval.py @@ -5,8 +5,8 @@ from collections.abc import AsyncIterable from typing import Annotated, cast from agent_framework import ( - ChatMessage, Content, + Message, WorkflowEvent, tool, ) @@ -78,7 +78,7 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str # The output of the workflow comes from the orchestrator and it's a list of messages print("\n" + "=" * 60) print("Workflow summary:") - outputs = cast(list[ChatMessage], event.data) + outputs = cast(list[Message], event.data) for msg in outputs: speaker = msg.author_name or msg.role print(f"[{speaker}]: {msg.text}") @@ -99,8 +99,8 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str async def main() -> None: # 2. Create the agent with tools (approval mode is set per-tool via decorator) - chat_client = OpenAIChatClient() - database_agent = chat_client.as_agent( + client = OpenAIChatClient() + database_agent = client.as_agent( name="DatabaseAgent", instructions=( "You are a database assistant. You can view the database schema and execute " diff --git a/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py b/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py index a1c1086eec..e9e042020d 100644 --- a/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py +++ b/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py @@ -4,11 +4,11 @@ import asyncio from dataclasses import dataclass from agent_framework import ( + AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, - ChatAgent, - ChatMessage, Executor, + Message, WorkflowBuilder, WorkflowContext, WorkflowViz, @@ -39,7 +39,7 @@ class DispatchToExperts(Executor): @handler async def dispatch(self, prompt: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None: # Wrap the incoming prompt as a user message for each expert and request a response. - initial_message = ChatMessage("user", text=prompt) + initial_message = Message("user", text=prompt) await ctx.send_message(AgentExecutorRequest(messages=[initial_message], should_respond=True)) @@ -85,52 +85,49 @@ class AggregateInsights(Executor): await ctx.yield_output(consolidated) -def create_researcher_agent() -> ChatAgent: - """Creates a research domain expert agent.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=( - "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," - " opportunities, and risks." - ), - name="researcher", - ) - - -def create_marketer_agent() -> ChatAgent: - """Creates a marketing domain expert agent.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=( - "You're a creative marketing strategist. Craft compelling value propositions and target messaging" - " aligned to the prompt." - ), - name="marketer", - ) - - -def create_legal_agent() -> ChatAgent: - """Creates a legal domain expert agent.""" - return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( - instructions=( - "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" - " based on the prompt." - ), - name="legal", - ) - - async def main() -> None: """Build and run the concurrent workflow with visualization.""" + # Create agent instances + researcher = AgentExecutor( + AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + instructions=( + "You're an expert market and product researcher. Given a prompt, provide concise, factual insights," + " opportunities, and risks." + ), + name="researcher", + ) + ) + + marketer = AgentExecutor( + AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + instructions=( + "You're a creative marketing strategist. Craft compelling value propositions and target messaging" + " aligned to the prompt." + ), + name="marketer", + ) + ) + + legal = AgentExecutor( + AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent( + instructions=( + "You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns" + " based on the prompt." + ), + name="legal", + ) + ) + + # Create executor instances + dispatcher = DispatchToExperts(id="dispatcher") + aggregator = AggregateInsights(id="aggregator") + # Build a simple fan-out/fan-in workflow workflow = ( - WorkflowBuilder(start_executor="dispatcher") - .register_agent(create_researcher_agent, name="researcher") - .register_agent(create_marketer_agent, name="marketer") - .register_agent(create_legal_agent, name="legal") - .register_executor(lambda: DispatchToExperts(id="dispatcher"), name="dispatcher") - .register_executor(lambda: AggregateInsights(id="aggregator"), name="aggregator") - .add_fan_out_edges("dispatcher", ["researcher", "marketer", "legal"]) - .add_fan_in_edges(["researcher", "marketer", "legal"], "aggregator") + WorkflowBuilder(start_executor=dispatcher) + .add_fan_out_edges(dispatcher, [researcher, marketer, legal]) + .add_fan_in_edges([researcher, marketer, legal], aggregator) .build() ) diff --git a/python/samples/semantic-kernel-migration/README.md b/python/samples/semantic-kernel-migration/README.md index c1fa894a4c..d04239a00d 100644 --- a/python/samples/semantic-kernel-migration/README.md +++ b/python/samples/semantic-kernel-migration/README.md @@ -7,7 +7,7 @@ This gallery helps Semantic Kernel (SK) developers move to the Microsoft Agent F ## What’s Included ### Chat completion parity -- [01_basic_chat_completion.py](chat_completion/01_basic_chat_completion.py) — Minimal SK `ChatCompletionAgent` and AF `ChatAgent` conversation. +- [01_basic_chat_completion.py](chat_completion/01_basic_chat_completion.py) — Minimal SK `ChatCompletionAgent` and AF `Agent` conversation. - [02_chat_completion_with_tool.py](chat_completion/02_chat_completion_with_tool.py) — Adds a simple tool/function call in both SDKs. - [03_chat_completion_thread_and_stream.py](chat_completion/03_chat_completion_thread_and_stream.py) — Demonstrates thread reuse and streaming prompts. diff --git a/python/samples/semantic-kernel-migration/azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py b/python/samples/semantic-kernel-migration/azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py index 81c059fc90..93074bd856 100644 --- a/python/samples/semantic-kernel-migration/azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py +++ b/python/samples/semantic-kernel-migration/azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py @@ -39,18 +39,24 @@ async def run_semantic_kernel() -> None: async def run_agent_framework() -> None: - from agent_framework.azure import AzureAIAgentClient, HostedCodeInterpreterTool + from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider from azure.identity.aio import AzureCliCredential async with ( AzureCliCredential() as credential, - AzureAIAgentClient(credential=credential).as_agent( + AzureAIAgentsProvider(credential=credential) as provider, + ): + # Create a client to access hosted tool factory methods + client = AzureAIAgentClient(agents_client=provider._agents_client) + code_interpreter_tool = client.get_code_interpreter_tool() + + agent = await provider.create_agent( name="Analyst", instructions="Use the code interpreter for numeric work.", - tools=[HostedCodeInterpreterTool()], - ) as agent, - ): - # HostedCodeInterpreterTool mirrors the built-in Azure AI capability. + tools=[code_interpreter_tool], + ) + + # Code interpreter tool mirrors the built-in Azure AI capability. reply = await agent.run( "Use Python to compute 42 ** 2 and explain the result.", tool_choice="auto", diff --git a/python/samples/semantic-kernel-migration/chat_completion/01_basic_chat_completion.py b/python/samples/semantic-kernel-migration/chat_completion/01_basic_chat_completion.py index 74ecd1ecf5..63db51fb43 100644 --- a/python/samples/semantic-kernel-migration/chat_completion/01_basic_chat_completion.py +++ b/python/samples/semantic-kernel-migration/chat_completion/01_basic_chat_completion.py @@ -8,7 +8,7 @@ # uv run samples/semantic-kernel-migration/chat_completion/01_basic_chat_completion.py # Copyright (c) Microsoft. All rights reserved. -"""Basic SK ChatCompletionAgent vs Agent Framework ChatAgent. +"""Basic SK ChatCompletionAgent vs Agent Framework Agent. Both samples expect OpenAI-compatible environment variables (OPENAI_API_KEY or Azure OpenAI configuration). Update the prompts or client wiring to match your @@ -34,10 +34,10 @@ async def run_semantic_kernel() -> None: async def run_agent_framework() -> None: - """Call Agent Framework's ChatAgent created from OpenAIChatClient.""" + """Call Agent Framework's Agent created from OpenAIChatClient.""" from agent_framework.openai import OpenAIChatClient - # AF constructs a lightweight ChatAgent backed by OpenAIChatClient. + # AF constructs a lightweight Agent backed by OpenAIChatClient. chat_agent = OpenAIChatClient().as_agent( name="Support", instructions="Answer in one sentence.", diff --git a/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py b/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py index 034404990d..b5bf4c35d3 100644 --- a/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py +++ b/python/samples/semantic-kernel-migration/openai_assistant/02_openai_assistant_with_code_interpreter.py @@ -37,16 +37,19 @@ async def run_semantic_kernel() -> None: async def run_agent_framework() -> None: - from agent_framework import HostedCodeInterpreterTool from agent_framework.openai import OpenAIAssistantsClient assistants_client = OpenAIAssistantsClient() + + # Create code interpreter tool using static method + code_interpreter_tool = OpenAIAssistantsClient.get_code_interpreter_tool() + # AF exposes the same tool configuration via create_agent. async with assistants_client.as_agent( name="CodeRunner", instructions="Use the code interpreter when calculations are required.", model="gpt-4.1", - tools=[HostedCodeInterpreterTool()], + tools=[code_interpreter_tool], ) as assistant_agent: response = await assistant_agent.run( "Use Python to calculate the mean of [41, 42, 45] and explain the steps.", diff --git a/python/samples/semantic-kernel-migration/openai_responses/01_basic_responses_agent.py b/python/samples/semantic-kernel-migration/openai_responses/01_basic_responses_agent.py index 3402a2e1e3..fce6ecb6ad 100644 --- a/python/samples/semantic-kernel-migration/openai_responses/01_basic_responses_agent.py +++ b/python/samples/semantic-kernel-migration/openai_responses/01_basic_responses_agent.py @@ -35,12 +35,12 @@ async def run_semantic_kernel() -> None: async def run_agent_framework() -> None: - from agent_framework import ChatAgent + from agent_framework import Agent from agent_framework.openai import OpenAIResponsesClient - # AF ChatAgent can swap in an OpenAIResponsesClient directly. - chat_agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + # AF Agent can swap in an OpenAIResponsesClient directly. + chat_agent = Agent( + client=OpenAIResponsesClient(), instructions="Answer in one concise sentence.", name="Expert", ) diff --git a/python/samples/semantic-kernel-migration/openai_responses/02_responses_agent_with_tool.py b/python/samples/semantic-kernel-migration/openai_responses/02_responses_agent_with_tool.py index c770763bce..599367f9c5 100644 --- a/python/samples/semantic-kernel-migration/openai_responses/02_responses_agent_with_tool.py +++ b/python/samples/semantic-kernel-migration/openai_responses/02_responses_agent_with_tool.py @@ -42,7 +42,7 @@ async def run_semantic_kernel() -> None: async def run_agent_framework() -> None: - from agent_framework import ChatAgent + from agent_framework import Agent from agent_framework._tools import tool from agent_framework.openai import OpenAIResponsesClient @@ -50,8 +50,8 @@ async def run_agent_framework() -> None: async def add(a: float, b: float) -> float: return a + b - chat_agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + chat_agent = Agent( + client=OpenAIResponsesClient(), instructions="Use the add tool when math is required.", name="MathExpert", # AF registers the async function as a tool at construction. diff --git a/python/samples/semantic-kernel-migration/openai_responses/03_responses_agent_structured_output.py b/python/samples/semantic-kernel-migration/openai_responses/03_responses_agent_structured_output.py index bd37c3b33c..07d9d0b4c7 100644 --- a/python/samples/semantic-kernel-migration/openai_responses/03_responses_agent_structured_output.py +++ b/python/samples/semantic-kernel-migration/openai_responses/03_responses_agent_structured_output.py @@ -47,11 +47,11 @@ async def run_semantic_kernel() -> None: async def run_agent_framework() -> None: - from agent_framework import ChatAgent + from agent_framework import Agent from agent_framework.openai import OpenAIResponsesClient - chat_agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + chat_agent = Agent( + client=OpenAIResponsesClient(), instructions="Return launch briefs as structured JSON.", name="ProductMarketer", ) diff --git a/python/samples/semantic-kernel-migration/orchestrations/concurrent_basic.py b/python/samples/semantic-kernel-migration/orchestrations/concurrent_basic.py index 72f0c24252..7a107d31ec 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/concurrent_basic.py +++ b/python/samples/semantic-kernel-migration/orchestrations/concurrent_basic.py @@ -15,10 +15,11 @@ import asyncio from collections.abc import Sequence from typing import cast -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.orchestrations import ConcurrentBuilder from azure.identity import AzureCliCredential -from semantic_kernel.agents import Agent, ChatCompletionAgent, ConcurrentOrchestration +from semantic_kernel.agents import ChatCompletionAgent, ConcurrentOrchestration from semantic_kernel.agents.runtime import InProcessRuntime from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion from semantic_kernel.contents import ChatMessageContent @@ -83,30 +84,30 @@ def _print_semantic_kernel_outputs(outputs: Sequence[ChatMessageContent]) -> Non ###################################################################### -async def run_agent_framework_example(prompt: str) -> Sequence[list[ChatMessage]]: - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) +async def run_agent_framework_example(prompt: str) -> Sequence[list[Message]]: + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - physics = chat_client.as_agent( + physics = client.as_agent( instructions=("You are an expert in physics. Answer questions from a physics perspective."), name="physics", ) - chemistry = chat_client.as_agent( + chemistry = client.as_agent( instructions=("You are an expert in chemistry. Answer questions from a chemistry perspective."), name="chemistry", ) workflow = ConcurrentBuilder(participants=[physics, chemistry]).build() - outputs: list[list[ChatMessage]] = [] + outputs: list[list[Message]] = [] async for event in workflow.run(prompt, stream=True): if event.type == "output": - outputs.append(cast(list[ChatMessage], event.data)) + outputs.append(cast(list[Message], event.data)) return outputs -def _print_agent_framework_outputs(conversations: Sequence[Sequence[ChatMessage]]) -> None: +def _print_agent_framework_outputs(conversations: Sequence[Sequence[Message]]) -> None: if not conversations: print("No Agent Framework output.") return diff --git a/python/samples/semantic-kernel-migration/orchestrations/group_chat.py b/python/samples/semantic-kernel-migration/orchestrations/group_chat.py index 539041a537..e244bd0c01 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/group_chat.py +++ b/python/samples/semantic-kernel-migration/orchestrations/group_chat.py @@ -16,7 +16,7 @@ import sys from collections.abc import Sequence from typing import Any, cast -from agent_framework import ChatAgent, ChatMessage +from agent_framework import Agent, Message from agent_framework.azure import AzureOpenAIChatClient, AzureOpenAIResponsesClient from agent_framework.orchestrations import GroupChatBuilder from azure.identity import AzureCliCredential @@ -224,21 +224,21 @@ async def run_semantic_kernel_example(task: str) -> str: async def run_agent_framework_example(task: str) -> str: credential = AzureCliCredential() - researcher = ChatAgent( + researcher = Agent( name="Researcher", description="Collects background information and potential resources.", instructions=( "Gather concise facts or considerations that help plan a community hackathon. " "Keep your responses factual and scannable." ), - chat_client=AzureOpenAIChatClient(credential=credential), + client=AzureOpenAIChatClient(credential=credential), ) - planner = ChatAgent( + planner = Agent( name="Planner", description="Turns the collected notes into a concrete action plan.", instructions=("Propose a structured action plan that accounts for logistics, roles, and timeline."), - chat_client=AzureOpenAIResponsesClient(credential=credential), + client=AzureOpenAIResponsesClient(credential=credential), ) workflow = GroupChatBuilder( @@ -253,7 +253,7 @@ async def run_agent_framework_example(task: str) -> str: if isinstance(data, list) and len(data) > 0: # Get the final message from the conversation final_message = data[-1] - final_response = final_message.text or "" if isinstance(final_message, ChatMessage) else str(data) + final_response = final_message.text or "" if isinstance(final_message, Message) else str(data) else: final_response = str(data) return final_response diff --git a/python/samples/semantic-kernel-migration/orchestrations/handoff.py b/python/samples/semantic-kernel-migration/orchestrations/handoff.py index 3fe024a9f4..9891442369 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/handoff.py +++ b/python/samples/semantic-kernel-migration/orchestrations/handoff.py @@ -16,11 +16,11 @@ from collections.abc import AsyncIterable, Iterator, Sequence from typing import cast from agent_framework import ( - ChatMessage, + Message, WorkflowEvent, ) -from agent_framework.orchestrations import HandoffBuilder, HandoffUserInputRequest from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.orchestrations import HandoffBuilder, HandoffUserInputRequest from azure.identity import AzureCliCredential from semantic_kernel.agents import Agent, ChatCompletionAgent, HandoffOrchestration, OrchestrationHandoffs from semantic_kernel.agents.runtime import InProcessRuntime @@ -228,10 +228,10 @@ def _collect_handoff_requests(events: list[WorkflowEvent]) -> list[WorkflowEvent return requests -def _extract_final_conversation(events: list[WorkflowEvent]) -> list[ChatMessage]: +def _extract_final_conversation(events: list[WorkflowEvent]) -> list[Message]: for event in events: if event.type == "output": - data = cast(list[ChatMessage], event.data) + data = cast(list[Message], event.data) return data return [] diff --git a/python/samples/semantic-kernel-migration/orchestrations/magentic.py b/python/samples/semantic-kernel-migration/orchestrations/magentic.py index d6509fb4d7..44a8efc832 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/magentic.py +++ b/python/samples/semantic-kernel-migration/orchestrations/magentic.py @@ -15,7 +15,7 @@ import asyncio from collections.abc import Sequence from typing import cast -from agent_framework import ChatAgent, HostedCodeInterpreterTool +from agent_framework import Agent from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient from agent_framework.orchestrations import MagenticBuilder from semantic_kernel.agents import ( @@ -129,29 +129,33 @@ def _print_semantic_kernel_outputs(outputs: Sequence[ChatMessageContent]) -> Non async def run_agent_framework_example(prompt: str) -> str | None: - researcher = ChatAgent( + researcher = Agent( name="ResearcherAgent", description="Specialist in research and information gathering", instructions=( "You are a Researcher. You find information without additional computation or quantitative analysis." ), - chat_client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"), + client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"), ) - coder = ChatAgent( + # Create code interpreter tool using instance method + coder_client = OpenAIResponsesClient() + code_interpreter_tool = coder_client.get_code_interpreter_tool() + + coder = Agent( name="CoderAgent", description="A helpful assistant that writes and executes code to process and analyze data.", instructions="You solve questions using code. Please provide detailed analysis and computation process.", - chat_client=OpenAIResponsesClient(), - tools=HostedCodeInterpreterTool(), + client=coder_client, + tools=code_interpreter_tool, ) # Create a manager agent for orchestration - manager_agent = ChatAgent( + manager_agent = Agent( name="MagenticManager", description="Orchestrator that coordinates the research and coding workflow", instructions="You coordinate a team to complete complex tasks efficiently.", - chat_client=OpenAIChatClient(), + client=OpenAIChatClient(), ) workflow = MagenticBuilder(participants=[researcher, coder], manager_agent=manager_agent).build() diff --git a/python/samples/semantic-kernel-migration/orchestrations/sequential.py b/python/samples/semantic-kernel-migration/orchestrations/sequential.py index 13bfdf82a0..c678bc22b8 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/sequential.py +++ b/python/samples/semantic-kernel-migration/orchestrations/sequential.py @@ -15,7 +15,7 @@ import asyncio from collections.abc import Sequence from typing import cast -from agent_framework import ChatMessage +from agent_framework import Message from agent_framework.azure import AzureOpenAIChatClient from agent_framework.orchestrations import SequentialBuilder from azure.identity import AzureCliCredential @@ -70,25 +70,25 @@ async def sk_agent_response_callback( ###################################################################### -async def run_agent_framework_example(prompt: str) -> list[ChatMessage]: - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) +async def run_agent_framework_example(prompt: str) -> list[Message]: + client = AzureOpenAIChatClient(credential=AzureCliCredential()) - writer = chat_client.as_agent( + writer = client.as_agent( instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."), name="writer", ) - reviewer = chat_client.as_agent( + reviewer = client.as_agent( instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."), name="reviewer", ) workflow = SequentialBuilder(participants=[writer, reviewer]).build() - conversation_outputs: list[list[ChatMessage]] = [] + conversation_outputs: list[list[Message]] = [] async for event in workflow.run(prompt, stream=True): if event.type == "output": - conversation_outputs.append(cast(list[ChatMessage], event.data)) + conversation_outputs.append(cast(list[Message], event.data)) return conversation_outputs[-1] if conversation_outputs else [] @@ -112,7 +112,7 @@ async def run_semantic_kernel_example(prompt: str) -> str: await runtime.stop_when_idle() -def _format_conversation(conversation: list[ChatMessage]) -> None: +def _format_conversation(conversation: list[Message]) -> None: if not conversation: print("No Agent Framework output.") return diff --git a/python/samples/semantic-kernel-migration/processes/fan_out_fan_in_process.py b/python/samples/semantic-kernel-migration/processes/fan_out_fan_in_process.py index afca864ea7..62325d3c7b 100644 --- a/python/samples/semantic-kernel-migration/processes/fan_out_fan_in_process.py +++ b/python/samples/semantic-kernel-migration/processes/fan_out_fan_in_process.py @@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, ClassVar, cast ###################################################################### # region Agent Framework imports ###################################################################### -from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler +from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler from pydantic import BaseModel, Field ###################################################################### diff --git a/python/samples/semantic-kernel-migration/processes/nested_process.py b/python/samples/semantic-kernel-migration/processes/nested_process.py index 775647d992..8fbe66acf3 100644 --- a/python/samples/semantic-kernel-migration/processes/nested_process.py +++ b/python/samples/semantic-kernel-migration/processes/nested_process.py @@ -26,7 +26,6 @@ from agent_framework import ( WorkflowBuilder, WorkflowContext, WorkflowExecutor, - handler, ) from pydantic import BaseModel, Field diff --git a/python/tests/samples/getting_started/test_chat_client_samples.py b/python/tests/samples/getting_started/test_chat_client_samples.py index 0a699c5908..df3c18b6d5 100644 --- a/python/tests/samples/getting_started/test_chat_client_samples.py +++ b/python/tests/samples/getting_started/test_chat_client_samples.py @@ -8,28 +8,28 @@ from typing import Any import pytest from pytest import MonkeyPatch, mark, param -from samples.getting_started.chat_client.azure_ai_chat_client import ( +from samples.getting_started.client.azure_ai_chat_client import ( main as azure_ai_chat_client, ) -from samples.getting_started.chat_client.azure_assistants_client import ( +from samples.getting_started.client.azure_assistants_client import ( main as azure_assistants_client, ) -from samples.getting_started.chat_client.azure_chat_client import ( +from samples.getting_started.client.azure_chat_client import ( main as azure_chat_client, ) -from samples.getting_started.chat_client.azure_responses_client import ( +from samples.getting_started.client.azure_responses_client import ( main as azure_responses_client, ) -from samples.getting_started.chat_client.chat_response_cancellation import ( +from samples.getting_started.client.chat_response_cancellation import ( main as chat_response_cancellation, ) -from samples.getting_started.chat_client.openai_assistants_client import ( +from samples.getting_started.client.openai_assistants_client import ( main as openai_assistants_client, ) -from samples.getting_started.chat_client.openai_chat_client import ( +from samples.getting_started.client.openai_chat_client import ( main as openai_chat_client, ) -from samples.getting_started.chat_client.openai_responses_client import ( +from samples.getting_started.client.openai_responses_client import ( main as openai_responses_client, ) diff --git a/python/uv.lock b/python/uv.lock index 0a6de3f940..fac55c8e21 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -96,7 +96,7 @@ wheels = [ [[package]] name = "agent-framework" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { virtual = "." } dependencies = [ { name = "agent-framework-core", extra = ["all"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -145,7 +145,7 @@ dev = [ [[package]] name = "agent-framework-a2a" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/a2a" } dependencies = [ { name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -160,7 +160,7 @@ requires-dist = [ [[package]] name = "agent-framework-ag-ui" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/ag-ui" } dependencies = [ { name = "ag-ui-protocol", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -188,7 +188,7 @@ provides-extras = ["dev"] [[package]] name = "agent-framework-anthropic" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/anthropic" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -203,7 +203,7 @@ requires-dist = [ [[package]] name = "agent-framework-azure-ai" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/azure-ai" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -222,7 +222,7 @@ requires-dist = [ [[package]] name = "agent-framework-azure-ai-search" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/azure-ai-search" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -237,7 +237,7 @@ requires-dist = [ [[package]] name = "agent-framework-azurefunctions" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/azurefunctions" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -259,7 +259,7 @@ dev = [] [[package]] name = "agent-framework-bedrock" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/bedrock" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -276,7 +276,7 @@ requires-dist = [ [[package]] name = "agent-framework-chatkit" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/chatkit" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -291,7 +291,7 @@ requires-dist = [ [[package]] name = "agent-framework-claude" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/claude" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -306,7 +306,7 @@ requires-dist = [ [[package]] name = "agent-framework-copilotstudio" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/copilotstudio" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -321,7 +321,7 @@ requires-dist = [ [[package]] name = "agent-framework-core" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/core" } dependencies = [ { name = "azure-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -393,7 +393,7 @@ provides-extras = ["all"] [[package]] name = "agent-framework-declarative" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/declarative" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -418,7 +418,7 @@ dev = [{ name = "types-pyyaml" }] [[package]] name = "agent-framework-devui" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/devui" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -454,7 +454,7 @@ provides-extras = ["dev", "all"] [[package]] name = "agent-framework-durabletask" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/durabletask" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -481,7 +481,7 @@ dev = [{ name = "types-python-dateutil", specifier = ">=2.9.0" }] [[package]] name = "agent-framework-foundry-local" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/foundry_local" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -496,7 +496,7 @@ requires-dist = [ [[package]] name = "agent-framework-github-copilot" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/github_copilot" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -511,7 +511,7 @@ requires-dist = [ [[package]] name = "agent-framework-lab" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/lab" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -590,7 +590,7 @@ dev = [ [[package]] name = "agent-framework-mem0" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/mem0" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -605,7 +605,7 @@ requires-dist = [ [[package]] name = "agent-framework-ollama" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/ollama" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -620,7 +620,7 @@ requires-dist = [ [[package]] name = "agent-framework-orchestrations" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/orchestrations" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -631,7 +631,7 @@ requires-dist = [{ name = "agent-framework-core", editable = "packages/core" }] [[package]] name = "agent-framework-purview" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/purview" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -648,7 +648,7 @@ requires-dist = [ [[package]] name = "agent-framework-redis" -version = "1.0.0b260130" +version = "1.0.0b260210" source = { editable = "packages/redis" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1356,7 +1356,7 @@ name = "clr-loader" version = "0.2.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } wheels = [ @@ -1835,7 +1835,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -2301,6 +2301,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/65/5b235b40581ad75ab97dcd8b4218022ae8e3ab77c13c919f1a1dfe9171fd/greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13", size = 273723, upload-time = "2026-01-23T15:30:37.521Z" }, { url = "https://files.pythonhosted.org/packages/ce/ad/eb4729b85cba2d29499e0a04ca6fbdd8f540afd7be142fd571eea43d712f/greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4", size = 574874, upload-time = "2026-01-23T16:00:54.551Z" }, { url = "https://files.pythonhosted.org/packages/87/32/57cad7fe4c8b82fdaa098c89498ef85ad92dfbb09d5eb713adedfc2ae1f5/greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5", size = 586309, upload-time = "2026-01-23T16:05:25.18Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/f041005cb87055e62b0d68680e88ec1a57f4688523d5e2fb305841bc8307/greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5", size = 597461, upload-time = "2026-01-23T16:15:51.943Z" }, { url = "https://files.pythonhosted.org/packages/87/eb/8a1ec2da4d55824f160594a75a9d8354a5fe0a300fb1c48e7944265217e1/greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe", size = 586985, upload-time = "2026-01-23T15:32:47.968Z" }, { url = "https://files.pythonhosted.org/packages/15/1c/0621dd4321dd8c351372ee8f9308136acb628600658a49be1b7504208738/greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729", size = 1547271, upload-time = "2026-01-23T16:04:18.977Z" }, { url = "https://files.pythonhosted.org/packages/9d/53/24047f8924c83bea7a59c8678d9571209c6bfe5f4c17c94a78c06024e9f2/greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4", size = 1613427, upload-time = "2026-01-23T15:33:44.428Z" }, @@ -2308,6 +2309,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, @@ -2316,6 +2318,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, @@ -2324,6 +2327,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, @@ -2332,6 +2336,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, @@ -2340,6 +2345,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, @@ -4559,8 +4565,8 @@ name = "powerfx" version = "0.0.34" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, + { name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" } wheels = [ @@ -5209,7 +5215,7 @@ name = "pythonnet" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } wheels = [