diff --git a/README.md b/README.md index caaf3052d6d7..96362f2af084 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Currently, there are three main APIs your application can target: - [Core](https://microsoft.github.io/autogen/dev/user-guide/core-user-guide/index.html) - [AgentChat](https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/index.html) -- [Extensions](https://microsoft.github.io/autogen/dev/reference/python/autogen_ext/autogen_ext.html) +- [Extensions](https://microsoft.github.io/autogen/dev/user-guide/extensions-user-guide/index.html) ## Core @@ -186,9 +186,9 @@ await app.WaitForShutdownAsync(); [TopicSubscription("agents")] public class HelloAgent( - IAgentContext context, + IAgentContext worker, [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : ConsoleAgent( - context, + worker, typeRegistry), ISayHello, IHandle, diff --git a/dotnet/samples/Hello/HelloAIAgents/HelloAIAgent.cs b/dotnet/samples/Hello/HelloAIAgents/HelloAIAgent.cs index 7261ee45d6ef..0e195ca5b1dd 100644 --- a/dotnet/samples/Hello/HelloAIAgents/HelloAIAgent.cs +++ b/dotnet/samples/Hello/HelloAIAgents/HelloAIAgent.cs @@ -8,11 +8,11 @@ namespace Hello; [TopicSubscription("agents")] public class HelloAIAgent( - IAgentRuntime context, + IAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, IHostApplicationLifetime hostApplicationLifetime, IChatClient client) : HelloAgent( - context, + worker, typeRegistry, hostApplicationLifetime), IHandle diff --git a/dotnet/samples/Hello/HelloAIAgents/Program.cs b/dotnet/samples/Hello/HelloAIAgents/Program.cs index a15effa5aaf0..f9780d62af98 100644 --- a/dotnet/samples/Hello/HelloAIAgents/Program.cs +++ b/dotnet/samples/Hello/HelloAIAgents/Program.cs @@ -33,10 +33,10 @@ namespace Hello { [TopicSubscription("agents")] public class HelloAgent( - IAgentRuntime context, + IAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, IHostApplicationLifetime hostApplicationLifetime) : ConsoleAgent( - context, + worker, typeRegistry), ISayHello, IHandle, diff --git a/dotnet/samples/Hello/HelloAgent/Program.cs b/dotnet/samples/Hello/HelloAgent/Program.cs index 7da454774c4c..dee73b1a47d3 100644 --- a/dotnet/samples/Hello/HelloAgent/Program.cs +++ b/dotnet/samples/Hello/HelloAgent/Program.cs @@ -18,9 +18,9 @@ namespace Hello { [TopicSubscription("agents")] public class HelloAgent( - IAgentRuntime context, IHostApplicationLifetime hostApplicationLifetime, + IAgentWorker worker, IHostApplicationLifetime hostApplicationLifetime, [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : Agent( - context, + worker, typeRegistry), ISayHello, IHandleConsole, @@ -35,7 +35,7 @@ public async Task Handle(NewMessageReceived item) await PublishMessageAsync(evt).ConfigureAwait(false); var goodbye = new ConversationClosed { - UserId = this.AgentId.Key, + UserId = this.AgentId.Type, UserMessage = "Goodbye" }; await PublishMessageAsync(goodbye).ConfigureAwait(false); diff --git a/dotnet/samples/Hello/HelloAgent/README.md b/dotnet/samples/Hello/HelloAgent/README.md index 23051b45cac5..53e3d6a65eba 100644 --- a/dotnet/samples/Hello/HelloAgent/README.md +++ b/dotnet/samples/Hello/HelloAgent/README.md @@ -43,9 +43,9 @@ Within that event handler you may optionally *emit* new events, which are then s ```csharp TopicSubscription("HelloAgents")] public class HelloAgent( - IAgentContext context, + iAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : ConsoleAgent( - context, + worker, typeRegistry), ISayHello, IHandle, diff --git a/dotnet/samples/Hello/HelloAgentState/Program.cs b/dotnet/samples/Hello/HelloAgentState/Program.cs index f22ad8089983..dbb16c3bbb9b 100644 --- a/dotnet/samples/Hello/HelloAgentState/Program.cs +++ b/dotnet/samples/Hello/HelloAgentState/Program.cs @@ -18,10 +18,10 @@ namespace Hello { [TopicSubscription("agents")] public class HelloAgent( - IAgentRuntime context, + IAgentWorker worker, IHostApplicationLifetime hostApplicationLifetime, [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : Agent( - context, + worker, typeRegistry), IHandleConsole, IHandle, diff --git a/dotnet/samples/Hello/HelloAgentState/README.md b/dotnet/samples/Hello/HelloAgentState/README.md index 2671d8e0de63..f46c66df1e1d 100644 --- a/dotnet/samples/Hello/HelloAgentState/README.md +++ b/dotnet/samples/Hello/HelloAgentState/README.md @@ -43,9 +43,9 @@ Within that event handler you may optionally *emit* new events, which are then s ```csharp TopicSubscription("HelloAgents")] public class HelloAgent( - IAgentContext context, + iAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : ConsoleAgent( - context, + worker, typeRegistry), ISayHello, IHandle, diff --git a/dotnet/samples/dev-team/DevTeam.Agents/Developer/Developer.cs b/dotnet/samples/dev-team/DevTeam.Agents/Developer/Developer.cs index afddc34f0e63..ffc474a93124 100644 --- a/dotnet/samples/dev-team/DevTeam.Agents/Developer/Developer.cs +++ b/dotnet/samples/dev-team/DevTeam.Agents/Developer/Developer.cs @@ -11,8 +11,8 @@ namespace DevTeam.Agents; [TopicSubscription("devteam")] -public class Dev(IAgentRuntime context, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, ILogger logger) - : SKAiAgent(context, memory, kernel, typeRegistry), IDevelopApps, +public class Dev(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, ILogger logger) + : SKAiAgent(worker, memory, kernel, typeRegistry), IDevelopApps, IHandle, IHandle { diff --git a/dotnet/samples/dev-team/DevTeam.Agents/DeveloperLead/DeveloperLead.cs b/dotnet/samples/dev-team/DevTeam.Agents/DeveloperLead/DeveloperLead.cs index bda70ebe8773..ffeefe7d430f 100644 --- a/dotnet/samples/dev-team/DevTeam.Agents/DeveloperLead/DeveloperLead.cs +++ b/dotnet/samples/dev-team/DevTeam.Agents/DeveloperLead/DeveloperLead.cs @@ -12,8 +12,8 @@ namespace DevTeam.Agents; [TopicSubscription("devteam")] -public class DeveloperLead(IAgentRuntime context, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, ILogger logger) - : SKAiAgent(context, memory, kernel, typeRegistry), ILeadDevelopers, +public class DeveloperLead(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, ILogger logger) + : SKAiAgent(worker, memory, kernel, typeRegistry), ILeadDevelopers, IHandle, IHandle { diff --git a/dotnet/samples/dev-team/DevTeam.Agents/ProductManager/ProductManager.cs b/dotnet/samples/dev-team/DevTeam.Agents/ProductManager/ProductManager.cs index 5a89536d0ed4..5306a91838e3 100644 --- a/dotnet/samples/dev-team/DevTeam.Agents/ProductManager/ProductManager.cs +++ b/dotnet/samples/dev-team/DevTeam.Agents/ProductManager/ProductManager.cs @@ -11,8 +11,8 @@ namespace DevTeam.Agents; [TopicSubscription("devteam")] -public class ProductManager(IAgentRuntime context, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, ILogger logger) - : SKAiAgent(context, memory, kernel, typeRegistry), IManageProducts, +public class ProductManager(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, ILogger logger) + : SKAiAgent(worker, memory, kernel, typeRegistry), IManageProducts, IHandle, IHandle { diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Agents/AzureGenie.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/AzureGenie.cs index c23a62377e9e..85d498bcc5aa 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/Agents/AzureGenie.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/AzureGenie.cs @@ -9,8 +9,8 @@ using Microsoft.SemanticKernel.Memory; namespace Microsoft.AI.DevTeam; -public class AzureGenie(IAgentRuntime context, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, IManageAzure azureService) - : SKAiAgent(context, memory, kernel, typeRegistry), +public class AzureGenie(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, IManageAzure azureService) + : SKAiAgent(worker, memory, kernel, typeRegistry), IHandle, IHandle diff --git a/dotnet/samples/dev-team/DevTeam.Backend/Agents/Hubber.cs b/dotnet/samples/dev-team/DevTeam.Backend/Agents/Hubber.cs index 3dc9b2a1a87c..3ba0eeb69b25 100644 --- a/dotnet/samples/dev-team/DevTeam.Backend/Agents/Hubber.cs +++ b/dotnet/samples/dev-team/DevTeam.Backend/Agents/Hubber.cs @@ -12,8 +12,8 @@ namespace Microsoft.AI.DevTeam; -public class Hubber(IAgentRuntime context, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, IManageGithub ghService) - : SKAiAgent(context, memory, kernel, typeRegistry), +public class Hubber(IAgentWorker worker, Kernel kernel, ISemanticTextMemory memory, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, IManageGithub ghService) + : SKAiAgent(worker, memory, kernel, typeRegistry), IHandle, IHandle, IHandle, diff --git a/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/InferenceAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/InferenceAgent.cs index f4f06db40eef..bfcd7c6cc179 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/InferenceAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/InferenceAgent.cs @@ -5,10 +5,10 @@ using Microsoft.Extensions.AI; namespace Microsoft.AutoGen.Agents; public abstract class InferenceAgent( - IAgentRuntime context, + IAgentWorker worker, EventTypes typeRegistry, IChatClient client) - : Agent(context, typeRegistry) + : Agent(worker, typeRegistry) where T : IMessage, new() { protected IChatClient ChatClient { get; } = client; diff --git a/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/SKAiAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/SKAiAgent.cs index 0aec9a493807..1d0c57068e13 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/SKAiAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/AIAgent/SKAiAgent.cs @@ -8,18 +8,17 @@ using Microsoft.SemanticKernel.Memory; namespace Microsoft.AutoGen.Agents; -public abstract class SKAiAgent : Agent where T : class, new() +public abstract class SKAiAgent( + IAgentWorker worker, + ISemanticTextMemory memory, + Kernel kernel, + EventTypes typeRegistry) : Agent( + worker, + typeRegistry) where T : class, new() { - protected AgentState _state; - protected Kernel _kernel; - private readonly ISemanticTextMemory _memory; - - public SKAiAgent(IAgentRuntime context, ISemanticTextMemory memory, Kernel kernel, EventTypes typeRegistry) : base(context, typeRegistry) - { - _state = new(); - _memory = memory; - _kernel = kernel; - } + protected AgentState _state = new(); + protected Kernel _kernel = kernel; + private readonly ISemanticTextMemory _memory = memory; public void AddToHistory(string message, ChatUserType userType) => _state.History.Add(new ChatHistoryItem { diff --git a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/ConsoleAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/ConsoleAgent.cs index 082d55e7fcdb..7bfe5ab8d786 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/ConsoleAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/ConsoleAgent/ConsoleAgent.cs @@ -13,7 +13,7 @@ public abstract class ConsoleAgent : IOAgent, { // instead of the primary constructor above, make a constructr here that still calls the base constructor - public ConsoleAgent(IAgentRuntime context, [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : base(context, typeRegistry) + public ConsoleAgent(IAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes typeRegistry) : base(worker, typeRegistry) { _route = "console"; } diff --git a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/FileAgent/FileAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/FileAgent/FileAgent.cs index 7e37a1f0cfc6..ddab8a61ed58 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/FileAgent/FileAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/FileAgent/FileAgent.cs @@ -9,11 +9,11 @@ namespace Microsoft.AutoGen.Agents; [TopicSubscription("FileIO")] public abstract class FileAgent( - IAgentRuntime context, + IAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, string inputPath = "input.txt", string outputPath = "output.txt" - ) : IOAgent(context, typeRegistry), + ) : IOAgent(worker, typeRegistry), IUseFiles, IHandle, IHandle diff --git a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/IOAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/IOAgent.cs index 16dcd9598262..e2b53d694885 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/IOAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/IOAgent.cs @@ -5,12 +5,10 @@ using Microsoft.AutoGen.Core; namespace Microsoft.AutoGen.Agents; -public abstract class IOAgent : Agent +public abstract class IOAgent(IAgentWorker worker, EventTypes eventTypes) : Agent(worker, eventTypes) { public string _route = "base"; - protected IOAgent(IAgentRuntime context, EventTypes eventTypes) : base(context, eventTypes) - { - } + public virtual async Task Handle(Input item) { diff --git a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/WebAPIAgent/WebAPIAgent.cs b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/WebAPIAgent/WebAPIAgent.cs index fe79279ebe5d..3e43096bee29 100644 --- a/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/WebAPIAgent/WebAPIAgent.cs +++ b/dotnet/src/Microsoft.AutoGen/Agents/IOAgent/WebAPIAgent/WebAPIAgent.cs @@ -17,11 +17,11 @@ public abstract class WebAPIAgent : IOAgent, private readonly string _url = "/agents/webio"; public WebAPIAgent( - IAgentRuntime context, + IAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes typeRegistry, ILogger logger, string url = "/agents/webio") : base( - context, + worker, typeRegistry) { _url = url; diff --git a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorker.cs b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorker.cs index 2d86ebd301e2..f96f42a2c11b 100644 --- a/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorker.cs +++ b/dotnet/src/Microsoft.AutoGen/Core.Grpc/GrpcAgentWorker.cs @@ -2,7 +2,6 @@ // GrpcAgentWorker.cs using System.Collections.Concurrent; -using System.Diagnostics; using System.Reflection; using System.Threading.Channels; using Grpc.Core; @@ -18,8 +17,7 @@ public sealed class GrpcAgentWorker( IHostApplicationLifetime hostApplicationLifetime, IServiceProvider serviceProvider, [FromKeyedServices("AgentTypes")] IEnumerable> configuredAgentTypes, - ILogger logger, - DistributedContextPropagator distributedContextPropagator) : + ILogger logger) : IHostedService, IDisposable, IAgentWorker { private readonly object _channelLock = new(); @@ -37,7 +35,6 @@ public sealed class GrpcAgentWorker( private readonly IServiceProvider _serviceProvider = serviceProvider; private readonly IEnumerable> _configuredAgentTypes = configuredAgentTypes; private readonly ILogger _logger = logger; - private readonly DistributedContextPropagator _distributedContextPropagator = distributedContextPropagator; private readonly CancellationTokenSource _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); private AsyncDuplexStreamingCall? _channel; private Task? _readTask; @@ -178,8 +175,7 @@ private Agent GetOrActivateAgent(AgentId agentId) { if (_agentTypes.TryGetValue(agentId.Type, out var agentType)) { - var context = new AgentRuntime(agentId, this, _serviceProvider.GetRequiredService>(), _distributedContextPropagator); - agent = (Agent)ActivatorUtilities.CreateInstance(_serviceProvider, agentType, context); + agent = (Agent)ActivatorUtilities.CreateInstance(_serviceProvider, agentType, this); _agents.TryAdd((agentId.Type, agentId.Key), agent); } else diff --git a/dotnet/src/Microsoft.AutoGen/Core/Agent.cs b/dotnet/src/Microsoft.AutoGen/Core/Agent.cs index f2cb91feddb2..67905ecb0543 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/Agent.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/Agent.cs @@ -13,35 +13,43 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AutoGen.Core; - +/// +/// Represents the base class for an agent in the AutoGen system. +/// public abstract class Agent : IHandle { - public static readonly ActivitySource s_source = new("AutoGen.Agent"); - public AgentId AgentId => _runtime.AgentId; private readonly object _lock = new(); private readonly ConcurrentDictionary> _pendingRequests = []; - private readonly Channel _mailbox = Channel.CreateUnbounded(); - private readonly IAgentRuntime _runtime; - public string Route { get; set; } = "base"; + /// + /// The activity source for tracing. + /// + public static readonly ActivitySource s_source = new("Microsoft.AutoGen.Core.Agent"); + /// + /// Gets the unique identifier of the agent. + /// + public AgentId AgentId { get; private set; } + private readonly Channel _mailbox = Channel.CreateUnbounded(); protected internal ILogger _logger; - public IAgentRuntime Context => _runtime; + public AgentMessenger Messenger { get; private set; } + private readonly ConcurrentDictionary _handlersByMessageType; + internal Task Completion { get; private set; } + protected readonly EventTypes EventTypes; - protected Agent( - IAgentRuntime runtime, + protected Agent(IAgentWorker worker, EventTypes eventTypes, ILogger? logger = null) { - _runtime = runtime; - runtime.AgentInstance = this; - this.EventTypes = eventTypes; + EventTypes = eventTypes; + AgentId = new AgentId(this.GetType().Name, new Guid().ToString()); _logger = logger ?? LoggerFactory.Create(builder => { }).CreateLogger(); + _handlersByMessageType = new(GetType().GetHandlersLookupTable()); + Messenger = AgentMessengerFactory.Create(worker, DistributedContextPropagator.Current); AddImplicitSubscriptionsAsync().AsTask().Wait(); Completion = Start(); } - internal Task Completion { get; } private async ValueTask AddImplicitSubscriptionsAsync() { @@ -66,7 +74,7 @@ private async ValueTask AddImplicitSubscriptionsAsync() } }; // explicitly wait for this to complete - await _runtime.SendMessageAsync(new Message { AddSubscriptionRequest = subscriptionRequest }).ConfigureAwait(true); + await Messenger.SendMessageAsync(new Message { AddSubscriptionRequest = subscriptionRequest }).ConfigureAwait(true); } // using reflection, find all methods that Handle and subscribe to the topic T @@ -82,6 +90,11 @@ private async ValueTask AddImplicitSubscriptionsAsync() } } + + /// + /// Starts the message pump for the agent. + /// + /// A task representing the asynchronous operation. internal Task Start() { var didSuppress = false; @@ -173,18 +186,18 @@ public List Subscribe(string topic) } } }; - _runtime.SendMessageAsync(message).AsTask().Wait(); + Messenger.SendMessageAsync(message).AsTask().Wait(); return new List { topic }; } public async Task StoreAsync(AgentState state, CancellationToken cancellationToken = default) { - await _runtime.StoreAsync(state, cancellationToken).ConfigureAwait(false); + await Messenger.StoreAsync(state, cancellationToken).ConfigureAwait(false); return; } public async Task ReadAsync(AgentId agentId, CancellationToken cancellationToken = default) where T : IMessage, new() { - var agentstate = await _runtime.ReadAsync(agentId, cancellationToken).ConfigureAwait(false); + var agentstate = await Messenger.ReadAsync(agentId, cancellationToken).ConfigureAwait(false); return agentstate.FromAgentState(); } private void OnResponseCore(RpcResponse response) @@ -207,13 +220,13 @@ private async Task OnRequestCoreAsync(RpcRequest request, CancellationToken canc try { - response = await HandleRequest(request).ConfigureAwait(false); + response = await HandleRequestAsync(request).ConfigureAwait(false); } catch (Exception ex) { response = new RpcResponse { Error = ex.Message }; } - await _runtime.SendResponseAsync(request, response, cancellationToken).ConfigureAwait(false); + await Messenger.SendResponseAsync(request, response, cancellationToken).ConfigureAwait(false); } protected async Task RequestAsync(AgentId target, string method, Dictionary parameters) @@ -237,7 +250,7 @@ protected async Task RequestAsync(AgentId target, string method, Di activity?.SetTag("peer.service", target.ToString()); var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - Context!.Update(request, activity); + Messenger!.Update(request, activity); await this.InvokeWithActivityAsync( static async (state, ct) => { @@ -245,7 +258,7 @@ static async (state, ct) => self._pendingRequests.AddOrUpdate(request.RequestId, _ => completion, (_, __) => completion); - await state.Item1.Context!.SendRequestAsync(state.Item1, state.request, ct).ConfigureAwait(false); + await state.Item1.Messenger!.SendRequestAsync(state.Item1, state.request, ct).ConfigureAwait(false); await completion.Task.ConfigureAwait(false); }, @@ -270,11 +283,11 @@ public async ValueTask PublishEventAsync(CloudEvent item, CancellationToken canc activity?.SetTag("peer.service", $"{item.Type}/{item.Source}"); // TODO: fix activity - _runtime.Update(item, activity); + Messenger.Update(item, activity); await this.InvokeWithActivityAsync( static async ((Agent Agent, CloudEvent Event) state, CancellationToken ct) => { - await state.Agent._runtime.PublishEventAsync(state.Event).ConfigureAwait(false); + await state.Agent.Messenger.PublishEventAsync(state.Event).ConfigureAwait(false); }, (this, item), activity, @@ -320,7 +333,7 @@ public Task CallHandler(CloudEvent item) return Task.CompletedTask; } - public Task HandleRequest(RpcRequest request) => Task.FromResult(new RpcResponse { Error = "Not implemented" }); + public Task HandleRequestAsync(RpcRequest request) => Task.FromResult(new RpcResponse { Error = "Not implemented" }); //TODO: should this be async and cancellable? public virtual Task HandleObject(object item) diff --git a/dotnet/src/Microsoft.AutoGen/Core/AgentExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core/AgentExtensions.cs index 49efcb50e962..911107f784c5 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/AgentExtensions.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/AgentExtensions.cs @@ -22,7 +22,7 @@ public static class AgentExtensions public static Activity? ExtractActivity(this Agent agent, string activityName, IDictionary metadata) { Activity? activity; - var (traceParent, traceState) = agent.Context.GetTraceIdAndState(metadata); + var (traceParent, traceState) = agent.Messenger.GetTraceIdAndState(metadata); if (!string.IsNullOrEmpty(traceParent)) { if (ActivityContext.TryParse(traceParent, traceState, isRemote: true, out var parentContext)) @@ -43,7 +43,7 @@ public static class AgentExtensions activity.TraceStateString = traceState; } - var baggage = agent.Context.ExtractMetadata(metadata); + var baggage = agent.Messenger.ExtractMetadata(metadata); foreach (var baggageItem in baggage) { diff --git a/dotnet/src/Microsoft.AutoGen/Core/AgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core/AgentMessenger.cs similarity index 93% rename from dotnet/src/Microsoft.AutoGen/Core/AgentRuntime.cs rename to dotnet/src/Microsoft.AutoGen/Core/AgentMessenger.cs index 63290927a74f..66b1c0c65da9 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/AgentRuntime.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/AgentMessenger.cs @@ -1,21 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. -// AgentRuntime.cs +// AgentMessenger.cs using System.Diagnostics; using Google.Protobuf.Collections; using Microsoft.AutoGen.Contracts; -using Microsoft.Extensions.Logging; using static Microsoft.AutoGen.Contracts.CloudEvent.Types; namespace Microsoft.AutoGen.Core; -public sealed class AgentRuntime(AgentId agentId, IAgentWorker worker, ILogger logger, DistributedContextPropagator distributedContextPropagator) : IAgentRuntime +public sealed class AgentMessenger(IAgentWorker worker, DistributedContextPropagator distributedContextPropagator) { private readonly IAgentWorker worker = worker; - public AgentId AgentId { get; } = agentId; - private ILogger Logger { get; } = logger; - public Agent? AgentInstance { get; set; } private DistributedContextPropagator DistributedContextPropagator { get; } = distributedContextPropagator; public (string?, string?) GetTraceIdAndState(IDictionary metadata) { diff --git a/dotnet/src/Microsoft.AutoGen/Core/AgentMessengerFactory.cs b/dotnet/src/Microsoft.AutoGen/Core/AgentMessengerFactory.cs new file mode 100644 index 000000000000..c008f9f2d5aa --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core/AgentMessengerFactory.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AgentMessengerFactory.cs + +using System.Diagnostics; +namespace Microsoft.AutoGen.Core; +public class AgentMessengerFactory() +{ + public static AgentMessenger Create(IAgentWorker worker, DistributedContextPropagator distributedContextPropagator) + { + return new AgentMessenger(worker, distributedContextPropagator); + } +} diff --git a/dotnet/src/Microsoft.AutoGen/Core/AgentWorker.cs b/dotnet/src/Microsoft.AutoGen/Core/AgentWorker.cs index d88b61e39e97..3b70461a39fd 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/AgentWorker.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/AgentWorker.cs @@ -2,48 +2,34 @@ // AgentWorker.cs using System.Collections.Concurrent; -using System.Diagnostics; using System.Threading.Channels; using Microsoft.AutoGen.Contracts; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; namespace Microsoft.AutoGen.Core; -public class AgentWorker : +public class AgentWorker( +IHostApplicationLifetime hostApplicationLifetime, +IServiceProvider serviceProvider, +[FromKeyedServices("AgentTypes")] IEnumerable> configuredAgentTypes) : IHostedService, IAgentWorker { private readonly ConcurrentDictionary _agentTypes = new(); private readonly ConcurrentDictionary<(string Type, string Key), Agent> _agents = new(); - private readonly ILogger _logger; private readonly Channel _mailbox = Channel.CreateUnbounded(); private readonly ConcurrentDictionary _agentStates = new(); private readonly ConcurrentDictionary _pendingClientRequests = new(); - private readonly CancellationTokenSource _shutdownCts; - private readonly IServiceProvider _serviceProvider; - private readonly IEnumerable> _configuredAgentTypes; + private readonly CancellationTokenSource _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly IEnumerable> _configuredAgentTypes = configuredAgentTypes; private readonly ConcurrentDictionary _subscriptionsByAgentType = new(); private readonly ConcurrentDictionary> _subscriptionsByTopic = new(); - private readonly DistributedContextPropagator _distributedContextPropagator; private readonly CancellationTokenSource _shutdownCancellationToken = new(); private Task? _mailboxTask; private readonly object _channelLock = new(); - public AgentWorker( - IHostApplicationLifetime hostApplicationLifetime, - IServiceProvider serviceProvider, - [FromKeyedServices("AgentTypes")] IEnumerable> configuredAgentTypes, - ILogger logger, - DistributedContextPropagator distributedContextPropagator) - { - _logger = logger; - _serviceProvider = serviceProvider; - _configuredAgentTypes = configuredAgentTypes; - _distributedContextPropagator = distributedContextPropagator; - _shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping); - } // this is the in-memory version - we just pass the message directly to the agent(s) that handle this type of event public async ValueTask PublishEventAsync(CloudEvent cloudEvent, CancellationToken cancellationToken = default) { @@ -196,8 +182,7 @@ private Agent GetOrActivateAgent(AgentId agentId) { if (_agentTypes.TryGetValue(agentId.Type, out var agentType)) { - var context = new AgentRuntime(agentId, this, _serviceProvider.GetRequiredService>(), _distributedContextPropagator); - agent = (Agent)ActivatorUtilities.CreateInstance(_serviceProvider, agentType, context); + agent = (Agent)ActivatorUtilities.CreateInstance(_serviceProvider, agentType, this); _agents.TryAdd((agentId.Type, agentId.Key), agent); } else diff --git a/dotnet/src/Microsoft.AutoGen/Core/Client.cs b/dotnet/src/Microsoft.AutoGen/Core/Client.cs index 274154afa740..1d523005fedf 100644 --- a/dotnet/src/Microsoft.AutoGen/Core/Client.cs +++ b/dotnet/src/Microsoft.AutoGen/Core/Client.cs @@ -1,14 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Client.cs - -using System.Diagnostics; -using Microsoft.AutoGen.Contracts; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace Microsoft.AutoGen.Core; -public sealed class Client(IAgentWorker runtime, DistributedContextPropagator distributedContextPropagator, - [FromKeyedServices("EventTypes")] EventTypes eventTypes, ILogger logger) - : Agent(new AgentRuntime(new AgentId("client", Guid.NewGuid().ToString()), runtime, logger, distributedContextPropagator), eventTypes) +public sealed class Client(IAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes eventTypes) + : Agent(worker, eventTypes) { } diff --git a/dotnet/src/Microsoft.AutoGen/Core/IAgentRuntime.cs b/dotnet/src/Microsoft.AutoGen/Core/IAgentRuntime.cs deleted file mode 100644 index fd2b71d43b0d..000000000000 --- a/dotnet/src/Microsoft.AutoGen/Core/IAgentRuntime.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// IAgentRuntime.cs - -using System.Diagnostics; -using Microsoft.AutoGen.Contracts; - -namespace Microsoft.AutoGen.Core; - -public interface IAgentRuntime -{ - AgentId AgentId { get; } - Agent? AgentInstance { get; set; } - ValueTask StoreAsync(AgentState value, CancellationToken cancellationToken = default); - ValueTask ReadAsync(AgentId agentId, CancellationToken cancellationToken = default); - ValueTask SendResponseAsync(RpcRequest request, RpcResponse response, CancellationToken cancellationToken = default); - ValueTask SendRequestAsync(Agent agent, RpcRequest request, CancellationToken cancellationToken = default); - ValueTask SendMessageAsync(Message message, CancellationToken cancellationToken = default); - ValueTask PublishEventAsync(CloudEvent @event, CancellationToken cancellationToken = default); - void Update(RpcRequest request, Activity? activity); - void Update(CloudEvent cloudEvent, Activity? activity); - (string?, string?) GetTraceIdAndState(IDictionary metadata); - IDictionary ExtractMetadata(IDictionary metadata); -} diff --git a/dotnet/src/Microsoft.AutoGen/Core/IHandleExtensions.cs b/dotnet/src/Microsoft.AutoGen/Core/IHandleExtensions.cs new file mode 100644 index 000000000000..44014a4abcca --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Core/IHandleExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// IHandleExtensions.cs + +using System.Reflection; + +namespace Microsoft.AutoGen.Core; + +/// +/// Provides extension methods for types implementing the IHandle interface. +/// +public static class IHandleExtensions +{ + /// + /// Gets all the handler methods from the interfaces implemented by the specified type. + /// + /// The type to get the handler methods from. + /// An array of MethodInfo objects representing the handler methods. + public static MethodInfo[] GetHandlers(this Type type) + { + var handlers = type.GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IHandle<>)); + return handlers.SelectMany(h => h.GetMethods().Where(m => m.Name == "Handle")).ToArray(); + } + + /// + /// Gets a lookup table of handler methods from the interfaces implemented by the specified type. + /// + /// The type to get the handler methods from. + /// A dictionary where the key is the generic type and the value is the MethodInfo of the handler method. + public static Dictionary GetHandlersLookupTable(this Type type) + { + var handlers = type.GetHandlers(); + return handlers.ToDictionary(h => + { + var generic = h.DeclaringType!.GetGenericArguments(); + return generic[0]; + }); + } +} diff --git a/dotnet/test/Microsoft.AutoGen.Agents.Tests/AgentTests.cs b/dotnet/test/Microsoft.AutoGen.Agents.Tests/AgentTests.cs index bd169d644b53..08a88da048de 100644 --- a/dotnet/test/Microsoft.AutoGen.Agents.Tests/AgentTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Agents.Tests/AgentTests.cs @@ -23,12 +23,8 @@ public class AgentTests(InMemoryAgentRuntimeFixture fixture) [Fact] public async Task ItInvokeRightHandlerTestAsync() { - var mockContext = new Mock(); - mockContext.SetupGet(x => x.AgentId).Returns(new AgentId("test", "test")); - // mock SendMessageAsync - mockContext.Setup(x => x.SendMessageAsync(It.IsAny(), It.IsAny())) - .Returns(new ValueTask()); - var agent = new TestAgent(mockContext.Object, new EventTypes(TypeRegistry.Empty, [], []), new Logger(new LoggerFactory())); + var mockWorker = new Mock(); + var agent = new TestAgent(mockWorker.Object, new EventTypes(TypeRegistry.Empty, [], []), new Logger(new LoggerFactory())); await agent.HandleObject("hello world"); await agent.HandleObject(42); @@ -65,9 +61,9 @@ await client.PublishMessageAsync(new TextMessage() public class TestAgent : Agent, IHandle, IHandle, IHandle { public TestAgent( - IAgentRuntime context, + IAgentWorker worker, [FromKeyedServices("EventTypes")] EventTypes eventTypes, - Logger? logger = null) : base(context, eventTypes, logger) + Logger? logger = null) : base(worker, eventTypes, logger) { } diff --git a/dotnet/test/Microsoft.AutoGen.Integration.Tests/HelloAppHostIntegrationTests.cs b/dotnet/test/Microsoft.AutoGen.Integration.Tests/HelloAppHostIntegrationTests.cs index 18b35bd7039c..ba83b07f72df 100644 --- a/dotnet/test/Microsoft.AutoGen.Integration.Tests/HelloAppHostIntegrationTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Integration.Tests/HelloAppHostIntegrationTests.cs @@ -45,7 +45,7 @@ public async Task AppHostLogsHelloAgentE2E(TestEndpoints testEndpoints) //sleep to make sure the app is running await Task.Delay(20000); app.EnsureNoErrorsLogged(); - app.EnsureLogContains("HelloAgents said Goodbye"); + app.EnsureLogContains("HelloAgent said Goodbye"); app.EnsureLogContains("Wild Hello from Python!"); await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(15)); diff --git a/dotnet/test/Microsoft.AutoGen.Integration.Tests/InMemoryRuntimeIntegrationTests.cs b/dotnet/test/Microsoft.AutoGen.Integration.Tests/InMemoryRuntimeIntegrationTests.cs index 0795cf8be61d..b6bdd9eb72d9 100644 --- a/dotnet/test/Microsoft.AutoGen.Integration.Tests/InMemoryRuntimeIntegrationTests.cs +++ b/dotnet/test/Microsoft.AutoGen.Integration.Tests/InMemoryRuntimeIntegrationTests.cs @@ -24,7 +24,7 @@ public async Task HelloAgentsE2EInMemory(string appHostAssemblyPath) await Task.Delay(15000); app.EnsureNoErrorsLogged(); app.EnsureLogContains("Hello World"); - app.EnsureLogContains("HelloAgents said Goodbye"); + app.EnsureLogContains("HelloAgent said Goodbye"); await app.StopAsync().WaitAsync(TimeSpan.FromSeconds(15)); } diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py index 74a80ac7bbd3..7afa7f48a516 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py @@ -21,13 +21,13 @@ from ..base import Handoff as HandoffBase from ..base import Response from ..messages import ( - AgentMessage, + AgentEvent, ChatMessage, HandoffMessage, MultiModalMessage, TextMessage, - ToolCallMessage, - ToolCallResultMessage, + ToolCallExecutionEvent, + ToolCallRequestEvent, ) from ..state import AssistantAgentState from ._base_chat_agent import BaseChatAgent @@ -292,7 +292,7 @@ async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: async def on_messages_stream( self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken - ) -> AsyncGenerator[AgentMessage | Response, None]: + ) -> AsyncGenerator[AgentEvent | ChatMessage | Response, None]: # Add messages to the model context. for msg in messages: if isinstance(msg, MultiModalMessage) and self._model_client.capabilities["vision"] is False: @@ -300,7 +300,7 @@ async def on_messages_stream( self._model_context.append(UserMessage(content=msg.content, source=msg.source)) # Inner messages. - inner_messages: List[AgentMessage] = [] + inner_messages: List[AgentEvent | ChatMessage] = [] # Generate an inference result based on the current model context. llm_messages = self._system_messages + self._model_context @@ -321,7 +321,7 @@ async def on_messages_stream( # Process tool calls. assert isinstance(result.content, list) and all(isinstance(item, FunctionCall) for item in result.content) - tool_call_msg = ToolCallMessage(content=result.content, source=self.name, models_usage=result.usage) + tool_call_msg = ToolCallRequestEvent(content=result.content, source=self.name, models_usage=result.usage) event_logger.debug(tool_call_msg) # Add the tool call message to the output. inner_messages.append(tool_call_msg) @@ -329,7 +329,7 @@ async def on_messages_stream( # Execute the tool calls. results = await asyncio.gather(*[self._execute_tool_call(call, cancellation_token) for call in result.content]) - tool_call_result_msg = ToolCallResultMessage(content=results, source=self.name) + tool_call_result_msg = ToolCallExecutionEvent(content=results, source=self.name) event_logger.debug(tool_call_result_msg) self._model_context.append(FunctionExecutionResultMessage(content=results)) inner_messages.append(tool_call_result_msg) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_base_chat_agent.py b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_base_chat_agent.py index c06fb8d6db53..a91e8f8fc75b 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_base_chat_agent.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_base_chat_agent.py @@ -5,7 +5,7 @@ from ..base import ChatAgent, Response, TaskResult from ..messages import ( - AgentMessage, + AgentEvent, ChatMessage, TextMessage, ) @@ -42,16 +42,38 @@ def produced_message_types(self) -> List[type[ChatMessage]]: @abstractmethod async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response: - """Handles incoming messages and returns a response.""" + """Handles incoming messages and returns a response. + + .. note:: + + Agents are stateful and the messages passed to this method should + be the new messages since the last call to this method. The agent + should maintain its state between calls to this method. For example, + if the agent needs to remember the previous messages to respond to + the current message, it should store the previous messages in the + agent state. + + """ ... async def on_messages_stream( self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken - ) -> AsyncGenerator[AgentMessage | Response, None]: + ) -> AsyncGenerator[AgentEvent | ChatMessage | Response, None]: """Handles incoming messages and returns a stream of messages and and the final item is the response. The base implementation in :class:`BaseChatAgent` simply calls :meth:`on_messages` and yields - the messages in the response.""" + the messages in the response. + + .. note:: + + Agents are stateful and the messages passed to this method should + be the new messages since the last call to this method. The agent + should maintain its state between calls to this method. For example, + if the agent needs to remember the previous messages to respond to + the current message, it should store the previous messages in the + agent state. + + """ response = await self.on_messages(messages, cancellation_token) for inner_message in response.inner_messages or []: yield inner_message @@ -67,7 +89,7 @@ async def run( if cancellation_token is None: cancellation_token = CancellationToken() input_messages: List[ChatMessage] = [] - output_messages: List[AgentMessage] = [] + output_messages: List[AgentEvent | ChatMessage] = [] if task is None: pass elif isinstance(task, str): @@ -97,13 +119,13 @@ async def run_stream( *, task: str | ChatMessage | List[ChatMessage] | None = None, cancellation_token: CancellationToken | None = None, - ) -> AsyncGenerator[AgentMessage | TaskResult, None]: + ) -> AsyncGenerator[AgentEvent | ChatMessage | TaskResult, None]: """Run the agent with the given task and return a stream of messages and the final task result as the last item in the stream.""" if cancellation_token is None: cancellation_token = CancellationToken() input_messages: List[ChatMessage] = [] - output_messages: List[AgentMessage] = [] + output_messages: List[AgentEvent | ChatMessage] = [] if task is None: pass elif isinstance(task, str): diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_society_of_mind_agent.py b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_society_of_mind_agent.py index 4c13c10bc386..52e594ad785a 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_society_of_mind_agent.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_society_of_mind_agent.py @@ -8,7 +8,7 @@ from ..base import TaskResult, Team from ..messages import ( - AgentMessage, + AgentEvent, ChatMessage, HandoffMessage, MultiModalMessage, @@ -119,13 +119,13 @@ async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: async def on_messages_stream( self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken - ) -> AsyncGenerator[AgentMessage | Response, None]: + ) -> AsyncGenerator[AgentEvent | ChatMessage | Response, None]: # Prepare the task for the team of agents. task = list(messages) # Run the team of agents. result: TaskResult | None = None - inner_messages: List[AgentMessage] = [] + inner_messages: List[AgentEvent | ChatMessage] = [] count = 0 async for inner_msg in self._team.run_stream(task=task, cancellation_token=cancellation_token): if isinstance(inner_msg, TaskResult): diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_chat_agent.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_chat_agent.py index c27dc232a3d9..d41d1f6be44b 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_chat_agent.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_chat_agent.py @@ -3,7 +3,7 @@ from autogen_core import CancellationToken -from ..messages import AgentMessage, ChatMessage +from ..messages import AgentEvent, ChatMessage from ._task import TaskRunner @@ -14,7 +14,7 @@ class Response: chat_message: ChatMessage """A chat message produced by the agent as the response.""" - inner_messages: List[AgentMessage] | None = None + inner_messages: List[AgentEvent | ChatMessage] | None = None """Inner messages produced by the agent.""" @@ -46,7 +46,7 @@ async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: def on_messages_stream( self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken - ) -> AsyncGenerator[AgentMessage | Response, None]: + ) -> AsyncGenerator[AgentEvent | ChatMessage | Response, None]: """Handles incoming messages and returns a stream of inner messages and and the final item is the response.""" ... diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_task.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_task.py index d0d9aefe70a7..ecf05b170866 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_task.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_task.py @@ -3,14 +3,14 @@ from autogen_core import CancellationToken -from ..messages import AgentMessage, ChatMessage +from ..messages import AgentEvent, ChatMessage @dataclass class TaskResult: """Result of running a task.""" - messages: Sequence[AgentMessage] + messages: Sequence[AgentEvent | ChatMessage] """Messages produced by the task.""" stop_reason: str | None = None @@ -38,7 +38,7 @@ def run_stream( *, task: str | ChatMessage | List[ChatMessage] | None = None, cancellation_token: CancellationToken | None = None, - ) -> AsyncGenerator[AgentMessage | TaskResult, None]: + ) -> AsyncGenerator[AgentEvent | ChatMessage | TaskResult, None]: """Run the task and produces a stream of messages and the final result :class:`TaskResult` as the last item in the stream. diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py index 211b912c1f25..8975c75aad12 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from typing import List, Sequence -from ..messages import AgentMessage, StopMessage +from ..messages import AgentEvent, ChatMessage, StopMessage class TerminatedException(BaseException): ... @@ -50,7 +50,7 @@ def terminated(self) -> bool: ... @abstractmethod - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: """Check if the conversation should be terminated based on the messages received since the last time the condition was called. Return a StopMessage if the conversation should be terminated, or None otherwise. @@ -88,7 +88,7 @@ def __init__(self, *conditions: TerminationCondition) -> None: def terminated(self) -> bool: return all(condition.terminated for condition in self._conditions) - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: raise TerminatedException("Termination condition has already been reached.") # Check all remaining conditions. @@ -120,7 +120,7 @@ def __init__(self, *conditions: TerminationCondition) -> None: def terminated(self) -> bool: return any(condition.terminated for condition in self._conditions) - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: raise RuntimeError("Termination condition has already been reached") stop_messages = await asyncio.gather(*[condition(messages) for condition in self._conditions]) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py b/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py index bab3e03ff350..c554f477ad26 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py @@ -2,7 +2,7 @@ from typing import List, Sequence from ..base import TerminatedException, TerminationCondition -from ..messages import AgentMessage, HandoffMessage, MultiModalMessage, StopMessage, TextMessage +from ..messages import AgentEvent, ChatMessage, HandoffMessage, MultiModalMessage, StopMessage, TextMessage class StopMessageTermination(TerminationCondition): @@ -15,7 +15,7 @@ def __init__(self) -> None: def terminated(self) -> bool: return self._terminated - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: raise TerminatedException("Termination condition has already been reached") for message in messages: @@ -43,7 +43,7 @@ def __init__(self, max_messages: int) -> None: def terminated(self) -> bool: return self._message_count >= self._max_messages - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: raise TerminatedException("Termination condition has already been reached") self._message_count += len(messages) @@ -73,7 +73,7 @@ def __init__(self, text: str) -> None: def terminated(self) -> bool: return self._terminated - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: raise TerminatedException("Termination condition has already been reached") for message in messages: @@ -128,7 +128,7 @@ def terminated(self) -> bool: or (self._max_completion_token is not None and self._completion_token_count >= self._max_completion_token) ) - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: raise TerminatedException("Termination condition has already been reached") for message in messages: @@ -163,7 +163,7 @@ def __init__(self, target: str) -> None: def terminated(self) -> bool: return self._terminated - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: raise TerminatedException("Termination condition has already been reached") for message in messages: @@ -194,7 +194,7 @@ def __init__(self, timeout_seconds: float) -> None: def terminated(self) -> bool: return self._terminated - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: raise TerminatedException("Termination condition has already been reached") @@ -242,7 +242,7 @@ def set(self) -> None: """Set the termination condition to terminated.""" self._setted = True - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: raise TerminatedException("Termination condition has already been reached") if self._setted: @@ -273,7 +273,7 @@ def __init__(self, sources: List[str]) -> None: def terminated(self) -> bool: return self._terminated - async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: raise TerminatedException("Termination condition has already been reached") if not messages: diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/messages.py b/python/packages/autogen-agentchat/src/autogen_agentchat/messages.py index fddd6bea5bf8..547b4a7fab96 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/messages.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/messages.py @@ -9,7 +9,7 @@ from autogen_core import FunctionCall, Image from autogen_core.models import FunctionExecutionResult, RequestUsage from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import Annotated +from typing_extensions import Annotated, deprecated class BaseMessage(BaseModel): @@ -63,6 +63,7 @@ class HandoffMessage(BaseMessage): type: Literal["HandoffMessage"] = "HandoffMessage" +@deprecated("Will be removed in 0.4.0, use ToolCallRequestEvent instead.") class ToolCallMessage(BaseMessage): """A message signaling the use of tools.""" @@ -72,6 +73,7 @@ class ToolCallMessage(BaseMessage): type: Literal["ToolCallMessage"] = "ToolCallMessage" +@deprecated("Will be removed in 0.4.0, use ToolCallExecutionEvent instead.") class ToolCallResultMessage(BaseMessage): """A message signaling the results of tool calls.""" @@ -81,15 +83,37 @@ class ToolCallResultMessage(BaseMessage): type: Literal["ToolCallResultMessage"] = "ToolCallResultMessage" +class ToolCallRequestEvent(BaseMessage): + """An event signaling a request to use tools.""" + + content: List[FunctionCall] + """The tool calls.""" + + type: Literal["ToolCallRequestEvent"] = "ToolCallRequestEvent" + + +class ToolCallExecutionEvent(BaseMessage): + """An event signaling the execution of tool calls.""" + + content: List[FunctionExecutionResult] + """The tool call results.""" + + type: Literal["ToolCallExecutionEvent"] = "ToolCallExecutionEvent" + + ChatMessage = Annotated[TextMessage | MultiModalMessage | StopMessage | HandoffMessage, Field(discriminator="type")] -"""Messages for agent-to-agent communication.""" +"""Messages for agent-to-agent communication only.""" + + +AgentEvent = Annotated[ToolCallRequestEvent | ToolCallExecutionEvent, Field(discriminator="type")] +"""Events emitted by agents and teams when they work, not used for agent-to-agent communication.""" AgentMessage = Annotated[ - TextMessage | MultiModalMessage | StopMessage | HandoffMessage | ToolCallMessage | ToolCallResultMessage, + TextMessage | MultiModalMessage | StopMessage | HandoffMessage | ToolCallRequestEvent | ToolCallExecutionEvent, Field(discriminator="type"), ] -"""All message types.""" +"""(Deprecated, will be removed in 0.4.0) All message and event types.""" __all__ = [ @@ -98,8 +122,11 @@ class ToolCallResultMessage(BaseMessage): "MultiModalMessage", "StopMessage", "HandoffMessage", + "ToolCallRequestEvent", + "ToolCallExecutionEvent", "ToolCallMessage", "ToolCallResultMessage", "ChatMessage", + "AgentEvent", "AgentMessage", ] diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/state/_states.py b/python/packages/autogen-agentchat/src/autogen_agentchat/state/_states.py index 4bf3d4709943..ddc57e23d94c 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/state/_states.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/state/_states.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field from ..messages import ( - AgentMessage, + AgentEvent, ChatMessage, ) @@ -36,7 +36,7 @@ class TeamState(BaseState): class BaseGroupChatManagerState(BaseState): """Base state for all group chat managers.""" - message_thread: List[AgentMessage] = Field(default_factory=list) + message_thread: List[AgentEvent | ChatMessage] = Field(default_factory=list) current_turn: int = Field(default=0) type: str = Field(default="BaseGroupChatManagerState") diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/task/__init__.py b/python/packages/autogen-agentchat/src/autogen_agentchat/task/__init__.py index 6ff2077882ba..a45983d83c3a 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/task/__init__.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/task/__init__.py @@ -27,39 +27,39 @@ from ..conditions import ( TokenUsageTermination as TokenUsageTerminationAlias, ) -from ..messages import AgentMessage +from ..messages import AgentEvent, ChatMessage from ..ui import Console as ConsoleAlias -@deprecated("Moved to autogen_agentchat.terminations.ExternalTermination. Will remove this in 0.4.0.", stacklevel=2) +@deprecated("Moved to autogen_agentchat.conditions.ExternalTermination. Will remove this in 0.4.0.", stacklevel=2) class ExternalTermination(ExternalTerminationAlias): ... -@deprecated("Moved to autogen_agentchat.terminations.HandoffTermination. Will remove this in 0.4.0.", stacklevel=2) +@deprecated("Moved to autogen_agentchat.conditions.HandoffTermination. Will remove this in 0.4.0.", stacklevel=2) class HandoffTermination(HandoffTerminationAlias): ... -@deprecated("Moved to autogen_agentchat.terminations.MaxMessageTermination. Will remove this in 0.4.0.", stacklevel=2) +@deprecated("Moved to autogen_agentchat.conditions.MaxMessageTermination. Will remove this in 0.4.0.", stacklevel=2) class MaxMessageTermination(MaxMessageTerminationAlias): ... -@deprecated("Moved to autogen_agentchat.terminations.SourceMatchTermination. Will remove this in 0.4.0.", stacklevel=2) +@deprecated("Moved to autogen_agentchat.conditions.SourceMatchTermination. Will remove this in 0.4.0.", stacklevel=2) class SourceMatchTermination(SourceMatchTerminationAlias): ... -@deprecated("Moved to autogen_agentchat.terminations.StopMessageTermination. Will remove this in 0.4.0.", stacklevel=2) +@deprecated("Moved to autogen_agentchat.conditions.StopMessageTermination. Will remove this in 0.4.0.", stacklevel=2) class StopMessageTermination(StopMessageTerminationAlias): ... -@deprecated("Moved to autogen_agentchat.terminations.TextMentionTermination. Will remove this in 0.4.0.", stacklevel=2) +@deprecated("Moved to autogen_agentchat.conditions.TextMentionTermination. Will remove this in 0.4.0.", stacklevel=2) class TextMentionTermination(TextMentionTerminationAlias): ... -@deprecated("Moved to autogen_agentchat.terminations.TimeoutTermination. Will remove this in 0.4.0.", stacklevel=2) +@deprecated("Moved to autogen_agentchat.conditions.TimeoutTermination. Will remove this in 0.4.0.", stacklevel=2) class TimeoutTermination(TimeoutTerminationAlias): ... -@deprecated("Moved to autogen_agentchat.terminations.TokenUsageTermination. Will remove this in 0.4.0.", stacklevel=2) +@deprecated("Moved to autogen_agentchat.conditions.TokenUsageTermination. Will remove this in 0.4.0.", stacklevel=2) class TokenUsageTermination(TokenUsageTerminationAlias): ... @@ -68,7 +68,7 @@ class TokenUsageTermination(TokenUsageTerminationAlias): ... @deprecated("Moved to autogen_agentchat.ui.Console. Will remove this in 0.4.0.", stacklevel=2) async def Console( - stream: AsyncGenerator[AgentMessage | T, None], + stream: AsyncGenerator[AgentEvent | ChatMessage | T, None], *, no_inline_images: bool = False, ) -> T: diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.py index 7a6496acbe87..1d8d30d586d4 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.py @@ -19,7 +19,7 @@ from ... import EVENT_LOGGER_NAME from ...base import ChatAgent, TaskResult, Team, TerminationCondition -from ...messages import AgentMessage, ChatMessage, TextMessage +from ...messages import AgentEvent, ChatMessage, TextMessage from ...state import TeamState from ._chat_agent_container import ChatAgentContainer from ._events import GroupChatMessage, GroupChatReset, GroupChatStart, GroupChatTermination @@ -62,7 +62,7 @@ def __init__( # Constants for the closure agent to collect the output messages. self._stop_reason: str | None = None - self._output_message_queue: asyncio.Queue[AgentMessage | None] = asyncio.Queue() + self._output_message_queue: asyncio.Queue[AgentEvent | ChatMessage | None] = asyncio.Queue() # Create a runtime for the team. # TODO: The runtime should be created by a managed context. @@ -273,7 +273,7 @@ async def run_stream( *, task: str | ChatMessage | List[ChatMessage] | None = None, cancellation_token: CancellationToken | None = None, - ) -> AsyncGenerator[AgentMessage | TaskResult, None]: + ) -> AsyncGenerator[AgentEvent | ChatMessage | TaskResult, None]: """Run the team and produces a stream of messages and the final result of the type :class:`TaskResult` as the last item in the stream. Once the team is stopped, the termination condition is reset. @@ -405,7 +405,7 @@ async def stop_runtime() -> None: cancellation_token=cancellation_token, ) # Collect the output messages in order. - output_messages: List[AgentMessage] = [] + output_messages: List[AgentEvent | ChatMessage] = [] # Yield the messsages until the queue is empty. while True: message_future = asyncio.ensure_future(self._output_message_queue.get()) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat_manager.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat_manager.py index 84725cecdd65..45469292d4cd 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat_manager.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat_manager.py @@ -5,7 +5,7 @@ from autogen_core import DefaultTopicId, MessageContext, event, rpc from ...base import TerminationCondition -from ...messages import AgentMessage, ChatMessage, StopMessage +from ...messages import AgentEvent, ChatMessage, StopMessage from ._events import ( GroupChatAgentResponse, GroupChatRequestPublish, @@ -48,7 +48,7 @@ def __init__( raise ValueError("The group topic type must not be in the participant topic types.") self._participant_topic_types = participant_topic_types self._participant_descriptions = participant_descriptions - self._message_thread: List[AgentMessage] = [] + self._message_thread: List[AgentEvent | ChatMessage] = [] self._termination_condition = termination_condition if max_turns is not None and max_turns <= 0: raise ValueError("The maximum number of turns must be greater than 0.") @@ -115,7 +115,7 @@ async def handle_start(self, message: GroupChatStart, ctx: MessageContext) -> No @event async def handle_agent_response(self, message: GroupChatAgentResponse, ctx: MessageContext) -> None: # Append the message to the message thread and construct the delta. - delta: List[AgentMessage] = [] + delta: List[AgentEvent | ChatMessage] = [] if message.agent_response.inner_messages is not None: for inner_message in message.agent_response.inner_messages: self._message_thread.append(inner_message) @@ -180,7 +180,7 @@ async def validate_group_state(self, messages: List[ChatMessage] | None) -> None ... @abstractmethod - async def select_speaker(self, thread: List[AgentMessage]) -> str: + async def select_speaker(self, thread: List[AgentEvent | ChatMessage]) -> str: """Select a speaker from the participants and return the topic type of the selected speaker.""" ... diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_events.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_events.py index ed325fcb5159..853b854a4541 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_events.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_events.py @@ -3,7 +3,7 @@ from pydantic import BaseModel from ...base import Response -from ...messages import AgentMessage, ChatMessage, StopMessage +from ...messages import AgentEvent, ChatMessage, StopMessage class GroupChatStart(BaseModel): @@ -29,7 +29,7 @@ class GroupChatRequestPublish(BaseModel): class GroupChatMessage(BaseModel): """A message from a group chat.""" - message: AgentMessage + message: AgentEvent | ChatMessage """The message that was published.""" diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_magentic_one/_magentic_one_orchestrator.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_magentic_one/_magentic_one_orchestrator.py index dcdf8b91809d..6e0d12f7f754 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_magentic_one/_magentic_one_orchestrator.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_magentic_one/_magentic_one_orchestrator.py @@ -13,14 +13,14 @@ from .... import TRACE_LOGGER_NAME from ....base import Response, TerminationCondition from ....messages import ( - AgentMessage, + AgentEvent, ChatMessage, HandoffMessage, MultiModalMessage, StopMessage, TextMessage, - ToolCallMessage, - ToolCallResultMessage, + ToolCallExecutionEvent, + ToolCallRequestEvent, ) from ....state import MagenticOneOrchestratorState from .._base_group_chat_manager import BaseGroupChatManager @@ -167,7 +167,7 @@ async def handle_start(self, message: GroupChatStart, ctx: MessageContext) -> No @event async def handle_agent_response(self, message: GroupChatAgentResponse, ctx: MessageContext) -> None: # type: ignore - delta: List[AgentMessage] = [] + delta: List[AgentEvent | ChatMessage] = [] if message.agent_response.inner_messages is not None: for inner_message in message.agent_response.inner_messages: delta.append(inner_message) @@ -210,7 +210,7 @@ async def load_state(self, state: Mapping[str, Any]) -> None: self._n_rounds = orchestrator_state.n_rounds self._n_stalls = orchestrator_state.n_stalls - async def select_speaker(self, thread: List[AgentMessage]) -> str: + async def select_speaker(self, thread: List[AgentEvent | ChatMessage]) -> str: """Not used in this orchestrator, we select next speaker in _orchestrate_step.""" return "" @@ -427,7 +427,7 @@ def _thread_to_context(self) -> List[LLMMessage]: """Convert the message thread to a context for the model.""" context: List[LLMMessage] = [] for m in self._message_thread: - if isinstance(m, ToolCallMessage | ToolCallResultMessage): + if isinstance(m, ToolCallRequestEvent | ToolCallExecutionEvent): # Ignore tool call messages. continue elif isinstance(m, StopMessage | HandoffMessage): diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_round_robin_group_chat.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_round_robin_group_chat.py index 3e17943b90b2..d6901f04c988 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_round_robin_group_chat.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_round_robin_group_chat.py @@ -1,7 +1,7 @@ from typing import Any, Callable, List, Mapping from ...base import ChatAgent, TerminationCondition -from ...messages import AgentMessage, ChatMessage +from ...messages import AgentEvent, ChatMessage from ...state import RoundRobinManagerState from ._base_group_chat import BaseGroupChat from ._base_group_chat_manager import BaseGroupChatManager @@ -53,7 +53,7 @@ async def load_state(self, state: Mapping[str, Any]) -> None: self._current_turn = round_robin_state.current_turn self._next_speaker_index = round_robin_state.next_speaker_index - async def select_speaker(self, thread: List[AgentMessage]) -> str: + async def select_speaker(self, thread: List[AgentEvent | ChatMessage]) -> str: """Select a speaker from the participants in a round-robin fashion.""" current_speaker_index = self._next_speaker_index self._next_speaker_index = (current_speaker_index + 1) % len(self._participant_topic_types) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py index 5f161d0c6858..735e533c908e 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py @@ -7,14 +7,14 @@ from ... import TRACE_LOGGER_NAME from ...base import ChatAgent, TerminationCondition from ...messages import ( - AgentMessage, + AgentEvent, ChatMessage, HandoffMessage, MultiModalMessage, StopMessage, TextMessage, - ToolCallMessage, - ToolCallResultMessage, + ToolCallExecutionEvent, + ToolCallRequestEvent, ) from ...state import SelectorManagerState from ._base_group_chat import BaseGroupChat @@ -38,7 +38,7 @@ def __init__( model_client: ChatCompletionClient, selector_prompt: str, allow_repeated_speaker: bool, - selector_func: Callable[[Sequence[AgentMessage]], str | None] | None, + selector_func: Callable[[Sequence[AgentEvent | ChatMessage]], str | None] | None, ) -> None: super().__init__( group_topic_type, @@ -78,7 +78,7 @@ async def load_state(self, state: Mapping[str, Any]) -> None: self._current_turn = selector_state.current_turn self._previous_speaker = selector_state.previous_speaker - async def select_speaker(self, thread: List[AgentMessage]) -> str: + async def select_speaker(self, thread: List[AgentEvent | ChatMessage]) -> str: """Selects the next speaker in a group chat using a ChatCompletion client, with the selector function as override if it returns a speaker name. @@ -95,7 +95,7 @@ async def select_speaker(self, thread: List[AgentMessage]) -> str: # Construct the history of the conversation. history_messages: List[str] = [] for msg in thread: - if isinstance(msg, ToolCallMessage | ToolCallResultMessage): + if isinstance(msg, ToolCallRequestEvent | ToolCallExecutionEvent): # Ignore tool call messages. continue # The agent type must be the same as the topic type, which we use as the agent name. @@ -204,7 +204,7 @@ class SelectorGroupChat(BaseGroupChat): Must contain '{roles}', '{participants}', and '{history}' to be filled in. allow_repeated_speaker (bool, optional): Whether to allow the same speaker to be selected consecutively. Defaults to False. - selector_func (Callable[[Sequence[AgentMessage]], str | None], optional): A custom selector + selector_func (Callable[[Sequence[AgentEvent | ChatMessage]], str | None], optional): A custom selector function that takes the conversation history and returns the name of the next speaker. If provided, this function will be used to override the model to select the next speaker. If the function returns None, the model will be used to select the next speaker. @@ -278,7 +278,7 @@ async def book_trip() -> str: from autogen_agentchat.teams import SelectorGroupChat from autogen_agentchat.conditions import TextMentionTermination from autogen_agentchat.ui import Console - from autogen_agentchat.messages import AgentMessage + from autogen_agentchat.messages import AgentEvent, ChatMessage async def main() -> None: @@ -304,7 +304,7 @@ def check_calculation(x: int, y: int, answer: int) -> str: system_message="Check the answer and respond with 'Correct!' or 'Incorrect!'", ) - def selector_func(messages: Sequence[AgentMessage]) -> str | None: + def selector_func(messages: Sequence[AgentEvent | ChatMessage]) -> str | None: if len(messages) == 1 or messages[-1].content == "Incorrect!": return "Agent1" if messages[-1].source == "Agent1": @@ -341,7 +341,7 @@ def __init__( Read the above conversation. Then select the next role from {participants} to play. Only return the role. """, allow_repeated_speaker: bool = False, - selector_func: Callable[[Sequence[AgentMessage]], str | None] | None = None, + selector_func: Callable[[Sequence[AgentEvent | ChatMessage]], str | None] | None = None, ): super().__init__( participants, diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_swarm_group_chat.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_swarm_group_chat.py index 436fe8e4cdae..a31a693e086f 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_swarm_group_chat.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_swarm_group_chat.py @@ -1,7 +1,7 @@ from typing import Any, Callable, List, Mapping from ...base import ChatAgent, TerminationCondition -from ...messages import AgentMessage, ChatMessage, HandoffMessage +from ...messages import AgentEvent, ChatMessage, HandoffMessage from ...state import SwarmManagerState from ._base_group_chat import BaseGroupChat from ._base_group_chat_manager import BaseGroupChatManager @@ -64,7 +64,7 @@ async def reset(self) -> None: await self._termination_condition.reset() self._current_speaker = self._participant_topic_types[0] - async def select_speaker(self, thread: List[AgentMessage]) -> str: + async def select_speaker(self, thread: List[AgentEvent | ChatMessage]) -> str: """Select a speaker from the participants based on handoff message. Looks for the last handoff message in the thread to determine the next speaker.""" if len(thread) == 0: diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/ui/_console.py b/python/packages/autogen-agentchat/src/autogen_agentchat/ui/_console.py index 364526386f24..6315b504977c 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/ui/_console.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/ui/_console.py @@ -7,7 +7,7 @@ from autogen_core.models import RequestUsage from autogen_agentchat.base import Response, TaskResult -from autogen_agentchat.messages import AgentMessage, MultiModalMessage +from autogen_agentchat.messages import AgentEvent, ChatMessage, MultiModalMessage def _is_running_in_iterm() -> bool: @@ -22,7 +22,7 @@ def _is_output_a_tty() -> bool: async def Console( - stream: AsyncGenerator[AgentMessage | T, None], + stream: AsyncGenerator[AgentEvent | ChatMessage | T, None], *, no_inline_images: bool = False, ) -> T: @@ -32,7 +32,7 @@ async def Console( Returns the last processed TaskResult or Response. Args: - stream (AsyncGenerator[AgentMessage | TaskResult, None] | AsyncGenerator[AgentMessage | Response, None]): Message stream to render. + stream (AsyncGenerator[AgentEvent | ChatMessage | TaskResult, None] | AsyncGenerator[AgentEvent | ChatMessage | Response, None]): Message stream to render. This can be from :meth:`~autogen_agentchat.base.TaskRunner.run_stream` or :meth:`~autogen_agentchat.base.ChatAgent.on_messages_stream`. no_inline_images (bool, optional): If terminal is iTerm2 will render images inline. Use this to disable this behavior. Defaults to False. @@ -93,7 +93,7 @@ async def Console( else: # Cast required for mypy to be happy - message = cast(AgentMessage, message) # type: ignore + message = cast(AgentEvent | ChatMessage, message) # type: ignore output = f"{'-' * 10} {message.source} {'-' * 10}\n{_message_to_str(message, render_image_iterm=render_image_iterm)}\n" if message.models_usage: output += f"[Prompt tokens: {message.models_usage.prompt_tokens}, Completion tokens: {message.models_usage.completion_tokens}]\n" @@ -114,7 +114,7 @@ def _image_to_iterm(image: Image) -> str: return f"\033]1337;File=inline=1:{image_data}\a\n" -def _message_to_str(message: AgentMessage, *, render_image_iterm: bool = False) -> str: +def _message_to_str(message: AgentEvent | ChatMessage, *, render_image_iterm: bool = False) -> str: if isinstance(message, MultiModalMessage): result: List[str] = [] for c in message.content: diff --git a/python/packages/autogen-agentchat/tests/test_assistant_agent.py b/python/packages/autogen-agentchat/tests/test_assistant_agent.py index c132e3a4862c..67969cfce8a7 100644 --- a/python/packages/autogen-agentchat/tests/test_assistant_agent.py +++ b/python/packages/autogen-agentchat/tests/test_assistant_agent.py @@ -12,8 +12,8 @@ HandoffMessage, MultiModalMessage, TextMessage, - ToolCallMessage, - ToolCallResultMessage, + ToolCallExecutionEvent, + ToolCallRequestEvent, ) from autogen_core import Image from autogen_core.tools import FunctionTool @@ -136,11 +136,11 @@ async def test_run_with_tools(monkeypatch: pytest.MonkeyPatch) -> None: assert len(result.messages) == 4 assert isinstance(result.messages[0], TextMessage) assert result.messages[0].models_usage is None - assert isinstance(result.messages[1], ToolCallMessage) + assert isinstance(result.messages[1], ToolCallRequestEvent) assert result.messages[1].models_usage is not None assert result.messages[1].models_usage.completion_tokens == 5 assert result.messages[1].models_usage.prompt_tokens == 10 - assert isinstance(result.messages[2], ToolCallResultMessage) + assert isinstance(result.messages[2], ToolCallExecutionEvent) assert result.messages[2].models_usage is None assert isinstance(result.messages[3], TextMessage) assert result.messages[3].content == "pass" @@ -235,11 +235,11 @@ async def test_run_with_tools_and_reflection(monkeypatch: pytest.MonkeyPatch) -> assert len(result.messages) == 4 assert isinstance(result.messages[0], TextMessage) assert result.messages[0].models_usage is None - assert isinstance(result.messages[1], ToolCallMessage) + assert isinstance(result.messages[1], ToolCallRequestEvent) assert result.messages[1].models_usage is not None assert result.messages[1].models_usage.completion_tokens == 5 assert result.messages[1].models_usage.prompt_tokens == 10 - assert isinstance(result.messages[2], ToolCallResultMessage) + assert isinstance(result.messages[2], ToolCallExecutionEvent) assert result.messages[2].models_usage is None assert isinstance(result.messages[3], TextMessage) assert result.messages[3].content == "Hello" @@ -323,11 +323,11 @@ async def test_handoffs(monkeypatch: pytest.MonkeyPatch) -> None: assert len(result.messages) == 4 assert isinstance(result.messages[0], TextMessage) assert result.messages[0].models_usage is None - assert isinstance(result.messages[1], ToolCallMessage) + assert isinstance(result.messages[1], ToolCallRequestEvent) assert result.messages[1].models_usage is not None assert result.messages[1].models_usage.completion_tokens == 43 assert result.messages[1].models_usage.prompt_tokens == 42 - assert isinstance(result.messages[2], ToolCallResultMessage) + assert isinstance(result.messages[2], ToolCallExecutionEvent) assert result.messages[2].models_usage is None assert isinstance(result.messages[3], HandoffMessage) assert result.messages[3].content == handoff.message diff --git a/python/packages/autogen-agentchat/tests/test_group_chat.py b/python/packages/autogen-agentchat/tests/test_group_chat.py index 6a3c91b805c3..d3a0f2e56f98 100644 --- a/python/packages/autogen-agentchat/tests/test_group_chat.py +++ b/python/packages/autogen-agentchat/tests/test_group_chat.py @@ -14,14 +14,14 @@ from autogen_agentchat.base import Handoff, Response, TaskResult from autogen_agentchat.conditions import HandoffTermination, MaxMessageTermination, TextMentionTermination from autogen_agentchat.messages import ( - AgentMessage, + AgentEvent, ChatMessage, HandoffMessage, MultiModalMessage, StopMessage, TextMessage, - ToolCallMessage, - ToolCallResultMessage, + ToolCallExecutionEvent, + ToolCallRequestEvent, ) from autogen_agentchat.teams import ( RoundRobinGroupChat, @@ -323,8 +323,8 @@ async def test_round_robin_group_chat_with_tools(monkeypatch: pytest.MonkeyPatch ) assert len(result.messages) == 8 assert isinstance(result.messages[0], TextMessage) # task - assert isinstance(result.messages[1], ToolCallMessage) # tool call - assert isinstance(result.messages[2], ToolCallResultMessage) # tool call result + assert isinstance(result.messages[1], ToolCallRequestEvent) # tool call + assert isinstance(result.messages[2], ToolCallExecutionEvent) # tool call result assert isinstance(result.messages[3], TextMessage) # tool use agent response assert isinstance(result.messages[4], TextMessage) # echo agent response assert isinstance(result.messages[5], TextMessage) # tool use agent response @@ -747,7 +747,7 @@ async def test_selector_group_chat_custom_selector(monkeypatch: pytest.MonkeyPat agent3 = _EchoAgent("agent3", description="echo agent 3") agent4 = _EchoAgent("agent4", description="echo agent 4") - def _select_agent(messages: Sequence[AgentMessage]) -> str | None: + def _select_agent(messages: Sequence[AgentEvent | ChatMessage]) -> str | None: if len(messages) == 0: return "agent1" elif messages[-1].source == "agent1": @@ -920,8 +920,8 @@ async def test_swarm_handoff_using_tool_calls(monkeypatch: pytest.MonkeyPatch) - result = await team.run(task="task") assert len(result.messages) == 7 assert result.messages[0].content == "task" - assert isinstance(result.messages[1], ToolCallMessage) - assert isinstance(result.messages[2], ToolCallResultMessage) + assert isinstance(result.messages[1], ToolCallRequestEvent) + assert isinstance(result.messages[2], ToolCallExecutionEvent) assert result.messages[3].content == "handoff to agent2" assert result.messages[4].content == "Transferred to agent1." assert result.messages[5].content == "Hello" diff --git a/python/packages/autogen-core/docs/src/_static/custom.css b/python/packages/autogen-core/docs/src/_static/custom.css index 6023df8b191e..3b32374aa797 100644 --- a/python/packages/autogen-core/docs/src/_static/custom.css +++ b/python/packages/autogen-core/docs/src/_static/custom.css @@ -73,4 +73,15 @@ html[data-theme="light"] .bd-header { .bd-content .sd-tab-set>label { background-color: transparent; border: none; -} \ No newline at end of file +} + +.card-title { + font-size: 1.2rem; + font-weight: bold; +} + +.card-title svg { + font-size: 2rem; + vertical-align: bottom; + margin-right: 5px; +} diff --git a/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-agentchat.html b/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-agentchat.html new file mode 100644 index 000000000000..684eb304ac7f --- /dev/null +++ b/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-agentchat.html @@ -0,0 +1,38 @@ +{# Displays the TOC-subtree for pages nested under the currently active top-level TOCtree element. #} + + diff --git a/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-core.html b/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-core.html new file mode 100644 index 000000000000..17defa6c773a --- /dev/null +++ b/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-core.html @@ -0,0 +1,38 @@ +{# Displays the TOC-subtree for pages nested under the currently active top-level TOCtree element. #} + + \ No newline at end of file diff --git a/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-extensions.html b/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-extensions.html new file mode 100644 index 000000000000..6c252fe3fb9f --- /dev/null +++ b/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-extensions.html @@ -0,0 +1,39 @@ +{# Displays the TOC-subtree for pages nested under the currently active top-level TOCtree element. #} + + \ No newline at end of file diff --git a/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-studio.html b/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-studio.html new file mode 100644 index 000000000000..7408ca37e084 --- /dev/null +++ b/python/packages/autogen-core/docs/src/_templates/sidebar-nav-bs-studio.html @@ -0,0 +1,32 @@ +{# Displays the TOC-subtree for pages nested under the currently active top-level TOCtree element. #} + + \ No newline at end of file diff --git a/python/packages/autogen-core/docs/src/conf.py b/python/packages/autogen-core/docs/src/conf.py index d64f14f863ed..dd4eac7744ba 100644 --- a/python/packages/autogen-core/docs/src/conf.py +++ b/python/packages/autogen-core/docs/src/conf.py @@ -88,15 +88,14 @@ html_theme_options = { - "header_links_before_dropdown": 4, + "header_links_before_dropdown": 6, "navbar_align": "left", - "show_nav_level": 4, "check_switcher": False, # "navbar_start": ["navbar-logo", "version-switcher"], # "switcher": { # "json_url": "/_static/switcher.json", # }, - "show_prev_next": False, + "show_prev_next": True, "icon_links": [ { "name": "GitHub", @@ -133,6 +132,10 @@ html_js_files = ["custom-icon.js", "override-switcher-button.js"] html_sidebars = { "packages/index": [], + "user-guide/core-user-guide/**": ["sidebar-nav-bs-core"], + "user-guide/agentchat-user-guide/**": ["sidebar-nav-bs-agentchat"], + "user-guide/extensions-user-guide/**": ["sidebar-nav-bs-extensions"], + "user-guide/autogenstudio-user-guide/**": ["sidebar-nav-bs-studio"], } html_context = { diff --git a/python/packages/autogen-core/docs/src/index.md b/python/packages/autogen-core/docs/src/index.md index bebde85c1cce..cc10e405693b 100644 --- a/python/packages/autogen-core/docs/src/index.md +++ b/python/packages/autogen-core/docs/src/index.md @@ -43,11 +43,13 @@ A framework for building AI agents and multi-agent applications
-::::{grid} 1 1 2 2 +::::{grid} +:gutter: 2 :::{grid-item-card} :shadow: none :margin: 2 0 0 0 +:columns: 12 12 12 12
@@ -70,9 +72,29 @@ Get Started ``` ::: +:::{grid-item-card} {fas}`palette;pst-color-primary` Studio +:shadow: none +:margin: 2 0 0 0 +:columns: 12 12 12 12 + +No-code platform for authoring and interacting with multi-agent teams. + ++++ + +```{button-ref} user-guide/autogenstudio-user-guide/index +:color: secondary + +Get Started +``` + +::: + + :::{grid-item-card} {fas}`cube;pst-color-primary` Core :shadow: none :margin: 2 0 0 0 +:columns: 12 12 6 6 + Provides building blocks for creating asynchronous, event driven multi-agent systems. @@ -89,6 +111,29 @@ Get Started ``` ::: + +:::{grid-item-card} {fas}`puzzle-piece;pst-color-primary` Extensions +:shadow: none +:margin: 2 0 0 0 +:columns: 12 12 6 6 + + +Implementations of core components that interface with external services, or use extra dependencies. For example, Docker based code execution. + +```sh +pip install 'autogen-ext==0.4.0.dev11' +``` + ++++ + +```{button-ref} user-guide/extensions-user-guide/index +:color: secondary + +Get Started +``` + +::: + ::::
@@ -97,7 +142,9 @@ Get Started :maxdepth: 3 :hidden: -user-guide/index -packages/index +user-guide/agentchat-user-guide/index +user-guide/core-user-guide/index +user-guide/extensions-user-guide/index +Studio reference/index ``` diff --git a/python/packages/autogen-core/docs/src/packages/index.md b/python/packages/autogen-core/docs/src/packages/index.md deleted file mode 100644 index 913ce842e5c8..000000000000 --- a/python/packages/autogen-core/docs/src/packages/index.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -myst: - html_meta: - "description lang=en": | - AutoGen packages provide a set of functionality for building multi-agent applications with AI agents. ---- - - - -# Packages - -## 0.4 - -(pkg-info-autogen-agentchat)= - -:::{card} {fas}`people-group;pst-color-primary` AutoGen AgentChat -:class-title: card-title -:shadow: none - -Library that is at a similar level of abstraction as AutoGen 0.2, including default agents and group chat. - -```sh -pip install 'autogen-agentchat==0.4.0.dev11' -``` - -[{fas}`circle-info;pst-color-primary` User Guide](/user-guide/agentchat-user-guide/index.md) | [{fas}`file-code;pst-color-primary` API Reference](/reference/python/autogen_agentchat.rst) | [{fab}`python;pst-color-primary` PyPI](https://pypi.org/project/autogen-agentchat/0.4.0.dev11/) | [{fab}`github;pst-color-primary` Source](https://github.com/microsoft/autogen/tree/main/python/packages/autogen-agentchat) -::: - -(pkg-info-autogen-core)= - -:::{card} {fas}`cube;pst-color-primary` AutoGen Core -:class-title: card-title -:shadow: none - -Implements the core functionality of the AutoGen framework, providing basic building blocks for creating multi-agent systems. - -```sh -pip install 'autogen-core==0.4.0.dev11' -``` - -[{fas}`circle-info;pst-color-primary` User Guide](/user-guide/core-user-guide/index.md) | [{fas}`file-code;pst-color-primary` API Reference](/reference/python/autogen_core.rst) | [{fab}`python;pst-color-primary` PyPI](https://pypi.org/project/autogen-core/0.4.0.dev11/) | [{fab}`github;pst-color-primary` Source](https://github.com/microsoft/autogen/tree/main/python/packages/autogen-core) -::: - -(pkg-info-autogen-ext)= - -:::{card} {fas}`puzzle-piece;pst-color-primary` AutoGen Extensions -:class-title: card-title -:shadow: none - -Implementations of core components that interface with external services, or use extra dependencies. For example, Docker based code execution. - -```sh -pip install 'autogen-ext==0.4.0.dev11' -``` - -Extras: - -- `langchain` needed for {py:class}`~autogen_ext.tools.LangChainToolAdapter` -- `azure` needed for {py:class}`~autogen_ext.code_executors.azure.ACADynamicSessionsCodeExecutor` -- `docker` needed for {py:class}`~autogen_ext.code_executors.docker.DockerCommandLineCodeExecutor` -- `openai` needed for {py:class}`~autogen_ext.models.OpenAIChatCompletionClient` - -[{fas}`circle-info;pst-color-primary` User Guide](/user-guide/extensions-user-guide/index.md) | [{fas}`file-code;pst-color-primary` API Reference](/reference/python/autogen_ext.agents.web_surfer.rst) | [{fab}`python;pst-color-primary` PyPI](https://pypi.org/project/autogen-ext/0.4.0.dev11/) | [{fab}`github;pst-color-primary` Source](https://github.com/microsoft/autogen/tree/main/python/packages/autogen-ext) -::: - -(pkg-info-autogen-magentic-one)= - -:::{card} {fas}`users;pst-color-primary` Magentic One -:class-title: card-title -:shadow: none - -A generalist multi-agent softbot utilizing five agents to tackle intricate tasks involving multi-step planning and real-world actions. - -```{note} -Not yet available on PyPI. Please install from source. -``` - -[{fab}`github;pst-color-primary` Source](https://github.com/microsoft/autogen/tree/main/python/packages/autogen-magentic-one) -::: - -## 0.2 - -(pkg-info-autogen-02)= - -:::{card} {fas}`robot;pst-color-primary` AutoGen -:class-title: card-title -:shadow: none -Existing AutoGen library that provides a high-level abstraction for building multi-agent systems. - -```sh -pip install 'autogen-agentchat~=0.2' -``` - -[{fas}`circle-info;pst-color-primary` Documentation](https://microsoft.github.io/autogen/0.2/) | [{fab}`python;pst-color-primary` PyPI](https://pypi.org/project/autogen-agentchat/0.2.38/) | [{fab}`github;pst-color-primary` Source](https://github.com/microsoft/autogen/tree/0.2/) -::: - -## Other - -(pkg-info-autogenbench)= - -:::{card} {fas}`chart-bar;pst-color-primary` AutoGen Bench -:class-title: card-title -:shadow: none - -AutoGenBench is a tool for repeatedly running pre-defined AutoGen tasks in tightly-controlled initial conditions. - -```sh -pip install autogenbench -``` - -[{fab}`python;pst-color-primary` PyPI](https://pypi.org/project/autogenbench/) | [{fab}`github;pst-color-primary` Source](https://github.com/microsoft/autogen/tree/main/python/packages/agbench) -::: diff --git a/python/packages/autogen-core/docs/src/reference/python/autogen_core.tools.rst b/python/packages/autogen-core/docs/src/reference/python/autogen_core.tools.rst index d4cba212476b..c78d6a750614 100644 --- a/python/packages/autogen-core/docs/src/reference/python/autogen_core.tools.rst +++ b/python/packages/autogen-core/docs/src/reference/python/autogen_core.tools.rst @@ -1,5 +1,5 @@ autogen\_core.tools -============================== +=================== .. automodule:: autogen_core.tools diff --git a/python/packages/autogen-core/docs/src/reference/python/autogen_ext.tools.langchain.rst b/python/packages/autogen-core/docs/src/reference/python/autogen_ext.tools.langchain.rst index 68a3291e6a61..416a3b550e6c 100644 --- a/python/packages/autogen-core/docs/src/reference/python/autogen_ext.tools.langchain.rst +++ b/python/packages/autogen-core/docs/src/reference/python/autogen_ext.tools.langchain.rst @@ -1,8 +1,8 @@ -autogen\_ext.tools -================== +autogen\_ext.tools.langchain +============================ -.. automodule:: autogen_ext.tools +.. automodule:: autogen_ext.tools.langchain :members: :undoc-members: :show-inheritance: diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/company-research.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/company-research.ipynb index a9959ccc158a..2111dd098e93 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/company-research.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/company-research.ipynb @@ -1,416 +1,416 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Company Research \n", - "\n", - "\n", - "Conducting company research, or competitive analysis, is a critical part of any business strategy. In this notebook, we will demonstrate how to create a team of agents to address this task. While there are many ways to translate a task into an agentic implementation, we will explore a sequential approach. We will create agents corresponding to steps in the research process and give them tools to perform their tasks.\n", - "\n", - "- **Search Agent**: Searches the web for information about a company. Will have access to a search engine API tool to retrieve search results.\n", - "- **Stock Analysis Agent**: Retrieves the company's stock information from a financial data API, computes basic statistics (current price, 52-week high, 52-week low, etc.), and generates a plot of the stock price year-to-date, saving it to a file. Will have access to a financial data API tool to retrieve stock information.\n", - "- **Report Agent**: Generates a report based on the information collected by the search and stock analysis agents. \n", - "\n", - "First, let's import the necessary modules." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_agentchat.agents import AssistantAgent\n", - "from autogen_agentchat.conditions import TextMentionTermination\n", - "from autogen_agentchat.teams import RoundRobinGroupChat\n", - "from autogen_agentchat.ui import Console\n", - "from autogen_core.tools import FunctionTool\n", - "from autogen_ext.models.openai import OpenAIChatCompletionClient" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Defining Tools \n", - "\n", - "Next, we will define the tools that the agents will use to perform their tasks. We will create a `google_search` that uses the Google Search API to search the web for information about a company. We will also create a `analyze_stock` function that uses the `yfinance` library to retrieve stock information for a company. \n", - "\n", - "Finally, we will wrap these functions into a `FunctionTool` class that will allow us to use them as tools in our agents. \n", - "\n", - "Note: The `google_search` function requires an API key to work. You can create a `.env` file in the same directory as this notebook and add your API key as \n", - "\n", - "```\n", - "GOOGLE_SEARCH_ENGINE_ID =xxx\n", - "GOOGLE_API_KEY=xxx \n", - "``` \n", - "\n", - "Also install required libraries \n", - "\n", - "```\n", - "pip install yfinance matplotlib pytz numpy pandas python-dotenv requests bs4\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "#!pip install yfinance matplotlib pytz numpy pandas python-dotenv requests bs4\n", - "\n", - "\n", - "def google_search(query: str, num_results: int = 2, max_chars: int = 500) -> list: # type: ignore[type-arg]\n", - " import os\n", - " import time\n", - "\n", - " import requests\n", - " from bs4 import BeautifulSoup\n", - " from dotenv import load_dotenv\n", - "\n", - " load_dotenv()\n", - "\n", - " api_key = os.getenv(\"GOOGLE_API_KEY\")\n", - " search_engine_id = os.getenv(\"GOOGLE_SEARCH_ENGINE_ID\")\n", - "\n", - " if not api_key or not search_engine_id:\n", - " raise ValueError(\"API key or Search Engine ID not found in environment variables\")\n", - "\n", - " url = \"https://customsearch.googleapis.com/customsearch/v1\"\n", - " params = {\"key\": str(api_key), \"cx\": str(search_engine_id), \"q\": str(query), \"num\": str(num_results)}\n", - "\n", - " response = requests.get(url, params=params)\n", - "\n", - " if response.status_code != 200:\n", - " print(response.json())\n", - " raise Exception(f\"Error in API request: {response.status_code}\")\n", - "\n", - " results = response.json().get(\"items\", [])\n", - "\n", - " def get_page_content(url: str) -> str:\n", - " try:\n", - " response = requests.get(url, timeout=10)\n", - " soup = BeautifulSoup(response.content, \"html.parser\")\n", - " text = soup.get_text(separator=\" \", strip=True)\n", - " words = text.split()\n", - " content = \"\"\n", - " for word in words:\n", - " if len(content) + len(word) + 1 > max_chars:\n", - " break\n", - " content += \" \" + word\n", - " return content.strip()\n", - " except Exception as e:\n", - " print(f\"Error fetching {url}: {str(e)}\")\n", - " return \"\"\n", - "\n", - " enriched_results = []\n", - " for item in results:\n", - " body = get_page_content(item[\"link\"])\n", - " enriched_results.append(\n", - " {\"title\": item[\"title\"], \"link\": item[\"link\"], \"snippet\": item[\"snippet\"], \"body\": body}\n", - " )\n", - " time.sleep(1) # Be respectful to the servers\n", - "\n", - " return enriched_results\n", - "\n", - "\n", - "def analyze_stock(ticker: str) -> dict: # type: ignore[type-arg]\n", - " import os\n", - " from datetime import datetime, timedelta\n", - "\n", - " import matplotlib.pyplot as plt\n", - " import numpy as np\n", - " import pandas as pd\n", - " import yfinance as yf\n", - " from pytz import timezone # type: ignore\n", - "\n", - " stock = yf.Ticker(ticker)\n", - "\n", - " # Get historical data (1 year of data to ensure we have enough for 200-day MA)\n", - " end_date = datetime.now(timezone(\"UTC\"))\n", - " start_date = end_date - timedelta(days=365)\n", - " hist = stock.history(start=start_date, end=end_date)\n", - "\n", - " # Ensure we have data\n", - " if hist.empty:\n", - " return {\"error\": \"No historical data available for the specified ticker.\"}\n", - "\n", - " # Compute basic statistics and additional metrics\n", - " current_price = stock.info.get(\"currentPrice\", hist[\"Close\"].iloc[-1])\n", - " year_high = stock.info.get(\"fiftyTwoWeekHigh\", hist[\"High\"].max())\n", - " year_low = stock.info.get(\"fiftyTwoWeekLow\", hist[\"Low\"].min())\n", - "\n", - " # Calculate 50-day and 200-day moving averages\n", - " ma_50 = hist[\"Close\"].rolling(window=50).mean().iloc[-1]\n", - " ma_200 = hist[\"Close\"].rolling(window=200).mean().iloc[-1]\n", - "\n", - " # Calculate YTD price change and percent change\n", - " ytd_start = datetime(end_date.year, 1, 1, tzinfo=timezone(\"UTC\"))\n", - " ytd_data = hist.loc[ytd_start:] # type: ignore[misc]\n", - " if not ytd_data.empty:\n", - " price_change = ytd_data[\"Close\"].iloc[-1] - ytd_data[\"Close\"].iloc[0]\n", - " percent_change = (price_change / ytd_data[\"Close\"].iloc[0]) * 100\n", - " else:\n", - " price_change = percent_change = np.nan\n", - "\n", - " # Determine trend\n", - " if pd.notna(ma_50) and pd.notna(ma_200):\n", - " if ma_50 > ma_200:\n", - " trend = \"Upward\"\n", - " elif ma_50 < ma_200:\n", - " trend = \"Downward\"\n", - " else:\n", - " trend = \"Neutral\"\n", - " else:\n", - " trend = \"Insufficient data for trend analysis\"\n", - "\n", - " # Calculate volatility (standard deviation of daily returns)\n", - " daily_returns = hist[\"Close\"].pct_change().dropna()\n", - " volatility = daily_returns.std() * np.sqrt(252) # Annualized volatility\n", - "\n", - " # Create result dictionary\n", - " result = {\n", - " \"ticker\": ticker,\n", - " \"current_price\": current_price,\n", - " \"52_week_high\": year_high,\n", - " \"52_week_low\": year_low,\n", - " \"50_day_ma\": ma_50,\n", - " \"200_day_ma\": ma_200,\n", - " \"ytd_price_change\": price_change,\n", - " \"ytd_percent_change\": percent_change,\n", - " \"trend\": trend,\n", - " \"volatility\": volatility,\n", - " }\n", - "\n", - " # Convert numpy types to Python native types for better JSON serialization\n", - " for key, value in result.items():\n", - " if isinstance(value, np.generic):\n", - " result[key] = value.item()\n", - "\n", - " # Generate plot\n", - " plt.figure(figsize=(12, 6))\n", - " plt.plot(hist.index, hist[\"Close\"], label=\"Close Price\")\n", - " plt.plot(hist.index, hist[\"Close\"].rolling(window=50).mean(), label=\"50-day MA\")\n", - " plt.plot(hist.index, hist[\"Close\"].rolling(window=200).mean(), label=\"200-day MA\")\n", - " plt.title(f\"{ticker} Stock Price (Past Year)\")\n", - " plt.xlabel(\"Date\")\n", - " plt.ylabel(\"Price ($)\")\n", - " plt.legend()\n", - " plt.grid(True)\n", - "\n", - " # Save plot to file\n", - " os.makedirs(\"coding\", exist_ok=True)\n", - " plot_file_path = f\"coding/{ticker}_stockprice.png\"\n", - " plt.savefig(plot_file_path)\n", - " print(f\"Plot saved as {plot_file_path}\")\n", - " result[\"plot_file_path\"] = plot_file_path\n", - "\n", - " return result" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "google_search_tool = FunctionTool(\n", - " google_search, description=\"Search Google for information, returns results with a snippet and body content\"\n", - ")\n", - "stock_analysis_tool = FunctionTool(analyze_stock, description=\"Analyze stock data and generate a plot\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Defining Agents\n", - "\n", - "Next, we will define the agents that will perform the tasks. We will create a `search_agent` that searches the web for information about a company, a `stock_analysis_agent` that retrieves stock information for a company, and a `report_agent` that generates a report based on the information collected by the other agents. " - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "search_agent = AssistantAgent(\n", - " name=\"Google_Search_Agent\",\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o\"),\n", - " tools=[google_search_tool],\n", - " description=\"Search Google for information, returns top 2 results with a snippet and body content\",\n", - " system_message=\"You are a helpful AI assistant. Solve tasks using your tools.\",\n", - ")\n", - "\n", - "stock_analysis_agent = AssistantAgent(\n", - " name=\"Stock_Analysis_Agent\",\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o\"),\n", - " tools=[stock_analysis_tool],\n", - " description=\"Analyze stock data and generate a plot\",\n", - " system_message=\"Perform data analysis.\",\n", - ")\n", - "\n", - "report_agent = AssistantAgent(\n", - " name=\"Report_Agent\",\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o\"),\n", - " description=\"Generate a report based the search and results of stock analysis\",\n", - " system_message=\"You are a helpful assistant that can generate a comprehensive report on a given topic based on search and stock analysis. When you done with generating the report, reply with TERMINATE.\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Creating the Team\n", - "\n", - "Finally, let's create a team of the three agents and set them to work on researching a company." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "team = RoundRobinGroupChat([stock_analysis_agent, search_agent, report_agent], max_turns=3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We use `max_turns=3` to limit the number of turns to exactly the same number of agents in the team. This effectively makes the agents work in a sequential manner." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "Write a financial report on American airlines\n", - "---------- Stock_Analysis_Agent ----------\n", - "[FunctionCall(id='call_tPh9gSfGrDu1nC2Ck5RlfbFY', arguments='{\"ticker\":\"AAL\"}', name='analyze_stock')]\n", - "[Prompt tokens: 64, Completion tokens: 16]\n", - "Plot saved as coding/AAL_stockprice.png\n", - "---------- Stock_Analysis_Agent ----------\n", - "[FunctionExecutionResult(content=\"{'ticker': 'AAL', 'current_price': 17.4, '52_week_high': 18.09, '52_week_low': 9.07, '50_day_ma': 13.376799983978271, '200_day_ma': 12.604399962425232, 'ytd_price_change': 3.9600000381469727, 'ytd_percent_change': 29.46428691803602, 'trend': 'Upward', 'volatility': 0.4461582174242901, 'plot_file_path': 'coding/AAL_stockprice.png'}\", call_id='call_tPh9gSfGrDu1nC2Ck5RlfbFY')]\n", - "---------- Stock_Analysis_Agent ----------\n", - "Tool calls:\n", - "analyze_stock({\"ticker\":\"AAL\"}) = {'ticker': 'AAL', 'current_price': 17.4, '52_week_high': 18.09, '52_week_low': 9.07, '50_day_ma': 13.376799983978271, '200_day_ma': 12.604399962425232, 'ytd_price_change': 3.9600000381469727, 'ytd_percent_change': 29.46428691803602, 'trend': 'Upward', 'volatility': 0.4461582174242901, 'plot_file_path': 'coding/AAL_stockprice.png'}\n", - "---------- Google_Search_Agent ----------\n", - "[FunctionCall(id='call_wSHc5Kw1ix3aQDXXT23opVnU', arguments='{\"query\":\"American Airlines financial report 2023\",\"num_results\":1}', name='google_search')]\n", - "[Prompt tokens: 268, Completion tokens: 25]\n", - "---------- Google_Search_Agent ----------\n", - "[FunctionExecutionResult(content=\"[{'title': 'American Airlines reports fourth-quarter and full-year 2023 financial ...', 'link': 'https://news.aa.com/news/news-details/2024/American-Airlines-reports-fourth-quarter-and-full-year-2023-financial-results-CORP-FI-01/default.aspx', 'snippet': 'Jan 25, 2024 ... American Airlines Group Inc. (NASDAQ: AAL) today reported its fourth-quarter and full-year 2023 financial results, including: Record\\\\xa0...', 'body': 'Just a moment... Enable JavaScript and cookies to continue'}]\", call_id='call_wSHc5Kw1ix3aQDXXT23opVnU')]\n", - "---------- Google_Search_Agent ----------\n", - "Tool calls:\n", - "google_search({\"query\":\"American Airlines financial report 2023\",\"num_results\":1}) = [{'title': 'American Airlines reports fourth-quarter and full-year 2023 financial ...', 'link': 'https://news.aa.com/news/news-details/2024/American-Airlines-reports-fourth-quarter-and-full-year-2023-financial-results-CORP-FI-01/default.aspx', 'snippet': 'Jan 25, 2024 ... American Airlines Group Inc. (NASDAQ: AAL) today reported its fourth-quarter and full-year 2023 financial results, including: Record\\xa0...', 'body': 'Just a moment... Enable JavaScript and cookies to continue'}]\n", - "---------- Report_Agent ----------\n", - "### American Airlines Financial Report\n", - "\n", - "#### Overview\n", - "American Airlines Group Inc. (NASDAQ: AAL) is a major American airline headquartered in Fort Worth, Texas. It is known as one of the largest airlines in the world by fleet size, revenue, and passenger kilometers flown. As of the current quarter in 2023, American Airlines has shown significant financial activities and stock performance noteworthy for investors and analysts.\n", - "\n", - "#### Stock Performance\n", - "- **Current Stock Price**: $17.40\n", - "- **52-Week Range**: The stock price has ranged from $9.07 to $18.09 over the past year, indicating considerable volatility and fluctuation in market interest.\n", - "- **Moving Averages**: \n", - " - 50-Day MA: $13.38\n", - " - 200-Day MA: $12.60\n", - " These moving averages suggest a strong upward trend in recent months as the 50-day moving average is positioned above the 200-day moving average, indicating bullish momentum.\n", - "\n", - "- **YTD Price Change**: $3.96\n", - "- **YTD Percent Change**: 29.46%\n", - " The year-to-date figures demonstrate a robust upward momentum, with the stock appreciating by nearly 29.5% since the beginning of the year.\n", - "\n", - "- **Trend**: The current stock trend for American Airlines is upward, reflecting positive market sentiment and performance improvements.\n", - "\n", - "- **Volatility**: 0.446, indicating moderate volatility in the stock, which may attract risk-tolerant investors seeking dynamic movements for potential profit.\n", - "\n", - "#### Recent Financial Performance\n", - "According to the latest financial reports of 2023 (accessed through a reliable source), American Airlines reported remarkable figures for both the fourth quarter and the full year 2023. Key highlights from the report include:\n", - "\n", - "- **Revenue Growth**: American Airlines experienced substantial revenue increases, driven by high demand for travel as pandemic-related restrictions eased globally.\n", - "- **Profit Margins**: The company managed to enhance its profitability, largely attributed to cost management strategies and increased operational efficiency.\n", - "- **Challenges**: Despite positive momentum, the airline industry faces ongoing challenges including fluctuating fuel prices, geopolitical tensions, and competition pressures.\n", - "\n", - "#### Strategic Initiatives\n", - "American Airlines has been focusing on several strategic initiatives to maintain its market leadership and improve its financial metrics:\n", - "1. **Fleet Modernization**: Continuation of investment in more fuel-efficient aircraft to reduce operating costs and environmental impact.\n", - "2. **Enhanced Customer Experience**: Introduction of new services and technology enhancements aimed at improving customer satisfaction.\n", - "3. **Operational Efficiency**: Streamlining processes to cut costs and increase overall effectiveness, which includes leveraging data analytics for better decision-making.\n", - "\n", - "#### Conclusion\n", - "American Airlines is demonstrating strong market performance and financial growth amid an evolving industry landscape. The company's stock has been on an upward trend, reflecting its solid operational strategies and recovery efforts post-COVID pandemic. Investors should remain mindful of external risks while considering American Airlines as a potential investment, supported by its current upward trajectory and strategic initiatives.\n", - "\n", - "For further details, investors are encouraged to review the full financial reports from American Airlines and assess ongoing market conditions.\n", - "\n", - "_TERMINATE_\n", - "[Prompt tokens: 360, Completion tokens: 633]\n", - "---------- Summary ----------\n", - "Number of messages: 8\n", - "Finish reason: Maximum number of turns 3 reached.\n", - "Total prompt tokens: 692\n", - "Total completion tokens: 674\n", - "Duration: 19.38 seconds\n" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Company Research \n", + "\n", + "\n", + "Conducting company research, or competitive analysis, is a critical part of any business strategy. In this notebook, we will demonstrate how to create a team of agents to address this task. While there are many ways to translate a task into an agentic implementation, we will explore a sequential approach. We will create agents corresponding to steps in the research process and give them tools to perform their tasks.\n", + "\n", + "- **Search Agent**: Searches the web for information about a company. Will have access to a search engine API tool to retrieve search results.\n", + "- **Stock Analysis Agent**: Retrieves the company's stock information from a financial data API, computes basic statistics (current price, 52-week high, 52-week low, etc.), and generates a plot of the stock price year-to-date, saving it to a file. Will have access to a financial data API tool to retrieve stock information.\n", + "- **Report Agent**: Generates a report based on the information collected by the search and stock analysis agents. \n", + "\n", + "First, let's import the necessary modules." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.conditions import TextMentionTermination\n", + "from autogen_agentchat.teams import RoundRobinGroupChat\n", + "from autogen_agentchat.ui import Console\n", + "from autogen_core.tools import FunctionTool\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining Tools \n", + "\n", + "Next, we will define the tools that the agents will use to perform their tasks. We will create a `google_search` that uses the Google Search API to search the web for information about a company. We will also create a `analyze_stock` function that uses the `yfinance` library to retrieve stock information for a company. \n", + "\n", + "Finally, we will wrap these functions into a `FunctionTool` class that will allow us to use them as tools in our agents. \n", + "\n", + "Note: The `google_search` function requires an API key to work. You can create a `.env` file in the same directory as this notebook and add your API key as \n", + "\n", + "```\n", + "GOOGLE_SEARCH_ENGINE_ID =xxx\n", + "GOOGLE_API_KEY=xxx \n", + "``` \n", + "\n", + "Also install required libraries \n", + "\n", + "```\n", + "pip install yfinance matplotlib pytz numpy pandas python-dotenv requests bs4\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "#!pip install yfinance matplotlib pytz numpy pandas python-dotenv requests bs4\n", + "\n", + "\n", + "def google_search(query: str, num_results: int = 2, max_chars: int = 500) -> list: # type: ignore[type-arg]\n", + " import os\n", + " import time\n", + "\n", + " import requests\n", + " from bs4 import BeautifulSoup\n", + " from dotenv import load_dotenv\n", + "\n", + " load_dotenv()\n", + "\n", + " api_key = os.getenv(\"GOOGLE_API_KEY\")\n", + " search_engine_id = os.getenv(\"GOOGLE_SEARCH_ENGINE_ID\")\n", + "\n", + " if not api_key or not search_engine_id:\n", + " raise ValueError(\"API key or Search Engine ID not found in environment variables\")\n", + "\n", + " url = \"https://customsearch.googleapis.com/customsearch/v1\"\n", + " params = {\"key\": str(api_key), \"cx\": str(search_engine_id), \"q\": str(query), \"num\": str(num_results)}\n", + "\n", + " response = requests.get(url, params=params)\n", + "\n", + " if response.status_code != 200:\n", + " print(response.json())\n", + " raise Exception(f\"Error in API request: {response.status_code}\")\n", + "\n", + " results = response.json().get(\"items\", [])\n", + "\n", + " def get_page_content(url: str) -> str:\n", + " try:\n", + " response = requests.get(url, timeout=10)\n", + " soup = BeautifulSoup(response.content, \"html.parser\")\n", + " text = soup.get_text(separator=\" \", strip=True)\n", + " words = text.split()\n", + " content = \"\"\n", + " for word in words:\n", + " if len(content) + len(word) + 1 > max_chars:\n", + " break\n", + " content += \" \" + word\n", + " return content.strip()\n", + " except Exception as e:\n", + " print(f\"Error fetching {url}: {str(e)}\")\n", + " return \"\"\n", + "\n", + " enriched_results = []\n", + " for item in results:\n", + " body = get_page_content(item[\"link\"])\n", + " enriched_results.append(\n", + " {\"title\": item[\"title\"], \"link\": item[\"link\"], \"snippet\": item[\"snippet\"], \"body\": body}\n", + " )\n", + " time.sleep(1) # Be respectful to the servers\n", + "\n", + " return enriched_results\n", + "\n", + "\n", + "def analyze_stock(ticker: str) -> dict: # type: ignore[type-arg]\n", + " import os\n", + " from datetime import datetime, timedelta\n", + "\n", + " import matplotlib.pyplot as plt\n", + " import numpy as np\n", + " import pandas as pd\n", + " import yfinance as yf\n", + " from pytz import timezone # type: ignore\n", + "\n", + " stock = yf.Ticker(ticker)\n", + "\n", + " # Get historical data (1 year of data to ensure we have enough for 200-day MA)\n", + " end_date = datetime.now(timezone(\"UTC\"))\n", + " start_date = end_date - timedelta(days=365)\n", + " hist = stock.history(start=start_date, end=end_date)\n", + "\n", + " # Ensure we have data\n", + " if hist.empty:\n", + " return {\"error\": \"No historical data available for the specified ticker.\"}\n", + "\n", + " # Compute basic statistics and additional metrics\n", + " current_price = stock.info.get(\"currentPrice\", hist[\"Close\"].iloc[-1])\n", + " year_high = stock.info.get(\"fiftyTwoWeekHigh\", hist[\"High\"].max())\n", + " year_low = stock.info.get(\"fiftyTwoWeekLow\", hist[\"Low\"].min())\n", + "\n", + " # Calculate 50-day and 200-day moving averages\n", + " ma_50 = hist[\"Close\"].rolling(window=50).mean().iloc[-1]\n", + " ma_200 = hist[\"Close\"].rolling(window=200).mean().iloc[-1]\n", + "\n", + " # Calculate YTD price change and percent change\n", + " ytd_start = datetime(end_date.year, 1, 1, tzinfo=timezone(\"UTC\"))\n", + " ytd_data = hist.loc[ytd_start:] # type: ignore[misc]\n", + " if not ytd_data.empty:\n", + " price_change = ytd_data[\"Close\"].iloc[-1] - ytd_data[\"Close\"].iloc[0]\n", + " percent_change = (price_change / ytd_data[\"Close\"].iloc[0]) * 100\n", + " else:\n", + " price_change = percent_change = np.nan\n", + "\n", + " # Determine trend\n", + " if pd.notna(ma_50) and pd.notna(ma_200):\n", + " if ma_50 > ma_200:\n", + " trend = \"Upward\"\n", + " elif ma_50 < ma_200:\n", + " trend = \"Downward\"\n", + " else:\n", + " trend = \"Neutral\"\n", + " else:\n", + " trend = \"Insufficient data for trend analysis\"\n", + "\n", + " # Calculate volatility (standard deviation of daily returns)\n", + " daily_returns = hist[\"Close\"].pct_change().dropna()\n", + " volatility = daily_returns.std() * np.sqrt(252) # Annualized volatility\n", + "\n", + " # Create result dictionary\n", + " result = {\n", + " \"ticker\": ticker,\n", + " \"current_price\": current_price,\n", + " \"52_week_high\": year_high,\n", + " \"52_week_low\": year_low,\n", + " \"50_day_ma\": ma_50,\n", + " \"200_day_ma\": ma_200,\n", + " \"ytd_price_change\": price_change,\n", + " \"ytd_percent_change\": percent_change,\n", + " \"trend\": trend,\n", + " \"volatility\": volatility,\n", + " }\n", + "\n", + " # Convert numpy types to Python native types for better JSON serialization\n", + " for key, value in result.items():\n", + " if isinstance(value, np.generic):\n", + " result[key] = value.item()\n", + "\n", + " # Generate plot\n", + " plt.figure(figsize=(12, 6))\n", + " plt.plot(hist.index, hist[\"Close\"], label=\"Close Price\")\n", + " plt.plot(hist.index, hist[\"Close\"].rolling(window=50).mean(), label=\"50-day MA\")\n", + " plt.plot(hist.index, hist[\"Close\"].rolling(window=200).mean(), label=\"200-day MA\")\n", + " plt.title(f\"{ticker} Stock Price (Past Year)\")\n", + " plt.xlabel(\"Date\")\n", + " plt.ylabel(\"Price ($)\")\n", + " plt.legend()\n", + " plt.grid(True)\n", + "\n", + " # Save plot to file\n", + " os.makedirs(\"coding\", exist_ok=True)\n", + " plot_file_path = f\"coding/{ticker}_stockprice.png\"\n", + " plt.savefig(plot_file_path)\n", + " print(f\"Plot saved as {plot_file_path}\")\n", + " result[\"plot_file_path\"] = plot_file_path\n", + "\n", + " return result" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "google_search_tool = FunctionTool(\n", + " google_search, description=\"Search Google for information, returns results with a snippet and body content\"\n", + ")\n", + "stock_analysis_tool = FunctionTool(analyze_stock, description=\"Analyze stock data and generate a plot\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining Agents\n", + "\n", + "Next, we will define the agents that will perform the tasks. We will create a `search_agent` that searches the web for information about a company, a `stock_analysis_agent` that retrieves stock information for a company, and a `report_agent` that generates a report based on the information collected by the other agents. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "search_agent = AssistantAgent(\n", + " name=\"Google_Search_Agent\",\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o\"),\n", + " tools=[google_search_tool],\n", + " description=\"Search Google for information, returns top 2 results with a snippet and body content\",\n", + " system_message=\"You are a helpful AI assistant. Solve tasks using your tools.\",\n", + ")\n", + "\n", + "stock_analysis_agent = AssistantAgent(\n", + " name=\"Stock_Analysis_Agent\",\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o\"),\n", + " tools=[stock_analysis_tool],\n", + " description=\"Analyze stock data and generate a plot\",\n", + " system_message=\"Perform data analysis.\",\n", + ")\n", + "\n", + "report_agent = AssistantAgent(\n", + " name=\"Report_Agent\",\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o\"),\n", + " description=\"Generate a report based the search and results of stock analysis\",\n", + " system_message=\"You are a helpful assistant that can generate a comprehensive report on a given topic based on search and stock analysis. When you done with generating the report, reply with TERMINATE.\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating the Team\n", + "\n", + "Finally, let's create a team of the three agents and set them to work on researching a company." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "team = RoundRobinGroupChat([stock_analysis_agent, search_agent, report_agent], max_turns=3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use `max_turns=3` to limit the number of turns to exactly the same number of agents in the team. This effectively makes the agents work in a sequential manner." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Write a financial report on American airlines\n", + "---------- Stock_Analysis_Agent ----------\n", + "[FunctionCall(id='call_tPh9gSfGrDu1nC2Ck5RlfbFY', arguments='{\"ticker\":\"AAL\"}', name='analyze_stock')]\n", + "[Prompt tokens: 64, Completion tokens: 16]\n", + "Plot saved as coding/AAL_stockprice.png\n", + "---------- Stock_Analysis_Agent ----------\n", + "[FunctionExecutionResult(content=\"{'ticker': 'AAL', 'current_price': 17.4, '52_week_high': 18.09, '52_week_low': 9.07, '50_day_ma': 13.376799983978271, '200_day_ma': 12.604399962425232, 'ytd_price_change': 3.9600000381469727, 'ytd_percent_change': 29.46428691803602, 'trend': 'Upward', 'volatility': 0.4461582174242901, 'plot_file_path': 'coding/AAL_stockprice.png'}\", call_id='call_tPh9gSfGrDu1nC2Ck5RlfbFY')]\n", + "---------- Stock_Analysis_Agent ----------\n", + "Tool calls:\n", + "analyze_stock({\"ticker\":\"AAL\"}) = {'ticker': 'AAL', 'current_price': 17.4, '52_week_high': 18.09, '52_week_low': 9.07, '50_day_ma': 13.376799983978271, '200_day_ma': 12.604399962425232, 'ytd_price_change': 3.9600000381469727, 'ytd_percent_change': 29.46428691803602, 'trend': 'Upward', 'volatility': 0.4461582174242901, 'plot_file_path': 'coding/AAL_stockprice.png'}\n", + "---------- Google_Search_Agent ----------\n", + "[FunctionCall(id='call_wSHc5Kw1ix3aQDXXT23opVnU', arguments='{\"query\":\"American Airlines financial report 2023\",\"num_results\":1}', name='google_search')]\n", + "[Prompt tokens: 268, Completion tokens: 25]\n", + "---------- Google_Search_Agent ----------\n", + "[FunctionExecutionResult(content=\"[{'title': 'American Airlines reports fourth-quarter and full-year 2023 financial ...', 'link': 'https://news.aa.com/news/news-details/2024/American-Airlines-reports-fourth-quarter-and-full-year-2023-financial-results-CORP-FI-01/default.aspx', 'snippet': 'Jan 25, 2024 ... American Airlines Group Inc. (NASDAQ: AAL) today reported its fourth-quarter and full-year 2023 financial results, including: Record\\\\xa0...', 'body': 'Just a moment... Enable JavaScript and cookies to continue'}]\", call_id='call_wSHc5Kw1ix3aQDXXT23opVnU')]\n", + "---------- Google_Search_Agent ----------\n", + "Tool calls:\n", + "google_search({\"query\":\"American Airlines financial report 2023\",\"num_results\":1}) = [{'title': 'American Airlines reports fourth-quarter and full-year 2023 financial ...', 'link': 'https://news.aa.com/news/news-details/2024/American-Airlines-reports-fourth-quarter-and-full-year-2023-financial-results-CORP-FI-01/default.aspx', 'snippet': 'Jan 25, 2024 ... American Airlines Group Inc. (NASDAQ: AAL) today reported its fourth-quarter and full-year 2023 financial results, including: Record\\xa0...', 'body': 'Just a moment... Enable JavaScript and cookies to continue'}]\n", + "---------- Report_Agent ----------\n", + "### American Airlines Financial Report\n", + "\n", + "#### Overview\n", + "American Airlines Group Inc. (NASDAQ: AAL) is a major American airline headquartered in Fort Worth, Texas. It is known as one of the largest airlines in the world by fleet size, revenue, and passenger kilometers flown. As of the current quarter in 2023, American Airlines has shown significant financial activities and stock performance noteworthy for investors and analysts.\n", + "\n", + "#### Stock Performance\n", + "- **Current Stock Price**: $17.40\n", + "- **52-Week Range**: The stock price has ranged from $9.07 to $18.09 over the past year, indicating considerable volatility and fluctuation in market interest.\n", + "- **Moving Averages**: \n", + " - 50-Day MA: $13.38\n", + " - 200-Day MA: $12.60\n", + " These moving averages suggest a strong upward trend in recent months as the 50-day moving average is positioned above the 200-day moving average, indicating bullish momentum.\n", + "\n", + "- **YTD Price Change**: $3.96\n", + "- **YTD Percent Change**: 29.46%\n", + " The year-to-date figures demonstrate a robust upward momentum, with the stock appreciating by nearly 29.5% since the beginning of the year.\n", + "\n", + "- **Trend**: The current stock trend for American Airlines is upward, reflecting positive market sentiment and performance improvements.\n", + "\n", + "- **Volatility**: 0.446, indicating moderate volatility in the stock, which may attract risk-tolerant investors seeking dynamic movements for potential profit.\n", + "\n", + "#### Recent Financial Performance\n", + "According to the latest financial reports of 2023 (accessed through a reliable source), American Airlines reported remarkable figures for both the fourth quarter and the full year 2023. Key highlights from the report include:\n", + "\n", + "- **Revenue Growth**: American Airlines experienced substantial revenue increases, driven by high demand for travel as pandemic-related restrictions eased globally.\n", + "- **Profit Margins**: The company managed to enhance its profitability, largely attributed to cost management strategies and increased operational efficiency.\n", + "- **Challenges**: Despite positive momentum, the airline industry faces ongoing challenges including fluctuating fuel prices, geopolitical tensions, and competition pressures.\n", + "\n", + "#### Strategic Initiatives\n", + "American Airlines has been focusing on several strategic initiatives to maintain its market leadership and improve its financial metrics:\n", + "1. **Fleet Modernization**: Continuation of investment in more fuel-efficient aircraft to reduce operating costs and environmental impact.\n", + "2. **Enhanced Customer Experience**: Introduction of new services and technology enhancements aimed at improving customer satisfaction.\n", + "3. **Operational Efficiency**: Streamlining processes to cut costs and increase overall effectiveness, which includes leveraging data analytics for better decision-making.\n", + "\n", + "#### Conclusion\n", + "American Airlines is demonstrating strong market performance and financial growth amid an evolving industry landscape. The company's stock has been on an upward trend, reflecting its solid operational strategies and recovery efforts post-COVID pandemic. Investors should remain mindful of external risks while considering American Airlines as a potential investment, supported by its current upward trajectory and strategic initiatives.\n", + "\n", + "For further details, investors are encouraged to review the full financial reports from American Airlines and assess ongoing market conditions.\n", + "\n", + "_TERMINATE_\n", + "[Prompt tokens: 360, Completion tokens: 633]\n", + "---------- Summary ----------\n", + "Number of messages: 8\n", + "Finish reason: Maximum number of turns 3 reached.\n", + "Total prompt tokens: 692\n", + "Total completion tokens: 674\n", + "Duration: 19.38 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a financial report on American airlines', type='TextMessage'), ToolCallRequestEvent(source='Stock_Analysis_Agent', models_usage=RequestUsage(prompt_tokens=64, completion_tokens=16), content=[FunctionCall(id='call_tPh9gSfGrDu1nC2Ck5RlfbFY', arguments='{\"ticker\":\"AAL\"}', name='analyze_stock')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='Stock_Analysis_Agent', models_usage=None, content=[FunctionExecutionResult(content=\"{'ticker': 'AAL', 'current_price': 17.4, '52_week_high': 18.09, '52_week_low': 9.07, '50_day_ma': 13.376799983978271, '200_day_ma': 12.604399962425232, 'ytd_price_change': 3.9600000381469727, 'ytd_percent_change': 29.46428691803602, 'trend': 'Upward', 'volatility': 0.4461582174242901, 'plot_file_path': 'coding/AAL_stockprice.png'}\", call_id='call_tPh9gSfGrDu1nC2Ck5RlfbFY')], type='ToolCallExecutionEvent'), TextMessage(source='Stock_Analysis_Agent', models_usage=None, content='Tool calls:\\nanalyze_stock({\"ticker\":\"AAL\"}) = {\\'ticker\\': \\'AAL\\', \\'current_price\\': 17.4, \\'52_week_high\\': 18.09, \\'52_week_low\\': 9.07, \\'50_day_ma\\': 13.376799983978271, \\'200_day_ma\\': 12.604399962425232, \\'ytd_price_change\\': 3.9600000381469727, \\'ytd_percent_change\\': 29.46428691803602, \\'trend\\': \\'Upward\\', \\'volatility\\': 0.4461582174242901, \\'plot_file_path\\': \\'coding/AAL_stockprice.png\\'}', type='TextMessage'), ToolCallRequestEvent(source='Google_Search_Agent', models_usage=RequestUsage(prompt_tokens=268, completion_tokens=25), content=[FunctionCall(id='call_wSHc5Kw1ix3aQDXXT23opVnU', arguments='{\"query\":\"American Airlines financial report 2023\",\"num_results\":1}', name='google_search')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='Google_Search_Agent', models_usage=None, content=[FunctionExecutionResult(content=\"[{'title': 'American Airlines reports fourth-quarter and full-year 2023 financial ...', 'link': 'https://news.aa.com/news/news-details/2024/American-Airlines-reports-fourth-quarter-and-full-year-2023-financial-results-CORP-FI-01/default.aspx', 'snippet': 'Jan 25, 2024 ... American Airlines Group Inc. (NASDAQ: AAL) today reported its fourth-quarter and full-year 2023 financial results, including: Record\\\\xa0...', 'body': 'Just a moment... Enable JavaScript and cookies to continue'}]\", call_id='call_wSHc5Kw1ix3aQDXXT23opVnU')], type='ToolCallExecutionEvent'), TextMessage(source='Google_Search_Agent', models_usage=None, content='Tool calls:\\ngoogle_search({\"query\":\"American Airlines financial report 2023\",\"num_results\":1}) = [{\\'title\\': \\'American Airlines reports fourth-quarter and full-year 2023 financial ...\\', \\'link\\': \\'https://news.aa.com/news/news-details/2024/American-Airlines-reports-fourth-quarter-and-full-year-2023-financial-results-CORP-FI-01/default.aspx\\', \\'snippet\\': \\'Jan 25, 2024 ... American Airlines Group Inc. (NASDAQ: AAL) today reported its fourth-quarter and full-year 2023 financial results, including: Record\\\\xa0...\\', \\'body\\': \\'Just a moment... Enable JavaScript and cookies to continue\\'}]', type='TextMessage'), TextMessage(source='Report_Agent', models_usage=RequestUsage(prompt_tokens=360, completion_tokens=633), content=\"### American Airlines Financial Report\\n\\n#### Overview\\nAmerican Airlines Group Inc. (NASDAQ: AAL) is a major American airline headquartered in Fort Worth, Texas. It is known as one of the largest airlines in the world by fleet size, revenue, and passenger kilometers flown. As of the current quarter in 2023, American Airlines has shown significant financial activities and stock performance noteworthy for investors and analysts.\\n\\n#### Stock Performance\\n- **Current Stock Price**: $17.40\\n- **52-Week Range**: The stock price has ranged from $9.07 to $18.09 over the past year, indicating considerable volatility and fluctuation in market interest.\\n- **Moving Averages**: \\n - 50-Day MA: $13.38\\n - 200-Day MA: $12.60\\n These moving averages suggest a strong upward trend in recent months as the 50-day moving average is positioned above the 200-day moving average, indicating bullish momentum.\\n\\n- **YTD Price Change**: $3.96\\n- **YTD Percent Change**: 29.46%\\n The year-to-date figures demonstrate a robust upward momentum, with the stock appreciating by nearly 29.5% since the beginning of the year.\\n\\n- **Trend**: The current stock trend for American Airlines is upward, reflecting positive market sentiment and performance improvements.\\n\\n- **Volatility**: 0.446, indicating moderate volatility in the stock, which may attract risk-tolerant investors seeking dynamic movements for potential profit.\\n\\n#### Recent Financial Performance\\nAccording to the latest financial reports of 2023 (accessed through a reliable source), American Airlines reported remarkable figures for both the fourth quarter and the full year 2023. Key highlights from the report include:\\n\\n- **Revenue Growth**: American Airlines experienced substantial revenue increases, driven by high demand for travel as pandemic-related restrictions eased globally.\\n- **Profit Margins**: The company managed to enhance its profitability, largely attributed to cost management strategies and increased operational efficiency.\\n- **Challenges**: Despite positive momentum, the airline industry faces ongoing challenges including fluctuating fuel prices, geopolitical tensions, and competition pressures.\\n\\n#### Strategic Initiatives\\nAmerican Airlines has been focusing on several strategic initiatives to maintain its market leadership and improve its financial metrics:\\n1. **Fleet Modernization**: Continuation of investment in more fuel-efficient aircraft to reduce operating costs and environmental impact.\\n2. **Enhanced Customer Experience**: Introduction of new services and technology enhancements aimed at improving customer satisfaction.\\n3. **Operational Efficiency**: Streamlining processes to cut costs and increase overall effectiveness, which includes leveraging data analytics for better decision-making.\\n\\n#### Conclusion\\nAmerican Airlines is demonstrating strong market performance and financial growth amid an evolving industry landscape. The company's stock has been on an upward trend, reflecting its solid operational strategies and recovery efforts post-COVID pandemic. Investors should remain mindful of external risks while considering American Airlines as a potential investment, supported by its current upward trajectory and strategic initiatives.\\n\\nFor further details, investors are encouraged to review the full financial reports from American Airlines and assess ongoing market conditions.\\n\\n_TERMINATE_\", type='TextMessage')], stop_reason='Maximum number of turns 3 reached.')" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+UAAAIjCAYAAABlBbqXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hUZfbA8e+09EYaSSAFQgm9S5XeEURxUWwgdrHruro/G5Z11bWLurpSVBAriIoUlQ7SlA4BQgglvfdkyv39cWcmCSmkT8r5PM88SWbuzH1ncjOZc895z6tRFEVBCCGEEEIIIYQQjU7r6AEIIYQQQgghhBCtlQTlQgghhBBCCCGEg0hQLoQQQgghhBBCOIgE5UIIIYQQQgghhINIUC6EEEIIIYQQQjiIBOVCCCGEEEIIIYSDSFAuhBBCCCGEEEI4iATlQgghhBBCCCGEg0hQLoQQQgghhBBCOIgE5UIIIUQT8vzzz6PRaEhNTXXI/ufNm0dERIRD9v3aa68RFRWFxWJxyP6bM6PRSGhoKB988IGjhyKEEKKGJCgXQgjR6D744AM0Gg2DBw+u1vZPPPEEGo2G66+/vsLbz549i0aj4T//+U+txvPjjz8yatQoAgMDcXNzo2PHjsyePZt169bZt4mPj+f555/nwIEDtdpHY1i6dCkajcZ+cXFxoUuXLtx///0kJSU5enhVys7O5tVXX+Uf//gHWm3Jx5PSz0er1RISEsLEiRPZvHlzg4zjgw8+YOnSpZfdbuXKlWg0Gv773/9WePu9996LwWDg4MGD9TzCihkMBh599FFefvllCgsLG2WfQggh6ocE5UIIIRrd8uXLiYiIYM+ePZw+fbrKbRVF4csvvyQiIoIff/yRnJyceh3Lf/7zH2bMmIFGo+Gpp57irbfeYtasWZw6dYqVK1fat4uPj2fhwoVNOii3eeGFF/j88895//33GTZsGB9++CFDhw4lPz//svf95JNPiI6OboRRlrV48WJMJhNz5swpd9uECRP4/PPPWbZsGffccw+HDh1i7Nix/PLLL/U+juoG5TfccAOTJ0/mySefLHfCY8+ePXz88cc88sgj9OnTp97HWJnbbruN1NRUVqxY0Wj7FEIIUXd6Rw9ACCFE6xIbG8vOnTv5/vvvufvuu1m+fDnPPfdcpdtv3ryZCxcu8PvvvzNp0iS+//575s6dWy9jMZlMvPjii0yYMIENGzaUuz05Oble9tPYpkyZwsCBAwG444478PPz48033+SHH36oMOgFyMvLw93dHYPB0JhDtVuyZAkzZszAxcWl3G1dunTh5ptvtv98zTXX0Lt3b95++22mTJnSmMMs48MPP6RHjx488sgj9kDYbDZz9913ExYWxvPPP9/gY1AUhcLCQlxdXfHx8WHixIksXbqU+fPnN/i+hRBC1A/JlAshhGhUy5cvp02bNkybNo3rrruO5cuXX3b77t27M2bMGMaPH3/Z7WsiNTWV7Oxshg8fXuHtgYGBgHpiYNCgQYCajbSVU5fOqH7zzTcMGDAAV1dX/P39ufnmm7l48WK5xzxx4gSzZ88mICAAV1dXunbtyv/93/9VOc64uDg6depEz549a1WGPnbsWEA9IQLqvHEPDw9iYmKYOnUqnp6e3HTTTfbbLp1TbrFYeOedd+jVqxcuLi4EBAQwefJk9u3bV2a7L774wv4a+Pr6csMNN3D+/PnLji82NpZDhw4xfvz4aj2fXr164e/vb38+27Zt429/+xthYWE4OzsTGhrKI488QkFBQZn7JSYmctttt9G+fXucnZ0JDg7m6quv5uzZswBERERw9OhRtmzZYv8djx49utJxRERE8Pzzz/Pll1+yceNGAN59910OHDjAhx9+iJubG0VFRTz33HN06tTJPrYnnniCoqKiMo+1ZMkSxo4dS2BgIM7OznTv3p0PP/ywwn1eddVVrF+/noEDB+Lq6lqmhH7ChAls376d9PT0ar2WQgghHE+CciGEEI1q+fLlXHvttTg5OTFnzhxOnTrF3r17K9y2qKiI7777zp7dnTNnDr///juJiYn1MpbAwEBcXV358ccfqwxiunXrxgsvvADAXXfdxeeff87nn3/OyJEjAXUu9+zZs9HpdLzyyivceeedfP/994wYMYLMzEz74xw6dIjBgwfz+++/c+edd/LOO+8wc+ZMfvzxx0r3HRMTw8iRI/H09GTz5s20bdu2xs8zJiYGAD8/P/t1JpOJSZMmERgYyH/+8x9mzZpV6f1vv/12Hn74YUJDQ3n11Vd58skncXFx4Y8//rBv8/LLL3PrrbfSuXNn3nzzTR5++GF+++03Ro4cWeY1qMjOnTsB6N+/f7WeT0ZGBhkZGfbn880335Cfn8+9997Le++9x6RJk3jvvfe49dZby9xv1qxZrFq1ittuu40PPviABx98kJycHM6dOwfA22+/Tfv27YmKirL/ji93wsRWon7vvfdy+vRpnn32WXtpu8ViYcaMGfznP/9h+vTpvPfee8ycOZO33nqrXH+EDz/8kPDwcP75z3/yxhtvEBoayn333ceiRYvK7TM6Opo5c+YwYcIE3nnnHfr27Wu/bcCAASiKYn9NhRBCNAOKEEII0Uj27dunAMrGjRsVRVEUi8WitG/fXnnooYcq3P7bb79VAOXUqVOKoihKdna24uLiorz11ltltouNjVUA5fXXX6/xmJ599lkFUNzd3ZUpU6YoL7/8srJ///5y2+3du1cBlCVLlpS5vri4WAkMDFR69uypFBQU2K//6aefFEB59tln7deNHDlS8fT0VOLi4so8hsVisX//3HPPKYCSkpKiHD9+XAkJCVEGDRqkpKenX/a5LFmyRAGUX3/9VUlJSVHOnz+vrFy5UvHz81NcXV2VCxcuKIqiKHPnzlUA5cknnyz3GHPnzlXCw8PtP//+++8KoDz44IPltrWN++zZs4pOp1NefvnlMrcfPnxY0ev15a6/1NNPP60ASk5OTrnbAOX2229XUlJSlOTkZGX37t3KuHHjFEB54403FEVRlPz8/HL3e+WVVxSNRmN/rTMyMqp1jPTo0UMZNWpUldtcavfu3YpWq1V8fX0VHx8fJTExUVEURfn8888VrVarbNu2rcz2H330kQIoO3bssF9X0XOYNGmS0rFjxzLXhYeHK4Cybt26CscSHx+vAMqrr75ao+cghBDCcSRTLoQQotEsX76ctm3bMmbMGAB7R/WVK1diNpsr3H7gwIF06tQJAE9PT6ZNm1avJewLFy5kxYoV9OvXj/Xr1/N///d/DBgwgP79+3P8+PHL3n/fvn0kJydz3333lZkPPW3aNKKiovj5558BSElJYevWrcyfP5+wsLAyj6HRaMo97pEjRxg1ahQRERH8+uuvtGnTptrPafz48QQEBBAaGsoNN9yAh4cHq1atol27dmW2u/feey/7WN999x0ajabCef+2cX///fdYLBZmz55Namqq/RIUFETnzp3ZtGlTlftIS0tDr9fj4eFR4e2ffvopAQEBBAYGMnjwYHbs2MGjjz7Kww8/DICrq6t927y8PFJTUxk2bBiKovDXX3/Zt3FycmLz5s1kZGRc9nnXxBVXXME9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/z46OpoLFy4wevRovLy86NmzZ7nH6dGjBwcOHCjzOh04cACNRmO/bt++fdx2223ceOON9n2fP38eJycnPD09ycnJoU2bNsydO5evvvoKJycnbrjhBtq2bVvp+D08PABwd3fHy8uL22+/nfvvv59Fixbh5eWFu7u7fTsvLy8+++wz2rdvX26O+saNG3nzzTf597//Xessd2FhIa6urowcObJZHxPNhdFoZOPGjUyYMKHCaQpCVEaOHVFbcuyIupJjSNSWHDu1Y7EoKLvUExkTJ4zH1715ROWVJSQr0iyC8m3bthEdHc1XX3112W2dnZ1xdnYud73BYCh38JvNZjQaDVqtFm0zmp/w+OOPM336dMLDw4mPj+e5555Dp9Nx4403otVqadOmDbfffjuPP/44/v7+eHl58cADDzB06FCGDRtW6eM+9NBDDB8+nDfffJOrr76a9evXs379egD769O5c2dWrVrFjBkz0Gg0PPPMM1gsFntnfFC75t95551069YNgB07dlT5+tpus/0e7r77bq6//np8fHzK/G5s3y9evJjrrruuXJf38PBw/vnPf7JhwwamTZtWq9dWq9Wi0WgqPF5Ew5HXW9SWHDuituTYEXUlx5CoLTl2aqbIZLZ/7+ri1Gxeu5qMs1lEop9++ikDBgwo02CsNbtw4QJz5syha9euzJ49Gz8/P/744w8CAgLs27z11ltcddVVzJo1i5EjRxIUFHTZ7udDhgzhk08+4Z133qFPnz5s2LCBp59+usw2b775Jm3atGHYsGFMnz6dSZMm0b9//3KP1blzZ4YNG0ZUVBSDBw+u0fPT6/X4+/uj15c/Z7R//34OHjxYYQd+b29vxo0bx6efflqj/QkhhBBCCCGaJpO5ZOloQzNKpNaEQzPlubm5nD592v5zbGwsBw4cwNfX1768V3Z2Nt988w1vvPGGo4bZ5KxcufKy27i4uLBo0SIWLVpUo8eeP38+8+fPL3PdY489Zv8+IiKC33//vcztCxYsAMo2llMUhfj4eO67777L7jMiIsLeJK4iffv2td9+uW3Xrl172f0JIYQQQgghmgfbGuUAOlkSrf7t27ePMWPG2H+2zQWfO3cuS5cuBdQAVFEU5syZ44ghilpISUnh66+/JjExkdtuu83RwxFCCCGEEEI0UyZzSeLPoJOgvN6NHj26yqwnwF133cVdd93VSCMS9SEoKAh/f38+/vhj2rRp4+jhCCGEEEIIIZopW6Zcpy3pYdXSNItGb6J5MZvNzapxnhBCCCGEEKJpKh2Ut1QSOQkhhBBCCCGEaJJs5esGCcqFEEIIIYQQQojGZcuU63UtN3Rtuc9MCCGEEEIIIUSzZlsSTS+ZciGEEEIIIYQQonEZreXr+hbaeR0kKBdCCCGEEEII0USZbeXrLbiRdMt9ZkIIIYQQQgghmjWTRTLlQlzWvHnzmDlzpqOHIYQQQgghhGhhjDKnXDRFr7zyCoMGDcLT05PAwEBmzpxJdHR0mW0KCwtZsGABfn5+eHh4MGvWLJKSkspsc+7cOaZNm4abmxuBgYH8/e9/x2QyNeZTqbalS5ei0Wjo1q1budu++eYbNBoNERER5W4rKCjA19cXf39/ioqKGmGkQgghhBBCiPoi5euiSdqyZQsLFizgjz/+YOPGjRiNRiZOnEheXp59m0ceeYQff/yRb775hi1bthAfH8+1115rv91sNjNt2jSKi4vZuXMny5YtY+nSpTz77LOOeErV4u7uTnJyMrt27Spz/aeffkpYWFiF9/nuu+/o0aMHUVFRrF69uhFGKYQQQgghhKgv0uitFVIUhXxjfqNfFEWp9hjXrVvHvHnz6NGjB3369GHp0qWcO3eO/fv3A5CVlcWnn37Km2++ydixYxkwYABLlixh586d/PHHHwBs2LCBY8eO8cUXX9C3b1+mTJnCiy++yKJFiyguLq5032azmUcffRQfHx/8/Px44oknyo193bp1jBgxwr7NVVddRUxMjP32sWPHcv/995e5T0pKCk5OTvz222+V7luv13PjjTeyePFi+3UXLlxg8+bN3HjjjRXe59NPP+Xmm2/m5ptv5tNPP630sYUQQgghhBBNj31JtBa8Trne0QNoagpMBQxeMbjR97v7xt24Gdxqdd+srCwAfH19Adi/fz9Go5Hx48fbt4mKiiIsLIxdu3YxZMgQdu3aRa9evWjbtq19m0mTJnHvvfdy9OhR+vXrV+G+3njjDZYuXcrixYvp1q0bb7zxBqtWrWLs2LH2bfLy8nj00Ufp3bs3ubm5PPvss1xzzTUcOHAArVbLHXfcwf33388bb7yBs7MzAF988QXt2rUr8zgVmT9/PqNHj+add97Bzc2NpUuXMnny5DLPwyYmJoZdu3bx/fffoygKjzzyCHFxcYSHh1fzlRVCCCGEEEI4kskic8pFE2exWHj44YcZPnw4PXv2BCAxMREnJyd8fHzKbNu2bVsSExPt21wayNp+tm1TkbfffpunnnqKa6+9lm7duvHRRx/h7e1dZptZs2Zx7bXX0qlTJ/r27cvixYs5fPgwx44dA7CX0f/www/2+yxdupR58+ah0VT9x9avXz86duzIt99+i6IoLF26lPnz51e47eLFi5kyZQpt2rTB19eXSZMmsWTJkiofXwghhBBCCNF02Luvt+CgXDLll3DVu7L7xt0O2W9tLFiwgCNHjrB9+/Z6Hc+5c+fo3r27/ed//vOfLFiwgISEBAYPLqkk0Ov1DBw4sEwJ+6lTp3j++efZvXs3qampWKx/SOfOnaNnz564uLhwyy23sHjxYmbPns2ff/7JkSNHWLNmTbXGNn/+fJYsWUJYWBh5eXlMnTqV999/v8w2ZrOZZcuW8c4779ivu/nmm3n88cd59tln0bbgRhFCCCGEEEK0FLbydYOUr7ceGo2m1mXkje3+++/np59+YuvWrbRv395+fVBQEMXFxWRmZpbJliclJREUFGTfZs+ePWUez9adPSgoiJCQEA4cOGC/zVYaXx1XX3014eHhfPLJJ4SEhGCxWOjZs2eZuep33HEHffv25cKFCyxZsoSxY8dWu6z8pptu4oknnuD555/nlltuQa8vfxivX7+eixcvcv3115e53mw289tvvzFhwoRqPx8hhBBCCCGEY9jK13UtOFPeck83tGCKonD//fezatUqfv/9dzp06FDm9gEDBmAwGMo0TYuOjubcuXMMHToUgKFDh3L48GGSk5Pt22zcuBEvLy+6d++OXq+nU6dO9ouvry/e3t4EBweze3dJJYHJZLI3mANIT08nOjqap59+mnHjxtGtWzcyMjLKPYdevXoxcOBAPvnkE1asWFFpCXpFfH19mTFjBlu2bKn0fp9++ik33HADBw4cKHO54YYbpOGbEEIIIYQQzYTJ2n3d0IK7r0umvBlasGABK1as4IcffsDT09M+B9zb2xtXV1e8vb25/fbbefTRR/H19cXLy4sHHniAoUOHMmTIEAAmTpxI9+7dueWWW3jttddITEzk6aefZsGCBfbmaxV56KGH+Pe//03nzp2JiorizTffJDMz0367reP6xx9/THBwMOfOnePJJ5+s8LFsDd/c3d255ppr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iIiIL6ido4iIiIiIiIiPKCkXERERERER8REl5SIiIiIiIiI+oqRcRERERERExEeUlIuIiIiIiIj4iJJyERERERERER9RUi4iIiIiIiLiI0rKRURERERERHxESbmIiIiIiIiIjygpFxERaeWuueYaLBYLFosFu91OYmIiEydO5D//+Q9ut7vWj/Pee+8RFRXVdIGKiIi0QUrKRURE2oDTTjuNtLQ0du7cyU8//cT48eP505/+xJlnnklFRYWvwxMREWmzlJSLiIi0AQ6Hg6SkJNq3b8+QIUN46KGH+Oabb/jpp5947733AHjxxRfp378/oaGhdOzYkVtuuYXCwkIAZs+ezbXXXkteXp531v3RRx8FoKysjHvuuYf27dsTGhrK8OHDmT17tm9eqIiIiJ9RUi4iItJGnXzyyQwcOJCvvvoKAKvVyiuvvMK6det4//33mTlzJvfddx8AI0eO5B//+AcRERGkpaWRlpbGPffcA8Btt93GwoUL+eSTT1i9ejUXXXQRp512Glu2bPHZaxMREfEXFsMwDF8HISIiIk3nmmuuITc3l6lTpx5x26WXXsrq1atZv379Ebd98cUX3HTTTWRlZQHmmvI777yT3Nxc7zG7d++ma9eu7N69m3bt2nmvnzBhAieccAJPPvlko78eERGR1iTA1wGIiIiI7xiGgcViAeDXX3/lqaeeYuPGjeTn51NRUUFpaSnFxcWEhIRUe/81a9bgcrno2bNnlevLysqIjY1t8vhFRET8nZJyERGRNmzDhg2kpKSwc+dOzjzzTG6++WaeeOIJYmJimD9/Ptdffz3l5eU1JuWFhYXYbDaWLVuGzWarcltYWFhzvAQRERG/pqRcRESkjZo5cyZr1qzhz3/+M8uWLcPtdvPCCy9gtZotZz777LMqxwcGBuJyuapcN3jwYFwuF5mZmYwZM6bZYhcREWktlJSLiIi0AWVlZaSnp+NyucjIyGDatGk89dRTnHnmmUyZMoW1a9fidDp59dVXOeuss/jtt9948803qzxGly5dKCwsZMaMGQwcOJCQkBB69uzJFVdcwZQpU3jhhRcYPHgw+/fvZ8aMGQwYMIAzzjjDR69YRETEP6j7uoiISBswbdo0kpOT6dKlC6eddhqzZs3ilVde4ZtvvsFmszFw4EBefPFFnnnmGfr168dHH33EU089VeUxRo4cyU033cQll1xCfHw8zz77LADvvvsuU6ZM4e6776ZXr16ce+65LFmyhE6dOvnipYqIiPgVdV8XERERERER8RHNlIuIiIiIiIj4iJJyERERERERER9RUi4iIiIiIiLiI0rKRURERERERHxESbmIiIiIiIiIjygpFxEREREREfERJeUiIiIiIiIiPqKkXERERERERMRHlJSLiIiIiIiI+IiSchEREREREREfUVIuIiIiIiIi4iP/D8qtFloq3E+YAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "stream = team.run_stream(task=\"Write a financial report on American airlines\")\n", + "await Console(stream)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } }, - { - "data": { - "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a financial report on American airlines', type='TextMessage'), ToolCallMessage(source='Stock_Analysis_Agent', models_usage=RequestUsage(prompt_tokens=64, completion_tokens=16), content=[FunctionCall(id='call_tPh9gSfGrDu1nC2Ck5RlfbFY', arguments='{\"ticker\":\"AAL\"}', name='analyze_stock')], type='ToolCallMessage'), ToolCallResultMessage(source='Stock_Analysis_Agent', models_usage=None, content=[FunctionExecutionResult(content=\"{'ticker': 'AAL', 'current_price': 17.4, '52_week_high': 18.09, '52_week_low': 9.07, '50_day_ma': 13.376799983978271, '200_day_ma': 12.604399962425232, 'ytd_price_change': 3.9600000381469727, 'ytd_percent_change': 29.46428691803602, 'trend': 'Upward', 'volatility': 0.4461582174242901, 'plot_file_path': 'coding/AAL_stockprice.png'}\", call_id='call_tPh9gSfGrDu1nC2Ck5RlfbFY')], type='ToolCallResultMessage'), TextMessage(source='Stock_Analysis_Agent', models_usage=None, content='Tool calls:\\nanalyze_stock({\"ticker\":\"AAL\"}) = {\\'ticker\\': \\'AAL\\', \\'current_price\\': 17.4, \\'52_week_high\\': 18.09, \\'52_week_low\\': 9.07, \\'50_day_ma\\': 13.376799983978271, \\'200_day_ma\\': 12.604399962425232, \\'ytd_price_change\\': 3.9600000381469727, \\'ytd_percent_change\\': 29.46428691803602, \\'trend\\': \\'Upward\\', \\'volatility\\': 0.4461582174242901, \\'plot_file_path\\': \\'coding/AAL_stockprice.png\\'}', type='TextMessage'), ToolCallMessage(source='Google_Search_Agent', models_usage=RequestUsage(prompt_tokens=268, completion_tokens=25), content=[FunctionCall(id='call_wSHc5Kw1ix3aQDXXT23opVnU', arguments='{\"query\":\"American Airlines financial report 2023\",\"num_results\":1}', name='google_search')], type='ToolCallMessage'), ToolCallResultMessage(source='Google_Search_Agent', models_usage=None, content=[FunctionExecutionResult(content=\"[{'title': 'American Airlines reports fourth-quarter and full-year 2023 financial ...', 'link': 'https://news.aa.com/news/news-details/2024/American-Airlines-reports-fourth-quarter-and-full-year-2023-financial-results-CORP-FI-01/default.aspx', 'snippet': 'Jan 25, 2024 ... American Airlines Group Inc. (NASDAQ: AAL) today reported its fourth-quarter and full-year 2023 financial results, including: Record\\\\xa0...', 'body': 'Just a moment... Enable JavaScript and cookies to continue'}]\", call_id='call_wSHc5Kw1ix3aQDXXT23opVnU')], type='ToolCallResultMessage'), TextMessage(source='Google_Search_Agent', models_usage=None, content='Tool calls:\\ngoogle_search({\"query\":\"American Airlines financial report 2023\",\"num_results\":1}) = [{\\'title\\': \\'American Airlines reports fourth-quarter and full-year 2023 financial ...\\', \\'link\\': \\'https://news.aa.com/news/news-details/2024/American-Airlines-reports-fourth-quarter-and-full-year-2023-financial-results-CORP-FI-01/default.aspx\\', \\'snippet\\': \\'Jan 25, 2024 ... American Airlines Group Inc. (NASDAQ: AAL) today reported its fourth-quarter and full-year 2023 financial results, including: Record\\\\xa0...\\', \\'body\\': \\'Just a moment... Enable JavaScript and cookies to continue\\'}]', type='TextMessage'), TextMessage(source='Report_Agent', models_usage=RequestUsage(prompt_tokens=360, completion_tokens=633), content=\"### American Airlines Financial Report\\n\\n#### Overview\\nAmerican Airlines Group Inc. (NASDAQ: AAL) is a major American airline headquartered in Fort Worth, Texas. It is known as one of the largest airlines in the world by fleet size, revenue, and passenger kilometers flown. As of the current quarter in 2023, American Airlines has shown significant financial activities and stock performance noteworthy for investors and analysts.\\n\\n#### Stock Performance\\n- **Current Stock Price**: $17.40\\n- **52-Week Range**: The stock price has ranged from $9.07 to $18.09 over the past year, indicating considerable volatility and fluctuation in market interest.\\n- **Moving Averages**: \\n - 50-Day MA: $13.38\\n - 200-Day MA: $12.60\\n These moving averages suggest a strong upward trend in recent months as the 50-day moving average is positioned above the 200-day moving average, indicating bullish momentum.\\n\\n- **YTD Price Change**: $3.96\\n- **YTD Percent Change**: 29.46%\\n The year-to-date figures demonstrate a robust upward momentum, with the stock appreciating by nearly 29.5% since the beginning of the year.\\n\\n- **Trend**: The current stock trend for American Airlines is upward, reflecting positive market sentiment and performance improvements.\\n\\n- **Volatility**: 0.446, indicating moderate volatility in the stock, which may attract risk-tolerant investors seeking dynamic movements for potential profit.\\n\\n#### Recent Financial Performance\\nAccording to the latest financial reports of 2023 (accessed through a reliable source), American Airlines reported remarkable figures for both the fourth quarter and the full year 2023. Key highlights from the report include:\\n\\n- **Revenue Growth**: American Airlines experienced substantial revenue increases, driven by high demand for travel as pandemic-related restrictions eased globally.\\n- **Profit Margins**: The company managed to enhance its profitability, largely attributed to cost management strategies and increased operational efficiency.\\n- **Challenges**: Despite positive momentum, the airline industry faces ongoing challenges including fluctuating fuel prices, geopolitical tensions, and competition pressures.\\n\\n#### Strategic Initiatives\\nAmerican Airlines has been focusing on several strategic initiatives to maintain its market leadership and improve its financial metrics:\\n1. **Fleet Modernization**: Continuation of investment in more fuel-efficient aircraft to reduce operating costs and environmental impact.\\n2. **Enhanced Customer Experience**: Introduction of new services and technology enhancements aimed at improving customer satisfaction.\\n3. **Operational Efficiency**: Streamlining processes to cut costs and increase overall effectiveness, which includes leveraging data analytics for better decision-making.\\n\\n#### Conclusion\\nAmerican Airlines is demonstrating strong market performance and financial growth amid an evolving industry landscape. The company's stock has been on an upward trend, reflecting its solid operational strategies and recovery efforts post-COVID pandemic. Investors should remain mindful of external risks while considering American Airlines as a potential investment, supported by its current upward trajectory and strategic initiatives.\\n\\nFor further details, investors are encouraged to review the full financial reports from American Airlines and assess ongoing market conditions.\\n\\n_TERMINATE_\", type='TextMessage')], stop_reason='Maximum number of turns 3 reached.')" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+UAAAIjCAYAAABlBbqXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hUZfbA8e+09EYaSSAFQgm9S5XeEURxUWwgdrHruro/G5Z11bWLurpSVBAriIoUlQ7SlA4BQgglvfdkyv39cWcmCSmkT8r5PM88SWbuzH1ncjOZc895z6tRFEVBCCGEEEIIIYQQjU7r6AEIIYQQQgghhBCtlQTlQgghhBBCCCGEg0hQLoQQQgghhBBCOIgE5UIIIYQQQgghhINIUC6EEEIIIYQQQjiIBOVCCCGEEEIIIYSDSFAuhBBCCCGEEEI4iATlQgghhBBCCCGEg0hQLoQQQgghhBBCOIgE5UIIIUQT8vzzz6PRaEhNTXXI/ufNm0dERIRD9v3aa68RFRWFxWJxyP6bM6PRSGhoKB988IGjhyKEEKKGJCgXQgjR6D744AM0Gg2DBw+u1vZPPPEEGo2G66+/vsLbz549i0aj4T//+U+txvPjjz8yatQoAgMDcXNzo2PHjsyePZt169bZt4mPj+f555/nwIEDtdpHY1i6dCkajcZ+cXFxoUuXLtx///0kJSU5enhVys7O5tVXX+Uf//gHWm3Jx5PSz0er1RISEsLEiRPZvHlzg4zjgw8+YOnSpZfdbuXKlWg0Gv773/9WePu9996LwWDg4MGD9TzCihkMBh599FFefvllCgsLG2WfQggh6ocE5UIIIRrd8uXLiYiIYM+ePZw+fbrKbRVF4csvvyQiIoIff/yRnJyceh3Lf/7zH2bMmIFGo+Gpp57irbfeYtasWZw6dYqVK1fat4uPj2fhwoVNOii3eeGFF/j88895//33GTZsGB9++CFDhw4lPz//svf95JNPiI6OboRRlrV48WJMJhNz5swpd9uECRP4/PPPWbZsGffccw+HDh1i7Nix/PLLL/U+juoG5TfccAOTJ0/mySefLHfCY8+ePXz88cc88sgj9OnTp97HWJnbbruN1NRUVqxY0Wj7FEIIUXd6Rw9ACCFE6xIbG8vOnTv5/vvvufvuu1m+fDnPPfdcpdtv3ryZCxcu8PvvvzNp0iS+//575s6dWy9jMZlMvPjii0yYMIENGzaUuz05Oble9tPYpkyZwsCBAwG444478PPz48033+SHH36oMOgFyMvLw93dHYPB0JhDtVuyZAkzZszAxcWl3G1dunTh5ptvtv98zTXX0Lt3b95++22mTJnSmMMs48MPP6RHjx488sgj9kDYbDZz9913ExYWxvPPP9/gY1AUhcLCQlxdXfHx8WHixIksXbqU+fPnN/i+hRBC1A/JlAshhGhUy5cvp02bNkybNo3rrruO5cuXX3b77t27M2bMGMaPH3/Z7WsiNTWV7Oxshg8fXuHtgYGBgHpiYNCgQYCajbSVU5fOqH7zzTcMGDAAV1dX/P39ufnmm7l48WK5xzxx4gSzZ88mICAAV1dXunbtyv/93/9VOc64uDg6depEz549a1WGPnbsWEA9IQLqvHEPDw9iYmKYOnUqnp6e3HTTTfbbLp1TbrFYeOedd+jVqxcuLi4EBAQwefJk9u3bV2a7L774wv4a+Pr6csMNN3D+/PnLji82NpZDhw4xfvz4aj2fXr164e/vb38+27Zt429/+xthYWE4OzsTGhrKI488QkFBQZn7JSYmctttt9G+fXucnZ0JDg7m6quv5uzZswBERERw9OhRtmzZYv8djx49utJxRERE8Pzzz/Pll1+yceNGAN59910OHDjAhx9+iJubG0VFRTz33HN06tTJPrYnnniCoqKiMo+1ZMkSxo4dS2BgIM7OznTv3p0PP/ywwn1eddVVrF+/noEDB+Lq6lqmhH7ChAls376d9PT0ar2WQgghHE+CciGEEI1q+fLlXHvttTg5OTFnzhxOnTrF3r17K9y2qKiI7777zp7dnTNnDr///juJiYn1MpbAwEBcXV358ccfqwxiunXrxgsvvADAXXfdxeeff87nn3/OyJEjAXUu9+zZs9HpdLzyyivceeedfP/994wYMYLMzEz74xw6dIjBgwfz+++/c+edd/LOO+8wc+ZMfvzxx0r3HRMTw8iRI/H09GTz5s20bdu2xs8zJiYGAD8/P/t1JpOJSZMmERgYyH/+8x9mzZpV6f1vv/12Hn74YUJDQ3n11Vd58skncXFx4Y8//rBv8/LLL3PrrbfSuXNn3nzzTR5++GF+++03Ro4cWeY1qMjOnTsB6N+/f7WeT0ZGBhkZGfbn880335Cfn8+9997Le++9x6RJk3jvvfe49dZby9xv1qxZrFq1ittuu40PPviABx98kJycHM6dOwfA22+/Tfv27YmKirL/ji93wsRWon7vvfdy+vRpnn32WXtpu8ViYcaMGfznP/9h+vTpvPfee8ycOZO33nqrXH+EDz/8kPDwcP75z3/yxhtvEBoayn333ceiRYvK7TM6Opo5c+YwYcIE3nnnHfr27Wu/bcCAASiKYn9NhRBCNAOKEEII0Uj27dunAMrGjRsVRVEUi8WitG/fXnnooYcq3P7bb79VAOXUqVOKoihKdna24uLiorz11ltltouNjVUA5fXXX6/xmJ599lkFUNzd3ZUpU6YoL7/8srJ///5y2+3du1cBlCVLlpS5vri4WAkMDFR69uypFBQU2K//6aefFEB59tln7deNHDlS8fT0VOLi4so8hsVisX//3HPPKYCSkpKiHD9+XAkJCVEGDRqkpKenX/a5LFmyRAGUX3/9VUlJSVHOnz+vrFy5UvHz81NcXV2VCxcuKIqiKHPnzlUA5cknnyz3GHPnzlXCw8PtP//+++8KoDz44IPltrWN++zZs4pOp1NefvnlMrcfPnxY0ev15a6/1NNPP60ASk5OTrnbAOX2229XUlJSlOTkZGX37t3KuHHjFEB54403FEVRlPz8/HL3e+WVVxSNRmN/rTMyMqp1jPTo0UMZNWpUldtcavfu3YpWq1V8fX0VHx8fJTExUVEURfn8888VrVarbNu2rcz2H330kQIoO3bssF9X0XOYNGmS0rFjxzLXhYeHK4Cybt26CscSHx+vAMqrr75ao+cghBDCcSRTLoQQotEsX76ctm3bMmbMGAB7R/WVK1diNpsr3H7gwIF06tQJAE9PT6ZNm1avJewLFy5kxYoV9OvXj/Xr1/N///d/DBgwgP79+3P8+PHL3n/fvn0kJydz3333lZkPPW3aNKKiovj5558BSElJYevWrcyfP5+wsLAyj6HRaMo97pEjRxg1ahQRERH8+uuvtGnTptrPafz48QQEBBAaGsoNN9yAh4cHq1atol27dmW2u/feey/7WN999x0ajabCef+2cX///fdYLBZmz55Namqq/RIUFETnzp3ZtGlTlftIS0tDr9fj4eFR4e2ffvopAQEBBAYGMnjwYHbs2MGjjz7Kww8/DICrq6t927y8PFJTUxk2bBiKovDXX3/Zt3FycmLz5s1kZGRc9nnXxBVXXME9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/z46OpoLFy4wevRovLy86NmzZ7nH6dGjBwcOHCjzOh04cACNRmO/bt++fdx2223ceOON9n2fP38eJycnPD09ycnJoU2bNsydO5evvvoKJycnbrjhBtq2bVvp+D08PABwd3fHy8uL22+/nfvvv59Fixbh5eWFu7u7fTsvLy8+++wz2rdvX26O+saNG3nzzTf597//Xessd2FhIa6urowcObJZHxPNhdFoZOPGjUyYMKHCaQpCVEaOHVFbcuyIupJjSNSWHDu1Y7EoKLvUExkTJ4zH1715ROWVJSQr0iyC8m3bthEdHc1XX3112W2dnZ1xdnYud73BYCh38JvNZjQaDVqtFm0zmp/w+OOPM336dMLDw4mPj+e5555Dp9Nx4403otVqadOmDbfffjuPP/44/v7+eHl58cADDzB06FCGDRtW6eM+9NBDDB8+nDfffJOrr76a9evXs379egD769O5c2dWrVrFjBkz0Gg0PPPMM1gsFntnfFC75t95551069YNgB07dlT5+tpus/0e7r77bq6//np8fHzK/G5s3y9evJjrrruuXJf38PBw/vnPf7JhwwamTZtWq9dWq9Wi0WgqPF5Ew5HXW9SWHDuituTYEXUlx5CoLTl2aqbIZLZ/7+ri1Gxeu5qMs1lEop9++ikDBgwo02CsNbtw4QJz5syha9euzJ49Gz8/P/744w8CAgLs27z11ltcddVVzJo1i5EjRxIUFHTZ7udDhgzhk08+4Z133qFPnz5s2LCBp59+usw2b775Jm3atGHYsGFMnz6dSZMm0b9//3KP1blzZ4YNG0ZUVBSDBw+u0fPT6/X4+/uj15c/Z7R//34OHjxYYQd+b29vxo0bx6efflqj/QkhhBBCCCGaJpO5ZOloQzNKpNaEQzPlubm5nD592v5zbGwsBw4cwNfX1768V3Z2Nt988w1vvPGGo4bZ5KxcufKy27i4uLBo0SIWLVpUo8eeP38+8+fPL3PdY489Zv8+IiKC33//vcztCxYsAMo2llMUhfj4eO67777L7jMiIsLeJK4iffv2td9+uW3Xrl172f0JIYQQQgghmgfbGuUAOlkSrf7t27ePMWPG2H+2zQWfO3cuS5cuBdQAVFEU5syZ44ghilpISUnh66+/JjExkdtuu83RwxFCCCGEEEI0UyZzSeLPoJOgvN6NHj26yqwnwF133cVdd93VSCMS9SEoKAh/f38+/vhj2rRp4+jhCCGEEEIIIZopW6Zcpy3pYdXSNItGb6J5MZvNzapxnhBCCCGEEKJpKh2Ut1QSOQkhhBBCCCGEaJJs5esGCcqFEEIIIYQQQojGZcuU63UtN3Rtuc9MCCGEEEIIIUSzZlsSTS+ZciGEEEIIIYQQonEZreXr+hbaeR0kKBdCCCGEEEII0USZbeXrLbiRdMt9ZkIIIYQQQgghmjWTRTLlQlzWvHnzmDlzpqOHIYQQQgghhGhhjDKnXDRFr7zyCoMGDcLT05PAwEBmzpxJdHR0mW0KCwtZsGABfn5+eHh4MGvWLJKSkspsc+7cOaZNm4abmxuBgYH8/e9/x2QyNeZTqbalS5ei0Wjo1q1budu++eYbNBoNERER5W4rKCjA19cXf39/ioqKGmGkQgghhBBCiPoi5euiSdqyZQsLFizgjz/+YOPGjRiNRiZOnEheXp59m0ceeYQff/yRb775hi1bthAfH8+1115rv91sNjNt2jSKi4vZuXMny5YtY+nSpTz77LOOeErV4u7uTnJyMrt27Spz/aeffkpYWFiF9/nuu+/o0aMHUVFRrF69uhFGKYQQQgghhKgv0uitFVIUhXxjfqNfFEWp9hjXrVvHvHnz6NGjB3369GHp0qWcO3eO/fv3A5CVlcWnn37Km2++ydixYxkwYABLlixh586d/PHHHwBs2LCBY8eO8cUXX9C3b1+mTJnCiy++yKJFiyguLq5032azmUcffRQfHx/8/Px44oknyo193bp1jBgxwr7NVVddRUxMjP32sWPHcv/995e5T0pKCk5OTvz222+V7luv13PjjTeyePFi+3UXLlxg8+bN3HjjjRXe59NPP+Xmm2/m5ptv5tNPP630sYUQQgghhBBNj31JtBa8Trne0QNoagpMBQxeMbjR97v7xt24Gdxqdd+srCwAfH19Adi/fz9Go5Hx48fbt4mKiiIsLIxdu3YxZMgQdu3aRa9evWjbtq19m0mTJnHvvfdy9OhR+vXrV+G+3njjDZYuXcrixYvp1q0bb7zxBqtWrWLs2LH2bfLy8nj00Ufp3bs3ubm5PPvss1xzzTUcOHAArVbLHXfcwf33388bb7yBs7MzAF988QXt2rUr8zgVmT9/PqNHj+add97Bzc2NpUuXMnny5DLPwyYmJoZdu3bx/fffoygKjzzyCHFxcYSHh1fzlRVCCCGEEEI4kskic8pFE2exWHj44YcZPnw4PXv2BCAxMREnJyd8fHzKbNu2bVsSExPt21wayNp+tm1TkbfffpunnnqKa6+9lm7duvHRRx/h7e1dZptZs2Zx7bXX0qlTJ/r27cvixYs5fPgwx44dA7CX0f/www/2+yxdupR58+ah0VT9x9avXz86duzIt99+i6IoLF26lPnz51e47eLFi5kyZQpt2rTB19eXSZMmsWTJkiofXwghhBBCCNF02Luvt+CgXDLll3DVu7L7xt0O2W9tLFiwgCNHjrB9+/Z6Hc+5c+fo3r27/ed//vOfLFiwgISEBAYPLqkk0Ov1DBw4sEwJ+6lTp3j++efZvXs3qampWKx/SOfOnaNnz564uLhwyy23sHjxYmbPns2ff/7JkSNHWLNmTbXGNn/+fJYsWUJYWBh5eXlMnTqV999/v8w2ZrOZZcuW8c4779ivu/nmm3n88cd59tln0bbgRhFCCCGEEEK0FLbydYOUr7ceGo2m1mXkje3+++/np59+YuvWrbRv395+fVBQEMXFxWRmZpbJliclJREUFGTfZs+ePWUez9adPSgoiJCQEA4cOGC/zVYaXx1XX3014eHhfPLJJ4SEhGCxWOjZs2eZuep33HEHffv25cKFCyxZsoSxY8dWu6z8pptu4oknnuD555/nlltuQa8vfxivX7+eixcvcv3115e53mw289tvvzFhwoRqPx8hhBBCCCGEY9jK13UtOFPeck83tGCKonD//fezatUqfv/9dzp06FDm9gEDBmAwGMo0TYuOjubcuXMMHToUgKFDh3L48GGSk5Pt22zcuBEvLy+6d++OXq+nU6dO9ouvry/e3t4EBweze3dJJYHJZLI3mANIT08nOjqap59+mnHjxtGtWzcyMjLKPYdevXoxcOBAPvnkE1asWFFpCXpFfH19mTFjBlu2bKn0fp9++ik33HADBw4cKHO54YYbpOGbEEIIIYQQzYTJ2n3d0IK7r0umvBlasGABK1as4IcffsDT09M+B9zb2xtXV1e8vb25/fbbefTRR/H19cXLy4sHHniAoUOHMmTIEAAmTpxI9+7dueWWW3jttddITEzk6aefZsGCBfbmaxV56KGH+Pe//03nzp2JiorizTffJDMz0367reP6xx9/THBwMOfOnePJJ5+s8LFsDd/c3d255ppr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iIiIL6ido4iIiIiIiIiPKCkXERERERER8REl5SIiIiIiIiI+oqRcRERERERExEeUlIuIiIiIiIj4iJJyERERERERER9RUi4iIiIiIiLiI0rKRURERERERHxESbmIiIiIiIiIjygpFxERaeWuueYaLBYLFosFu91OYmIiEydO5D//+Q9ut7vWj/Pee+8RFRXVdIGKiIi0QUrKRURE2oDTTjuNtLQ0du7cyU8//cT48eP505/+xJlnnklFRYWvwxMREWmzlJSLiIi0AQ6Hg6SkJNq3b8+QIUN46KGH+Oabb/jpp5947733AHjxxRfp378/oaGhdOzYkVtuuYXCwkIAZs+ezbXXXkteXp531v3RRx8FoKysjHvuuYf27dsTGhrK8OHDmT17tm9eqIiIiJ9RUi4iItJGnXzyyQwcOJCvvvoKAKvVyiuvvMK6det4//33mTlzJvfddx8AI0eO5B//+AcRERGkpaWRlpbGPffcA8Btt93GwoUL+eSTT1i9ejUXXXQRp512Glu2bPHZaxMREfEXFsMwDF8HISIiIk3nmmuuITc3l6lTpx5x26WXXsrq1atZv379Ebd98cUX3HTTTWRlZQHmmvI777yT3Nxc7zG7d++ma9eu7N69m3bt2nmvnzBhAieccAJPPvlko78eERGR1iTA1wGIiIiI7xiGgcViAeDXX3/lqaeeYuPGjeTn51NRUUFpaSnFxcWEhIRUe/81a9bgcrno2bNnlevLysqIjY1t8vhFRET8nZJyERGRNmzDhg2kpKSwc+dOzjzzTG6++WaeeOIJYmJimD9/Ptdffz3l5eU1JuWFhYXYbDaWLVuGzWarcltYWFhzvAQRERG/pqRcRESkjZo5cyZr1qzhz3/+M8uWLcPtdvPCCy9gtZotZz777LMqxwcGBuJyuapcN3jwYFwuF5mZmYwZM6bZYhcREWktlJSLiIi0AWVlZaSnp+NyucjIyGDatGk89dRTnHnmmUyZMoW1a9fidDp59dVXOeuss/jtt9948803qzxGly5dKCwsZMaMGQwcOJCQkBB69uzJFVdcwZQpU3jhhRcYPHgw+/fvZ8aMGQwYMIAzzjjDR69YRETEP6j7uoiISBswbdo0kpOT6dKlC6eddhqzZs3ilVde4ZtvvsFmszFw4EBefPFFnnnmGfr168dHH33EU089VeUxRo4cyU033cQll1xCfHw8zz77LADvvvsuU6ZM4e6776ZXr16ce+65LFmyhE6dOvnipYqIiPgVdV8XERERERER8RHNlIuIiIiIiIj4iJJyERERERERER9RUi4iIiIiIiLiI0rKRURERERERHxESbmIiIiIiIiIjygpFxEREREREfERJeUiIiIiIiIiPqKkXERERERERMRHlJSLiIiIiIiI+IiSchEREREREREfUVIuIiIiIiIi4iP/D8qtFloq3E+YAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "stream = team.run_stream(task=\"Write a financial report on American airlines\")\n", - "await Console(stream)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/literature-review.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/literature-review.ipynb index c4d22fa6eedb..c513e8278723 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/literature-review.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/literature-review.ipynb @@ -1,334 +1,334 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Literature Review\n", - "\n", - "A common task while exploring a new topic is to conduct a literature review. In this example we will explore how a multi-agent team can be configured to conduct a _simple_ literature review.\n", - "\n", - "- **Arxiv Search Agent**: Use the Arxiv API to search for papers related to a given topic and return results.\n", - "- **Google Search Agent**: Use the Google Search api to find papers related to a given topic and return results.\n", - "- **Report Agent**: Generate a report based on the information collected by the search and stock analysis agents.\n", - "\n", - "\n", - "First, let us import the necessary modules. " - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_agentchat.agents import AssistantAgent\n", - "from autogen_agentchat.conditions import TextMentionTermination\n", - "from autogen_agentchat.teams import RoundRobinGroupChat\n", - "from autogen_agentchat.ui import Console\n", - "from autogen_core.tools import FunctionTool\n", - "from autogen_ext.models.openai import OpenAIChatCompletionClient" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Defining Tools \n", - "\n", - "Next, we will define the tools that the agents will use to perform their tasks. In this case we will define a simple function `search_arxiv` that will use the `arxiv` library to search for papers related to a given topic. \n", - "\n", - "Finally, we will wrap the functions into a `FunctionTool` class that will allow us to use it as a tool in the agents. \n", - "\n", - "Note: You will need to set the appropriate environment variables for tools as needed.\n", - "\n", - "Also install required libraries: \n", - "\n", - "```bash\n", - "!pip install arxiv\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "def google_search(query: str, num_results: int = 2, max_chars: int = 500) -> list: # type: ignore[type-arg]\n", - " import os\n", - " import time\n", - "\n", - " import requests\n", - " from bs4 import BeautifulSoup\n", - " from dotenv import load_dotenv\n", - "\n", - " load_dotenv()\n", - "\n", - " api_key = os.getenv(\"GOOGLE_API_KEY\")\n", - " search_engine_id = os.getenv(\"GOOGLE_SEARCH_ENGINE_ID\")\n", - "\n", - " if not api_key or not search_engine_id:\n", - " raise ValueError(\"API key or Search Engine ID not found in environment variables\")\n", - "\n", - " url = \"https://www.googleapis.com/customsearch/v1\"\n", - " params = {\"key\": api_key, \"cx\": search_engine_id, \"q\": query, \"num\": num_results}\n", - "\n", - " response = requests.get(url, params=params) # type: ignore[arg-type]\n", - "\n", - " if response.status_code != 200:\n", - " print(response.json())\n", - " raise Exception(f\"Error in API request: {response.status_code}\")\n", - "\n", - " results = response.json().get(\"items\", [])\n", - "\n", - " def get_page_content(url: str) -> str:\n", - " try:\n", - " response = requests.get(url, timeout=10)\n", - " soup = BeautifulSoup(response.content, \"html.parser\")\n", - " text = soup.get_text(separator=\" \", strip=True)\n", - " words = text.split()\n", - " content = \"\"\n", - " for word in words:\n", - " if len(content) + len(word) + 1 > max_chars:\n", - " break\n", - " content += \" \" + word\n", - " return content.strip()\n", - " except Exception as e:\n", - " print(f\"Error fetching {url}: {str(e)}\")\n", - " return \"\"\n", - "\n", - " enriched_results = []\n", - " for item in results:\n", - " body = get_page_content(item[\"link\"])\n", - " enriched_results.append(\n", - " {\"title\": item[\"title\"], \"link\": item[\"link\"], \"snippet\": item[\"snippet\"], \"body\": body}\n", - " )\n", - " time.sleep(1) # Be respectful to the servers\n", - "\n", - " return enriched_results\n", - "\n", - "\n", - "def arxiv_search(query: str, max_results: int = 2) -> list: # type: ignore[type-arg]\n", - " \"\"\"\n", - " Search Arxiv for papers and return the results including abstracts.\n", - " \"\"\"\n", - " import arxiv\n", - "\n", - " client = arxiv.Client()\n", - " search = arxiv.Search(query=query, max_results=max_results, sort_by=arxiv.SortCriterion.Relevance)\n", - "\n", - " results = []\n", - " for paper in client.results(search):\n", - " results.append(\n", - " {\n", - " \"title\": paper.title,\n", - " \"authors\": [author.name for author in paper.authors],\n", - " \"published\": paper.published.strftime(\"%Y-%m-%d\"),\n", - " \"abstract\": paper.summary,\n", - " \"pdf_url\": paper.pdf_url,\n", - " }\n", - " )\n", - "\n", - " # # Write results to a file\n", - " # with open('arxiv_search_results.json', 'w') as f:\n", - " # json.dump(results, f, indent=2)\n", - "\n", - " return results" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "google_search_tool = FunctionTool(\n", - " google_search, description=\"Search Google for information, returns results with a snippet and body content\"\n", - ")\n", - "arxiv_search_tool = FunctionTool(\n", - " arxiv_search, description=\"Search Arxiv for papers related to a given topic, including abstracts\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Defining Agents \n", - "\n", - "Next, we will define the agents that will perform the tasks. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "google_search_agent = AssistantAgent(\n", - " name=\"Google_Search_Agent\",\n", - " tools=[google_search_tool],\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", - " description=\"An agent that can search Google for information, returns results with a snippet and body content\",\n", - " system_message=\"You are a helpful AI assistant. Solve tasks using your tools.\",\n", - ")\n", - "\n", - "arxiv_search_agent = AssistantAgent(\n", - " name=\"Arxiv_Search_Agent\",\n", - " tools=[arxiv_search_tool],\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", - " description=\"An agent that can search Arxiv for papers related to a given topic, including abstracts\",\n", - " system_message=\"You are a helpful AI assistant. Solve tasks using your tools. Specifically, you can take into consideration the user's request and craft a search query that is most likely to return relevant academi papers.\",\n", - ")\n", - "\n", - "\n", - "report_agent = AssistantAgent(\n", - " name=\"Report_Agent\",\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", - " description=\"Generate a report based on a given topic\",\n", - " system_message=\"You are a helpful assistant. Your task is to synthesize data extracted into a high quality literature review including CORRECT references. You MUST write a final report that is formatted as a literature review with CORRECT references. Your response should end with the word 'TERMINATE'\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Creating the Team \n", - "\n", - "Finally, we will create a team of agents and configure them to perform the tasks." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "termination = TextMentionTermination(\"TERMINATE\")\n", - "team = RoundRobinGroupChat(\n", - " participants=[google_search_agent, arxiv_search_agent, report_agent], termination_condition=termination\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "Write a literature review on no code tools for building multi agent ai systems\n", - "---------- Google_Search_Agent ----------\n", - "[FunctionCall(id='call_bNGwWFsfeTwDhtIpsI6GYISR', arguments='{\"query\":\"no code tools for building multi agent AI systems literature review\",\"num_results\":3}', name='google_search')]\n", - "[Prompt tokens: 123, Completion tokens: 29]\n", - "---------- Google_Search_Agent ----------\n", - "[FunctionExecutionResult(content='[{\\'title\\': \\'Literature Review — AutoGen\\', \\'link\\': \\'https://microsoft.github.io/autogen/dev//user-guide/agentchat-user-guide/examples/literature-review.html\\', \\'snippet\\': \\'run( task=\"Write a literature review on no code tools for building multi agent ai systems\", ) ... ### Conclusion No-code tools for building multi-agent AI systems\\\\xa0...\\', \\'body\\': \\'Literature Review — AutoGen Skip to main content Back to top Ctrl + K AutoGen 0.4 is a work in progress. Go here to find the 0.2 documentation. User Guide Packages API Reference Twitter GitHub PyPI User Guide Packages API Reference Twitter GitHub PyPI AgentChat Installation Quickstart Tutorial Models Messages Agents Teams Selector Group Chat Swarm Termination Custom Agents Managing State Examples Travel Planning Company Research Literature Review Core Quick Start Core Concepts Agent and\\'}, {\\'title\\': \\'Vertex AI Agent Builder | Google Cloud\\', \\'link\\': \\'https://cloud.google.com/products/agent-builder\\', \\'snippet\\': \\'Build and deploy enterprise ready generative AI experiences · Product highlights · Easily build no code conversational AI agents · Ground in Google search and/or\\\\xa0...\\', \\'body\\': \\'Vertex AI Agent Builder | Google Cloud Page Contents Vertex AI Agent Builder is making generative AI more reliable for the enterprise. Read the blog. Vertex AI Agent Builder Build and deploy enterprise ready generative AI experiences Create AI agents and applications using natural language or a code-first approach. Easily ground your agents or apps in enterprise data with a range of options. Vertex AI Agent Builder gathers all the surfaces and tools that developers need to build their AI agents\\'}, {\\'title\\': \\'AI tools I have found useful w/ research. What do you guys think ...\\', \\'link\\': \\'https://www.reddit.com/r/PhD/comments/14d6g09/ai_tools_i_have_found_useful_w_research_what_do/\\', \\'snippet\\': \"Jun 19, 2023 ... Need help deciding on the best ones, and to identify ones I\\'ve missed: ASSISTANTS (chatbots, multi-purpose) Chat with Open Large Language Models.\", \\'body\\': \\'Reddit - Dive into anything Skip to main content Open menu Open navigation Go to Reddit Home r/PhD A chip A close button Get app Get the Reddit app Log In Log in to Reddit Expand user menu Open settings menu Log In / Sign Up Advertise on Reddit Shop Collectible Avatars Get the Reddit app Scan this QR code to download the app now Or check it out in the app stores Go to PhD r/PhD r/PhD A subreddit dedicated to PhDs. Members Online • [deleted] ADMIN MOD AI tools I have found useful w/ research.\\'}]', call_id='call_bNGwWFsfeTwDhtIpsI6GYISR')]\n", - "---------- Google_Search_Agent ----------\n", - "Tool calls:\n", - "google_search({\"query\":\"no code tools for building multi agent AI systems literature review\",\"num_results\":3}) = [{'title': 'Literature Review — AutoGen', 'link': 'https://microsoft.github.io/autogen/dev//user-guide/agentchat-user-guide/examples/literature-review.html', 'snippet': 'run( task=\"Write a literature review on no code tools for building multi agent ai systems\", ) ... ### Conclusion No-code tools for building multi-agent AI systems\\xa0...', 'body': 'Literature Review — AutoGen Skip to main content Back to top Ctrl + K AutoGen 0.4 is a work in progress. Go here to find the 0.2 documentation. User Guide Packages API Reference Twitter GitHub PyPI User Guide Packages API Reference Twitter GitHub PyPI AgentChat Installation Quickstart Tutorial Models Messages Agents Teams Selector Group Chat Swarm Termination Custom Agents Managing State Examples Travel Planning Company Research Literature Review Core Quick Start Core Concepts Agent and'}, {'title': 'Vertex AI Agent Builder | Google Cloud', 'link': 'https://cloud.google.com/products/agent-builder', 'snippet': 'Build and deploy enterprise ready generative AI experiences · Product highlights · Easily build no code conversational AI agents · Ground in Google search and/or\\xa0...', 'body': 'Vertex AI Agent Builder | Google Cloud Page Contents Vertex AI Agent Builder is making generative AI more reliable for the enterprise. Read the blog. Vertex AI Agent Builder Build and deploy enterprise ready generative AI experiences Create AI agents and applications using natural language or a code-first approach. Easily ground your agents or apps in enterprise data with a range of options. Vertex AI Agent Builder gathers all the surfaces and tools that developers need to build their AI agents'}, {'title': 'AI tools I have found useful w/ research. What do you guys think ...', 'link': 'https://www.reddit.com/r/PhD/comments/14d6g09/ai_tools_i_have_found_useful_w_research_what_do/', 'snippet': \"Jun 19, 2023 ... Need help deciding on the best ones, and to identify ones I've missed: ASSISTANTS (chatbots, multi-purpose) Chat with Open Large Language Models.\", 'body': 'Reddit - Dive into anything Skip to main content Open menu Open navigation Go to Reddit Home r/PhD A chip A close button Get app Get the Reddit app Log In Log in to Reddit Expand user menu Open settings menu Log In / Sign Up Advertise on Reddit Shop Collectible Avatars Get the Reddit app Scan this QR code to download the app now Or check it out in the app stores Go to PhD r/PhD r/PhD A subreddit dedicated to PhDs. Members Online • [deleted] ADMIN MOD AI tools I have found useful w/ research.'}]\n", - "---------- Arxiv_Search_Agent ----------\n", - "[FunctionCall(id='call_ZdmwQGTO03X23GeRn6fwDN8q', arguments='{\"query\":\"no code tools for building multi agent AI systems\",\"max_results\":5}', name='arxiv_search')]\n", - "[Prompt tokens: 719, Completion tokens: 28]\n", - "---------- Arxiv_Search_Agent ----------\n", - "[FunctionExecutionResult(content='[{\\'title\\': \\'AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems\\', \\'authors\\': [\\'Victor Dibia\\', \\'Jingya Chen\\', \\'Gagan Bansal\\', \\'Suff Syed\\', \\'Adam Fourney\\', \\'Erkang Zhu\\', \\'Chi Wang\\', \\'Saleema Amershi\\'], \\'published\\': \\'2024-08-09\\', \\'abstract\\': \\'Multi-agent systems, where multiple agents (generative AI models + tools)\\\\ncollaborate, are emerging as an effective pattern for solving long-running,\\\\ncomplex tasks in numerous domains. However, specifying their parameters (such\\\\nas models, tools, and orchestration mechanisms etc,.) and debugging them\\\\nremains challenging for most developers. To address this challenge, we present\\\\nAUTOGEN STUDIO, a no-code developer tool for rapidly prototyping, debugging,\\\\nand evaluating multi-agent workflows built upon the AUTOGEN framework. AUTOGEN\\\\nSTUDIO offers a web interface and a Python API for representing LLM-enabled\\\\nagents using a declarative (JSON-based) specification. It provides an intuitive\\\\ndrag-and-drop UI for agent workflow specification, interactive evaluation and\\\\ndebugging of workflows, and a gallery of reusable agent components. We\\\\nhighlight four design principles for no-code multi-agent developer tools and\\\\ncontribute an open-source implementation at\\\\nhttps://github.com/microsoft/autogen/tree/main/samples/apps/autogen-studio\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2408.15247v1\\'}, {\\'title\\': \\'Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration\\', \\'authors\\': [\\'Cory Hymel\\', \\'Sida Peng\\', \\'Kevin Xu\\', \\'Charath Ranganathan\\'], \\'published\\': \\'2024-10-29\\', \\'abstract\\': \\'In recent years, with the rapid advancement of large language models (LLMs),\\\\nmulti-agent systems have become increasingly more capable of practical\\\\napplication. At the same time, the software development industry has had a\\\\nnumber of new AI-powered tools developed that improve the software development\\\\nlifecycle (SDLC). Academically, much attention has been paid to the role of\\\\nmulti-agent systems to the SDLC. And, while single-agent systems have\\\\nfrequently been examined in real-world applications, we have seen comparatively\\\\nfew real-world examples of publicly available commercial tools working together\\\\nin a multi-agent system with measurable improvements. In this experiment we\\\\ntest context sharing between Crowdbotics PRD AI, a tool for generating software\\\\nrequirements using AI, and GitHub Copilot, an AI pair-programming tool. By\\\\nsharing business requirements from PRD AI, we improve the code suggestion\\\\ncapabilities of GitHub Copilot by 13.8% and developer task success rate by\\\\n24.5% -- demonstrating a real-world example of commercially-available AI\\\\nsystems working together with improved outcomes.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.22129v1\\'}, {\\'title\\': \\'AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML\\', \\'authors\\': [\\'Patara Trirat\\', \\'Wonyong Jeong\\', \\'Sung Ju Hwang\\'], \\'published\\': \\'2024-10-03\\', \\'abstract\\': \"Automated machine learning (AutoML) accelerates AI development by automating\\\\ntasks in the development pipeline, such as optimal model search and\\\\nhyperparameter tuning. Existing AutoML systems often require technical\\\\nexpertise to set up complex tools, which is in general time-consuming and\\\\nrequires a large amount of human effort. Therefore, recent works have started\\\\nexploiting large language models (LLM) to lessen such burden and increase the\\\\nusability of AutoML frameworks via a natural language interface, allowing\\\\nnon-expert users to build their data-driven solutions. These methods, however,\\\\nare usually designed only for a particular process in the AI development\\\\npipeline and do not efficiently use the inherent capacity of the LLMs. This\\\\npaper proposes AutoML-Agent, a novel multi-agent framework tailored for\\\\nfull-pipeline AutoML, i.e., from data retrieval to model deployment.\\\\nAutoML-Agent takes user\\'s task descriptions, facilitates collaboration between\\\\nspecialized LLM agents, and delivers deployment-ready models. Unlike existing\\\\nwork, instead of devising a single plan, we introduce a retrieval-augmented\\\\nplanning strategy to enhance exploration to search for more optimal plans. We\\\\nalso decompose each plan into sub-tasks (e.g., data preprocessing and neural\\\\nnetwork design) each of which is solved by a specialized agent we build via\\\\nprompting executing in parallel, making the search process more efficient.\\\\nMoreover, we propose a multi-stage verification to verify executed results and\\\\nguide the code generation LLM in implementing successful solutions. Extensive\\\\nexperiments on seven downstream tasks using fourteen datasets show that\\\\nAutoML-Agent achieves a higher success rate in automating the full AutoML\\\\nprocess, yielding systems with good performance throughout the diverse domains.\", \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.02958v1\\'}, {\\'title\\': \\'Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges\\', \\'authors\\': [\\'Sivan Schwartz\\', \\'Avi Yaeli\\', \\'Segev Shlomov\\'], \\'published\\': \\'2023-08-10\\', \\'abstract\\': \\'Trust in AI agents has been extensively studied in the literature, resulting\\\\nin significant advancements in our understanding of this field. However, the\\\\nrapid advancements in Large Language Models (LLMs) and the emergence of\\\\nLLM-based AI agent frameworks pose new challenges and opportunities for further\\\\nresearch. In the field of process automation, a new generation of AI-based\\\\nagents has emerged, enabling the execution of complex tasks. At the same time,\\\\nthe process of building automation has become more accessible to business users\\\\nvia user-friendly no-code tools and training mechanisms. This paper explores\\\\nthese new challenges and opportunities, analyzes the main aspects of trust in\\\\nAI agents discussed in existing literature, and identifies specific\\\\nconsiderations and challenges relevant to this new generation of automation\\\\nagents. We also evaluate how nascent products in this category address these\\\\nconsiderations. Finally, we highlight several challenges that the research\\\\ncommunity should address in this evolving landscape.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2308.05391v1\\'}, {\\'title\\': \\'AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications\\', \\'authors\\': [\\'Xin Pang\\', \\'Zhucong Li\\', \\'Jiaxiang Chen\\', \\'Yuan Cheng\\', \\'Yinghui Xu\\', \\'Yuan Qi\\'], \\'published\\': \\'2024-04-07\\', \\'abstract\\': \\'We introduce AI2Apps, a Visual Integrated Development Environment (Visual\\\\nIDE) with full-cycle capabilities that accelerates developers to build\\\\ndeployable LLM-based AI agent Applications. This Visual IDE prioritizes both\\\\nthe Integrity of its development tools and the Visuality of its components,\\\\nensuring a smooth and efficient building experience.On one hand, AI2Apps\\\\nintegrates a comprehensive development toolkit ranging from a prototyping\\\\ncanvas and AI-assisted code editor to agent debugger, management system, and\\\\ndeployment tools all within a web-based graphical user interface. On the other\\\\nhand, AI2Apps visualizes reusable front-end and back-end code as intuitive\\\\ndrag-and-drop components. Furthermore, a plugin system named AI2Apps Extension\\\\n(AAE) is designed for Extensibility, showcasing how a new plugin with 20\\\\ncomponents enables web agent to mimic human-like browsing behavior. Our case\\\\nstudy demonstrates substantial efficiency improvements, with AI2Apps reducing\\\\ntoken consumption and API calls when debugging a specific sophisticated\\\\nmultimodal agent by approximately 90% and 80%, respectively. The AI2Apps,\\\\nincluding an online demo, open-source code, and a screencast video, is now\\\\npublicly accessible.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2404.04902v1\\'}]', call_id='call_ZdmwQGTO03X23GeRn6fwDN8q')]\n", - "---------- Arxiv_Search_Agent ----------\n", - "Tool calls:\n", - "arxiv_search({\"query\":\"no code tools for building multi agent AI systems\",\"max_results\":5}) = [{'title': 'AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems', 'authors': ['Victor Dibia', 'Jingya Chen', 'Gagan Bansal', 'Suff Syed', 'Adam Fourney', 'Erkang Zhu', 'Chi Wang', 'Saleema Amershi'], 'published': '2024-08-09', 'abstract': 'Multi-agent systems, where multiple agents (generative AI models + tools)\\ncollaborate, are emerging as an effective pattern for solving long-running,\\ncomplex tasks in numerous domains. However, specifying their parameters (such\\nas models, tools, and orchestration mechanisms etc,.) and debugging them\\nremains challenging for most developers. To address this challenge, we present\\nAUTOGEN STUDIO, a no-code developer tool for rapidly prototyping, debugging,\\nand evaluating multi-agent workflows built upon the AUTOGEN framework. AUTOGEN\\nSTUDIO offers a web interface and a Python API for representing LLM-enabled\\nagents using a declarative (JSON-based) specification. It provides an intuitive\\ndrag-and-drop UI for agent workflow specification, interactive evaluation and\\ndebugging of workflows, and a gallery of reusable agent components. We\\nhighlight four design principles for no-code multi-agent developer tools and\\ncontribute an open-source implementation at\\nhttps://github.com/microsoft/autogen/tree/main/samples/apps/autogen-studio', 'pdf_url': 'http://arxiv.org/pdf/2408.15247v1'}, {'title': 'Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration', 'authors': ['Cory Hymel', 'Sida Peng', 'Kevin Xu', 'Charath Ranganathan'], 'published': '2024-10-29', 'abstract': 'In recent years, with the rapid advancement of large language models (LLMs),\\nmulti-agent systems have become increasingly more capable of practical\\napplication. At the same time, the software development industry has had a\\nnumber of new AI-powered tools developed that improve the software development\\nlifecycle (SDLC). Academically, much attention has been paid to the role of\\nmulti-agent systems to the SDLC. And, while single-agent systems have\\nfrequently been examined in real-world applications, we have seen comparatively\\nfew real-world examples of publicly available commercial tools working together\\nin a multi-agent system with measurable improvements. In this experiment we\\ntest context sharing between Crowdbotics PRD AI, a tool for generating software\\nrequirements using AI, and GitHub Copilot, an AI pair-programming tool. By\\nsharing business requirements from PRD AI, we improve the code suggestion\\ncapabilities of GitHub Copilot by 13.8% and developer task success rate by\\n24.5% -- demonstrating a real-world example of commercially-available AI\\nsystems working together with improved outcomes.', 'pdf_url': 'http://arxiv.org/pdf/2410.22129v1'}, {'title': 'AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML', 'authors': ['Patara Trirat', 'Wonyong Jeong', 'Sung Ju Hwang'], 'published': '2024-10-03', 'abstract': \"Automated machine learning (AutoML) accelerates AI development by automating\\ntasks in the development pipeline, such as optimal model search and\\nhyperparameter tuning. Existing AutoML systems often require technical\\nexpertise to set up complex tools, which is in general time-consuming and\\nrequires a large amount of human effort. Therefore, recent works have started\\nexploiting large language models (LLM) to lessen such burden and increase the\\nusability of AutoML frameworks via a natural language interface, allowing\\nnon-expert users to build their data-driven solutions. These methods, however,\\nare usually designed only for a particular process in the AI development\\npipeline and do not efficiently use the inherent capacity of the LLMs. This\\npaper proposes AutoML-Agent, a novel multi-agent framework tailored for\\nfull-pipeline AutoML, i.e., from data retrieval to model deployment.\\nAutoML-Agent takes user's task descriptions, facilitates collaboration between\\nspecialized LLM agents, and delivers deployment-ready models. Unlike existing\\nwork, instead of devising a single plan, we introduce a retrieval-augmented\\nplanning strategy to enhance exploration to search for more optimal plans. We\\nalso decompose each plan into sub-tasks (e.g., data preprocessing and neural\\nnetwork design) each of which is solved by a specialized agent we build via\\nprompting executing in parallel, making the search process more efficient.\\nMoreover, we propose a multi-stage verification to verify executed results and\\nguide the code generation LLM in implementing successful solutions. Extensive\\nexperiments on seven downstream tasks using fourteen datasets show that\\nAutoML-Agent achieves a higher success rate in automating the full AutoML\\nprocess, yielding systems with good performance throughout the diverse domains.\", 'pdf_url': 'http://arxiv.org/pdf/2410.02958v1'}, {'title': 'Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges', 'authors': ['Sivan Schwartz', 'Avi Yaeli', 'Segev Shlomov'], 'published': '2023-08-10', 'abstract': 'Trust in AI agents has been extensively studied in the literature, resulting\\nin significant advancements in our understanding of this field. However, the\\nrapid advancements in Large Language Models (LLMs) and the emergence of\\nLLM-based AI agent frameworks pose new challenges and opportunities for further\\nresearch. In the field of process automation, a new generation of AI-based\\nagents has emerged, enabling the execution of complex tasks. At the same time,\\nthe process of building automation has become more accessible to business users\\nvia user-friendly no-code tools and training mechanisms. This paper explores\\nthese new challenges and opportunities, analyzes the main aspects of trust in\\nAI agents discussed in existing literature, and identifies specific\\nconsiderations and challenges relevant to this new generation of automation\\nagents. We also evaluate how nascent products in this category address these\\nconsiderations. Finally, we highlight several challenges that the research\\ncommunity should address in this evolving landscape.', 'pdf_url': 'http://arxiv.org/pdf/2308.05391v1'}, {'title': 'AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications', 'authors': ['Xin Pang', 'Zhucong Li', 'Jiaxiang Chen', 'Yuan Cheng', 'Yinghui Xu', 'Yuan Qi'], 'published': '2024-04-07', 'abstract': 'We introduce AI2Apps, a Visual Integrated Development Environment (Visual\\nIDE) with full-cycle capabilities that accelerates developers to build\\ndeployable LLM-based AI agent Applications. This Visual IDE prioritizes both\\nthe Integrity of its development tools and the Visuality of its components,\\nensuring a smooth and efficient building experience.On one hand, AI2Apps\\nintegrates a comprehensive development toolkit ranging from a prototyping\\ncanvas and AI-assisted code editor to agent debugger, management system, and\\ndeployment tools all within a web-based graphical user interface. On the other\\nhand, AI2Apps visualizes reusable front-end and back-end code as intuitive\\ndrag-and-drop components. Furthermore, a plugin system named AI2Apps Extension\\n(AAE) is designed for Extensibility, showcasing how a new plugin with 20\\ncomponents enables web agent to mimic human-like browsing behavior. Our case\\nstudy demonstrates substantial efficiency improvements, with AI2Apps reducing\\ntoken consumption and API calls when debugging a specific sophisticated\\nmultimodal agent by approximately 90% and 80%, respectively. The AI2Apps,\\nincluding an online demo, open-source code, and a screencast video, is now\\npublicly accessible.', 'pdf_url': 'http://arxiv.org/pdf/2404.04902v1'}]\n", - "---------- Report_Agent ----------\n", - "## Literature Review on No-Code Tools for Building Multi-Agent AI Systems\n", - "\n", - "### Introduction\n", - "\n", - "The emergence of multi-agent systems (MAS) has transformed various domains by enabling collaboration among multiple agents—ranging from generative AI models to orchestrated tools—to solve complex, long-term tasks. However, the traditional development of these systems often requires substantial technical expertise, making it inaccessible for non-developers. The introduction of no-code platforms aims to shift this paradigm, allowing users without formal programming knowledge to design, debug, and deploy multi-agent systems. This review synthesizes current literature concerning no-code tools developed for building multi-agent AI systems, highlighting recent advancements and emerging trends.\n", - "\n", - "### No-Code Development Tools\n", - "\n", - "#### AutoGen Studio\n", - "\n", - "One of the prominent no-code tools is **AutoGen Studio**, developed by Dibia et al. (2024). This tool provides a web interface and a declarative specification method utilizing JSON, enabling rapid prototyping, debugging, and evaluating multi-agent workflows. The drag-and-drop capabilities streamline the design process, making complex interactions between agents more manageable. The framework operates on four primary design principles that cater specifically to no-code development, contributing to an accessible pathway for users to harness multi-agent frameworks for various applications (Dibia et al., 2024).\n", - "\n", - "#### AI2Apps Visual IDE\n", - "\n", - "Another notable tool is **AI2Apps**, described by Pang et al. (2024). It serves as a Visual Integrated Development Environment that incorporates a comprehensive set of tools from prototyping to deployment. The platform's user-friendly interface allows for the visualization of code through drag-and-drop components, facilitating smoother integration of different agents. An extension system enhances the platform's capabilities, showcasing the potential for customization and scalability in agent application development. The reported efficiency improvements in token consumption and API calls indicate substantial benefits in user-centric design (Pang et al., 2024).\n", - "\n", - "### Performance Enhancements in Multi-Agent Configurations\n", - "\n", - "Hymel et al. (2024) examined the collaborative performance of commercially available AI tools, demonstrating a measurable improvement when integrating multiple agents in a shared configuration. Their experiments showcased how cooperation between tools like Crowdbotics PRD AI and GitHub Copilot significantly improved task success rates, illustrating the practical benefits of employing no-code tools in multi-agent environments. This synergy reflects the critical need for frameworks that inherently support such integrations, especially through no-code mechanisms, to enhance user experience and productivity (Hymel et al., 2024).\n", - "\n", - "### Trust and Usability in AI Agents\n", - "\n", - "The concept of trust in AI, particularly in LLM-based automation agents, has gained attention. Schwartz et al. (2023) addressed the challenges and considerations unique to this new generation of agents, highlighting how no-code platforms ease access and usability for non-technical users. The paper emphasizes the need for further research into the trust factors integral to effective multi-agent systems, advocating for a user-centric approach in the design and evaluation of these no-code tools (Schwartz et al., 2023).\n", - "\n", - "### Full-Pipeline AutoML with Multi-Agent Systems\n", - "\n", - "The **AutoML-Agent** framework proposed by Trirat et al. (2024) brings another layer of innovation to the no-code landscape. This framework enhances existing automated machine learning processes by using multiple specialized agents that collaboratively manage the full AI development pipeline from data retrieval to model deployment. The novelty lies in its retrieval-augmented planning strategy, which allows for efficient task decomposition and parallel execution, optimizing the overall development experience for non-experts (Trirat et al., 2024).\n", - "\n", - "### Conclusion\n", - "\n", - "The literature presents a growing array of no-code tools designed to democratize the development of multi-agent systems. Innovations such as AutoGen Studio, AI2Apps, and collaborative frameworks like AutoML-Agent highlight a trend towards user-centric, efficient design that encourages participation beyond technical boundaries. Future research should continue to explore aspects of trust, usability, and integration to further refine these tools and expand their applicability across various domains.\n", - "\n", - "### References\n", - "\n", - "- Dibia, V., Chen, J., Bansal, G., Syed, S., Fourney, A., Zhu, E., Wang, C., & Amershi, S. (2024). AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems. *arXiv:2408.15247*.\n", - "- Hymel, C., Peng, S., Xu, K., & Ranganathan, C. (2024). Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration. *arXiv:2410.22129*.\n", - "- Pang, X., Li, Z., Chen, J., Cheng, Y., Xu, Y., & Qi, Y. (2024). AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications. *arXiv:2404.04902*.\n", - "- Schwartz, S., Yaeli, A., & Shlomov, S. (2023). Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges. *arXiv:2308.05391*.\n", - "- Trirat, P., Jeong, W., & Hwang, S. J. (2024). AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML. *arXiv:2410.02958*.\n", - "\n", - "TERMINATE\n", - "[Prompt tokens: 2381, Completion tokens: 1090]\n", - "---------- Summary ----------\n", - "Number of messages: 8\n", - "Finish reason: Text 'TERMINATE' mentioned\n", - "Total prompt tokens: 3223\n", - "Total completion tokens: 1147\n", - "Duration: 17.06 seconds\n" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Literature Review\n", + "\n", + "A common task while exploring a new topic is to conduct a literature review. In this example we will explore how a multi-agent team can be configured to conduct a _simple_ literature review.\n", + "\n", + "- **Arxiv Search Agent**: Use the Arxiv API to search for papers related to a given topic and return results.\n", + "- **Google Search Agent**: Use the Google Search api to find papers related to a given topic and return results.\n", + "- **Report Agent**: Generate a report based on the information collected by the search and stock analysis agents.\n", + "\n", + "\n", + "First, let us import the necessary modules. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.conditions import TextMentionTermination\n", + "from autogen_agentchat.teams import RoundRobinGroupChat\n", + "from autogen_agentchat.ui import Console\n", + "from autogen_core.tools import FunctionTool\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining Tools \n", + "\n", + "Next, we will define the tools that the agents will use to perform their tasks. In this case we will define a simple function `search_arxiv` that will use the `arxiv` library to search for papers related to a given topic. \n", + "\n", + "Finally, we will wrap the functions into a `FunctionTool` class that will allow us to use it as a tool in the agents. \n", + "\n", + "Note: You will need to set the appropriate environment variables for tools as needed.\n", + "\n", + "Also install required libraries: \n", + "\n", + "```bash\n", + "!pip install arxiv\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def google_search(query: str, num_results: int = 2, max_chars: int = 500) -> list: # type: ignore[type-arg]\n", + " import os\n", + " import time\n", + "\n", + " import requests\n", + " from bs4 import BeautifulSoup\n", + " from dotenv import load_dotenv\n", + "\n", + " load_dotenv()\n", + "\n", + " api_key = os.getenv(\"GOOGLE_API_KEY\")\n", + " search_engine_id = os.getenv(\"GOOGLE_SEARCH_ENGINE_ID\")\n", + "\n", + " if not api_key or not search_engine_id:\n", + " raise ValueError(\"API key or Search Engine ID not found in environment variables\")\n", + "\n", + " url = \"https://www.googleapis.com/customsearch/v1\"\n", + " params = {\"key\": api_key, \"cx\": search_engine_id, \"q\": query, \"num\": num_results}\n", + "\n", + " response = requests.get(url, params=params) # type: ignore[arg-type]\n", + "\n", + " if response.status_code != 200:\n", + " print(response.json())\n", + " raise Exception(f\"Error in API request: {response.status_code}\")\n", + "\n", + " results = response.json().get(\"items\", [])\n", + "\n", + " def get_page_content(url: str) -> str:\n", + " try:\n", + " response = requests.get(url, timeout=10)\n", + " soup = BeautifulSoup(response.content, \"html.parser\")\n", + " text = soup.get_text(separator=\" \", strip=True)\n", + " words = text.split()\n", + " content = \"\"\n", + " for word in words:\n", + " if len(content) + len(word) + 1 > max_chars:\n", + " break\n", + " content += \" \" + word\n", + " return content.strip()\n", + " except Exception as e:\n", + " print(f\"Error fetching {url}: {str(e)}\")\n", + " return \"\"\n", + "\n", + " enriched_results = []\n", + " for item in results:\n", + " body = get_page_content(item[\"link\"])\n", + " enriched_results.append(\n", + " {\"title\": item[\"title\"], \"link\": item[\"link\"], \"snippet\": item[\"snippet\"], \"body\": body}\n", + " )\n", + " time.sleep(1) # Be respectful to the servers\n", + "\n", + " return enriched_results\n", + "\n", + "\n", + "def arxiv_search(query: str, max_results: int = 2) -> list: # type: ignore[type-arg]\n", + " \"\"\"\n", + " Search Arxiv for papers and return the results including abstracts.\n", + " \"\"\"\n", + " import arxiv\n", + "\n", + " client = arxiv.Client()\n", + " search = arxiv.Search(query=query, max_results=max_results, sort_by=arxiv.SortCriterion.Relevance)\n", + "\n", + " results = []\n", + " for paper in client.results(search):\n", + " results.append(\n", + " {\n", + " \"title\": paper.title,\n", + " \"authors\": [author.name for author in paper.authors],\n", + " \"published\": paper.published.strftime(\"%Y-%m-%d\"),\n", + " \"abstract\": paper.summary,\n", + " \"pdf_url\": paper.pdf_url,\n", + " }\n", + " )\n", + "\n", + " # # Write results to a file\n", + " # with open('arxiv_search_results.json', 'w') as f:\n", + " # json.dump(results, f, indent=2)\n", + "\n", + " return results" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "google_search_tool = FunctionTool(\n", + " google_search, description=\"Search Google for information, returns results with a snippet and body content\"\n", + ")\n", + "arxiv_search_tool = FunctionTool(\n", + " arxiv_search, description=\"Search Arxiv for papers related to a given topic, including abstracts\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining Agents \n", + "\n", + "Next, we will define the agents that will perform the tasks. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "google_search_agent = AssistantAgent(\n", + " name=\"Google_Search_Agent\",\n", + " tools=[google_search_tool],\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", + " description=\"An agent that can search Google for information, returns results with a snippet and body content\",\n", + " system_message=\"You are a helpful AI assistant. Solve tasks using your tools.\",\n", + ")\n", + "\n", + "arxiv_search_agent = AssistantAgent(\n", + " name=\"Arxiv_Search_Agent\",\n", + " tools=[arxiv_search_tool],\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", + " description=\"An agent that can search Arxiv for papers related to a given topic, including abstracts\",\n", + " system_message=\"You are a helpful AI assistant. Solve tasks using your tools. Specifically, you can take into consideration the user's request and craft a search query that is most likely to return relevant academi papers.\",\n", + ")\n", + "\n", + "\n", + "report_agent = AssistantAgent(\n", + " name=\"Report_Agent\",\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", + " description=\"Generate a report based on a given topic\",\n", + " system_message=\"You are a helpful assistant. Your task is to synthesize data extracted into a high quality literature review including CORRECT references. You MUST write a final report that is formatted as a literature review with CORRECT references. Your response should end with the word 'TERMINATE'\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating the Team \n", + "\n", + "Finally, we will create a team of agents and configure them to perform the tasks." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "termination = TextMentionTermination(\"TERMINATE\")\n", + "team = RoundRobinGroupChat(\n", + " participants=[google_search_agent, arxiv_search_agent, report_agent], termination_condition=termination\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Write a literature review on no code tools for building multi agent ai systems\n", + "---------- Google_Search_Agent ----------\n", + "[FunctionCall(id='call_bNGwWFsfeTwDhtIpsI6GYISR', arguments='{\"query\":\"no code tools for building multi agent AI systems literature review\",\"num_results\":3}', name='google_search')]\n", + "[Prompt tokens: 123, Completion tokens: 29]\n", + "---------- Google_Search_Agent ----------\n", + "[FunctionExecutionResult(content='[{\\'title\\': \\'Literature Review — AutoGen\\', \\'link\\': \\'https://microsoft.github.io/autogen/dev//user-guide/agentchat-user-guide/examples/literature-review.html\\', \\'snippet\\': \\'run( task=\"Write a literature review on no code tools for building multi agent ai systems\", ) ... ### Conclusion No-code tools for building multi-agent AI systems\\\\xa0...\\', \\'body\\': \\'Literature Review — AutoGen Skip to main content Back to top Ctrl + K AutoGen 0.4 is a work in progress. Go here to find the 0.2 documentation. User Guide Packages API Reference Twitter GitHub PyPI User Guide Packages API Reference Twitter GitHub PyPI AgentChat Installation Quickstart Tutorial Models Messages Agents Teams Selector Group Chat Swarm Termination Custom Agents Managing State Examples Travel Planning Company Research Literature Review Core Quick Start Core Concepts Agent and\\'}, {\\'title\\': \\'Vertex AI Agent Builder | Google Cloud\\', \\'link\\': \\'https://cloud.google.com/products/agent-builder\\', \\'snippet\\': \\'Build and deploy enterprise ready generative AI experiences · Product highlights · Easily build no code conversational AI agents · Ground in Google search and/or\\\\xa0...\\', \\'body\\': \\'Vertex AI Agent Builder | Google Cloud Page Contents Vertex AI Agent Builder is making generative AI more reliable for the enterprise. Read the blog. Vertex AI Agent Builder Build and deploy enterprise ready generative AI experiences Create AI agents and applications using natural language or a code-first approach. Easily ground your agents or apps in enterprise data with a range of options. Vertex AI Agent Builder gathers all the surfaces and tools that developers need to build their AI agents\\'}, {\\'title\\': \\'AI tools I have found useful w/ research. What do you guys think ...\\', \\'link\\': \\'https://www.reddit.com/r/PhD/comments/14d6g09/ai_tools_i_have_found_useful_w_research_what_do/\\', \\'snippet\\': \"Jun 19, 2023 ... Need help deciding on the best ones, and to identify ones I\\'ve missed: ASSISTANTS (chatbots, multi-purpose) Chat with Open Large Language Models.\", \\'body\\': \\'Reddit - Dive into anything Skip to main content Open menu Open navigation Go to Reddit Home r/PhD A chip A close button Get app Get the Reddit app Log In Log in to Reddit Expand user menu Open settings menu Log In / Sign Up Advertise on Reddit Shop Collectible Avatars Get the Reddit app Scan this QR code to download the app now Or check it out in the app stores Go to PhD r/PhD r/PhD A subreddit dedicated to PhDs. Members Online • [deleted] ADMIN MOD AI tools I have found useful w/ research.\\'}]', call_id='call_bNGwWFsfeTwDhtIpsI6GYISR')]\n", + "---------- Google_Search_Agent ----------\n", + "Tool calls:\n", + "google_search({\"query\":\"no code tools for building multi agent AI systems literature review\",\"num_results\":3}) = [{'title': 'Literature Review — AutoGen', 'link': 'https://microsoft.github.io/autogen/dev//user-guide/agentchat-user-guide/examples/literature-review.html', 'snippet': 'run( task=\"Write a literature review on no code tools for building multi agent ai systems\", ) ... ### Conclusion No-code tools for building multi-agent AI systems\\xa0...', 'body': 'Literature Review — AutoGen Skip to main content Back to top Ctrl + K AutoGen 0.4 is a work in progress. Go here to find the 0.2 documentation. User Guide Packages API Reference Twitter GitHub PyPI User Guide Packages API Reference Twitter GitHub PyPI AgentChat Installation Quickstart Tutorial Models Messages Agents Teams Selector Group Chat Swarm Termination Custom Agents Managing State Examples Travel Planning Company Research Literature Review Core Quick Start Core Concepts Agent and'}, {'title': 'Vertex AI Agent Builder | Google Cloud', 'link': 'https://cloud.google.com/products/agent-builder', 'snippet': 'Build and deploy enterprise ready generative AI experiences · Product highlights · Easily build no code conversational AI agents · Ground in Google search and/or\\xa0...', 'body': 'Vertex AI Agent Builder | Google Cloud Page Contents Vertex AI Agent Builder is making generative AI more reliable for the enterprise. Read the blog. Vertex AI Agent Builder Build and deploy enterprise ready generative AI experiences Create AI agents and applications using natural language or a code-first approach. Easily ground your agents or apps in enterprise data with a range of options. Vertex AI Agent Builder gathers all the surfaces and tools that developers need to build their AI agents'}, {'title': 'AI tools I have found useful w/ research. What do you guys think ...', 'link': 'https://www.reddit.com/r/PhD/comments/14d6g09/ai_tools_i_have_found_useful_w_research_what_do/', 'snippet': \"Jun 19, 2023 ... Need help deciding on the best ones, and to identify ones I've missed: ASSISTANTS (chatbots, multi-purpose) Chat with Open Large Language Models.\", 'body': 'Reddit - Dive into anything Skip to main content Open menu Open navigation Go to Reddit Home r/PhD A chip A close button Get app Get the Reddit app Log In Log in to Reddit Expand user menu Open settings menu Log In / Sign Up Advertise on Reddit Shop Collectible Avatars Get the Reddit app Scan this QR code to download the app now Or check it out in the app stores Go to PhD r/PhD r/PhD A subreddit dedicated to PhDs. Members Online • [deleted] ADMIN MOD AI tools I have found useful w/ research.'}]\n", + "---------- Arxiv_Search_Agent ----------\n", + "[FunctionCall(id='call_ZdmwQGTO03X23GeRn6fwDN8q', arguments='{\"query\":\"no code tools for building multi agent AI systems\",\"max_results\":5}', name='arxiv_search')]\n", + "[Prompt tokens: 719, Completion tokens: 28]\n", + "---------- Arxiv_Search_Agent ----------\n", + "[FunctionExecutionResult(content='[{\\'title\\': \\'AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems\\', \\'authors\\': [\\'Victor Dibia\\', \\'Jingya Chen\\', \\'Gagan Bansal\\', \\'Suff Syed\\', \\'Adam Fourney\\', \\'Erkang Zhu\\', \\'Chi Wang\\', \\'Saleema Amershi\\'], \\'published\\': \\'2024-08-09\\', \\'abstract\\': \\'Multi-agent systems, where multiple agents (generative AI models + tools)\\\\ncollaborate, are emerging as an effective pattern for solving long-running,\\\\ncomplex tasks in numerous domains. However, specifying their parameters (such\\\\nas models, tools, and orchestration mechanisms etc,.) and debugging them\\\\nremains challenging for most developers. To address this challenge, we present\\\\nAUTOGEN STUDIO, a no-code developer tool for rapidly prototyping, debugging,\\\\nand evaluating multi-agent workflows built upon the AUTOGEN framework. AUTOGEN\\\\nSTUDIO offers a web interface and a Python API for representing LLM-enabled\\\\nagents using a declarative (JSON-based) specification. It provides an intuitive\\\\ndrag-and-drop UI for agent workflow specification, interactive evaluation and\\\\ndebugging of workflows, and a gallery of reusable agent components. We\\\\nhighlight four design principles for no-code multi-agent developer tools and\\\\ncontribute an open-source implementation at\\\\nhttps://github.com/microsoft/autogen/tree/main/samples/apps/autogen-studio\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2408.15247v1\\'}, {\\'title\\': \\'Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration\\', \\'authors\\': [\\'Cory Hymel\\', \\'Sida Peng\\', \\'Kevin Xu\\', \\'Charath Ranganathan\\'], \\'published\\': \\'2024-10-29\\', \\'abstract\\': \\'In recent years, with the rapid advancement of large language models (LLMs),\\\\nmulti-agent systems have become increasingly more capable of practical\\\\napplication. At the same time, the software development industry has had a\\\\nnumber of new AI-powered tools developed that improve the software development\\\\nlifecycle (SDLC). Academically, much attention has been paid to the role of\\\\nmulti-agent systems to the SDLC. And, while single-agent systems have\\\\nfrequently been examined in real-world applications, we have seen comparatively\\\\nfew real-world examples of publicly available commercial tools working together\\\\nin a multi-agent system with measurable improvements. In this experiment we\\\\ntest context sharing between Crowdbotics PRD AI, a tool for generating software\\\\nrequirements using AI, and GitHub Copilot, an AI pair-programming tool. By\\\\nsharing business requirements from PRD AI, we improve the code suggestion\\\\ncapabilities of GitHub Copilot by 13.8% and developer task success rate by\\\\n24.5% -- demonstrating a real-world example of commercially-available AI\\\\nsystems working together with improved outcomes.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.22129v1\\'}, {\\'title\\': \\'AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML\\', \\'authors\\': [\\'Patara Trirat\\', \\'Wonyong Jeong\\', \\'Sung Ju Hwang\\'], \\'published\\': \\'2024-10-03\\', \\'abstract\\': \"Automated machine learning (AutoML) accelerates AI development by automating\\\\ntasks in the development pipeline, such as optimal model search and\\\\nhyperparameter tuning. Existing AutoML systems often require technical\\\\nexpertise to set up complex tools, which is in general time-consuming and\\\\nrequires a large amount of human effort. Therefore, recent works have started\\\\nexploiting large language models (LLM) to lessen such burden and increase the\\\\nusability of AutoML frameworks via a natural language interface, allowing\\\\nnon-expert users to build their data-driven solutions. These methods, however,\\\\nare usually designed only for a particular process in the AI development\\\\npipeline and do not efficiently use the inherent capacity of the LLMs. This\\\\npaper proposes AutoML-Agent, a novel multi-agent framework tailored for\\\\nfull-pipeline AutoML, i.e., from data retrieval to model deployment.\\\\nAutoML-Agent takes user\\'s task descriptions, facilitates collaboration between\\\\nspecialized LLM agents, and delivers deployment-ready models. Unlike existing\\\\nwork, instead of devising a single plan, we introduce a retrieval-augmented\\\\nplanning strategy to enhance exploration to search for more optimal plans. We\\\\nalso decompose each plan into sub-tasks (e.g., data preprocessing and neural\\\\nnetwork design) each of which is solved by a specialized agent we build via\\\\nprompting executing in parallel, making the search process more efficient.\\\\nMoreover, we propose a multi-stage verification to verify executed results and\\\\nguide the code generation LLM in implementing successful solutions. Extensive\\\\nexperiments on seven downstream tasks using fourteen datasets show that\\\\nAutoML-Agent achieves a higher success rate in automating the full AutoML\\\\nprocess, yielding systems with good performance throughout the diverse domains.\", \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.02958v1\\'}, {\\'title\\': \\'Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges\\', \\'authors\\': [\\'Sivan Schwartz\\', \\'Avi Yaeli\\', \\'Segev Shlomov\\'], \\'published\\': \\'2023-08-10\\', \\'abstract\\': \\'Trust in AI agents has been extensively studied in the literature, resulting\\\\nin significant advancements in our understanding of this field. However, the\\\\nrapid advancements in Large Language Models (LLMs) and the emergence of\\\\nLLM-based AI agent frameworks pose new challenges and opportunities for further\\\\nresearch. In the field of process automation, a new generation of AI-based\\\\nagents has emerged, enabling the execution of complex tasks. At the same time,\\\\nthe process of building automation has become more accessible to business users\\\\nvia user-friendly no-code tools and training mechanisms. This paper explores\\\\nthese new challenges and opportunities, analyzes the main aspects of trust in\\\\nAI agents discussed in existing literature, and identifies specific\\\\nconsiderations and challenges relevant to this new generation of automation\\\\nagents. We also evaluate how nascent products in this category address these\\\\nconsiderations. Finally, we highlight several challenges that the research\\\\ncommunity should address in this evolving landscape.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2308.05391v1\\'}, {\\'title\\': \\'AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications\\', \\'authors\\': [\\'Xin Pang\\', \\'Zhucong Li\\', \\'Jiaxiang Chen\\', \\'Yuan Cheng\\', \\'Yinghui Xu\\', \\'Yuan Qi\\'], \\'published\\': \\'2024-04-07\\', \\'abstract\\': \\'We introduce AI2Apps, a Visual Integrated Development Environment (Visual\\\\nIDE) with full-cycle capabilities that accelerates developers to build\\\\ndeployable LLM-based AI agent Applications. This Visual IDE prioritizes both\\\\nthe Integrity of its development tools and the Visuality of its components,\\\\nensuring a smooth and efficient building experience.On one hand, AI2Apps\\\\nintegrates a comprehensive development toolkit ranging from a prototyping\\\\ncanvas and AI-assisted code editor to agent debugger, management system, and\\\\ndeployment tools all within a web-based graphical user interface. On the other\\\\nhand, AI2Apps visualizes reusable front-end and back-end code as intuitive\\\\ndrag-and-drop components. Furthermore, a plugin system named AI2Apps Extension\\\\n(AAE) is designed for Extensibility, showcasing how a new plugin with 20\\\\ncomponents enables web agent to mimic human-like browsing behavior. Our case\\\\nstudy demonstrates substantial efficiency improvements, with AI2Apps reducing\\\\ntoken consumption and API calls when debugging a specific sophisticated\\\\nmultimodal agent by approximately 90% and 80%, respectively. The AI2Apps,\\\\nincluding an online demo, open-source code, and a screencast video, is now\\\\npublicly accessible.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2404.04902v1\\'}]', call_id='call_ZdmwQGTO03X23GeRn6fwDN8q')]\n", + "---------- Arxiv_Search_Agent ----------\n", + "Tool calls:\n", + "arxiv_search({\"query\":\"no code tools for building multi agent AI systems\",\"max_results\":5}) = [{'title': 'AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems', 'authors': ['Victor Dibia', 'Jingya Chen', 'Gagan Bansal', 'Suff Syed', 'Adam Fourney', 'Erkang Zhu', 'Chi Wang', 'Saleema Amershi'], 'published': '2024-08-09', 'abstract': 'Multi-agent systems, where multiple agents (generative AI models + tools)\\ncollaborate, are emerging as an effective pattern for solving long-running,\\ncomplex tasks in numerous domains. However, specifying their parameters (such\\nas models, tools, and orchestration mechanisms etc,.) and debugging them\\nremains challenging for most developers. To address this challenge, we present\\nAUTOGEN STUDIO, a no-code developer tool for rapidly prototyping, debugging,\\nand evaluating multi-agent workflows built upon the AUTOGEN framework. AUTOGEN\\nSTUDIO offers a web interface and a Python API for representing LLM-enabled\\nagents using a declarative (JSON-based) specification. It provides an intuitive\\ndrag-and-drop UI for agent workflow specification, interactive evaluation and\\ndebugging of workflows, and a gallery of reusable agent components. We\\nhighlight four design principles for no-code multi-agent developer tools and\\ncontribute an open-source implementation at\\nhttps://github.com/microsoft/autogen/tree/main/samples/apps/autogen-studio', 'pdf_url': 'http://arxiv.org/pdf/2408.15247v1'}, {'title': 'Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration', 'authors': ['Cory Hymel', 'Sida Peng', 'Kevin Xu', 'Charath Ranganathan'], 'published': '2024-10-29', 'abstract': 'In recent years, with the rapid advancement of large language models (LLMs),\\nmulti-agent systems have become increasingly more capable of practical\\napplication. At the same time, the software development industry has had a\\nnumber of new AI-powered tools developed that improve the software development\\nlifecycle (SDLC). Academically, much attention has been paid to the role of\\nmulti-agent systems to the SDLC. And, while single-agent systems have\\nfrequently been examined in real-world applications, we have seen comparatively\\nfew real-world examples of publicly available commercial tools working together\\nin a multi-agent system with measurable improvements. In this experiment we\\ntest context sharing between Crowdbotics PRD AI, a tool for generating software\\nrequirements using AI, and GitHub Copilot, an AI pair-programming tool. By\\nsharing business requirements from PRD AI, we improve the code suggestion\\ncapabilities of GitHub Copilot by 13.8% and developer task success rate by\\n24.5% -- demonstrating a real-world example of commercially-available AI\\nsystems working together with improved outcomes.', 'pdf_url': 'http://arxiv.org/pdf/2410.22129v1'}, {'title': 'AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML', 'authors': ['Patara Trirat', 'Wonyong Jeong', 'Sung Ju Hwang'], 'published': '2024-10-03', 'abstract': \"Automated machine learning (AutoML) accelerates AI development by automating\\ntasks in the development pipeline, such as optimal model search and\\nhyperparameter tuning. Existing AutoML systems often require technical\\nexpertise to set up complex tools, which is in general time-consuming and\\nrequires a large amount of human effort. Therefore, recent works have started\\nexploiting large language models (LLM) to lessen such burden and increase the\\nusability of AutoML frameworks via a natural language interface, allowing\\nnon-expert users to build their data-driven solutions. These methods, however,\\nare usually designed only for a particular process in the AI development\\npipeline and do not efficiently use the inherent capacity of the LLMs. This\\npaper proposes AutoML-Agent, a novel multi-agent framework tailored for\\nfull-pipeline AutoML, i.e., from data retrieval to model deployment.\\nAutoML-Agent takes user's task descriptions, facilitates collaboration between\\nspecialized LLM agents, and delivers deployment-ready models. Unlike existing\\nwork, instead of devising a single plan, we introduce a retrieval-augmented\\nplanning strategy to enhance exploration to search for more optimal plans. We\\nalso decompose each plan into sub-tasks (e.g., data preprocessing and neural\\nnetwork design) each of which is solved by a specialized agent we build via\\nprompting executing in parallel, making the search process more efficient.\\nMoreover, we propose a multi-stage verification to verify executed results and\\nguide the code generation LLM in implementing successful solutions. Extensive\\nexperiments on seven downstream tasks using fourteen datasets show that\\nAutoML-Agent achieves a higher success rate in automating the full AutoML\\nprocess, yielding systems with good performance throughout the diverse domains.\", 'pdf_url': 'http://arxiv.org/pdf/2410.02958v1'}, {'title': 'Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges', 'authors': ['Sivan Schwartz', 'Avi Yaeli', 'Segev Shlomov'], 'published': '2023-08-10', 'abstract': 'Trust in AI agents has been extensively studied in the literature, resulting\\nin significant advancements in our understanding of this field. However, the\\nrapid advancements in Large Language Models (LLMs) and the emergence of\\nLLM-based AI agent frameworks pose new challenges and opportunities for further\\nresearch. In the field of process automation, a new generation of AI-based\\nagents has emerged, enabling the execution of complex tasks. At the same time,\\nthe process of building automation has become more accessible to business users\\nvia user-friendly no-code tools and training mechanisms. This paper explores\\nthese new challenges and opportunities, analyzes the main aspects of trust in\\nAI agents discussed in existing literature, and identifies specific\\nconsiderations and challenges relevant to this new generation of automation\\nagents. We also evaluate how nascent products in this category address these\\nconsiderations. Finally, we highlight several challenges that the research\\ncommunity should address in this evolving landscape.', 'pdf_url': 'http://arxiv.org/pdf/2308.05391v1'}, {'title': 'AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications', 'authors': ['Xin Pang', 'Zhucong Li', 'Jiaxiang Chen', 'Yuan Cheng', 'Yinghui Xu', 'Yuan Qi'], 'published': '2024-04-07', 'abstract': 'We introduce AI2Apps, a Visual Integrated Development Environment (Visual\\nIDE) with full-cycle capabilities that accelerates developers to build\\ndeployable LLM-based AI agent Applications. This Visual IDE prioritizes both\\nthe Integrity of its development tools and the Visuality of its components,\\nensuring a smooth and efficient building experience.On one hand, AI2Apps\\nintegrates a comprehensive development toolkit ranging from a prototyping\\ncanvas and AI-assisted code editor to agent debugger, management system, and\\ndeployment tools all within a web-based graphical user interface. On the other\\nhand, AI2Apps visualizes reusable front-end and back-end code as intuitive\\ndrag-and-drop components. Furthermore, a plugin system named AI2Apps Extension\\n(AAE) is designed for Extensibility, showcasing how a new plugin with 20\\ncomponents enables web agent to mimic human-like browsing behavior. Our case\\nstudy demonstrates substantial efficiency improvements, with AI2Apps reducing\\ntoken consumption and API calls when debugging a specific sophisticated\\nmultimodal agent by approximately 90% and 80%, respectively. The AI2Apps,\\nincluding an online demo, open-source code, and a screencast video, is now\\npublicly accessible.', 'pdf_url': 'http://arxiv.org/pdf/2404.04902v1'}]\n", + "---------- Report_Agent ----------\n", + "## Literature Review on No-Code Tools for Building Multi-Agent AI Systems\n", + "\n", + "### Introduction\n", + "\n", + "The emergence of multi-agent systems (MAS) has transformed various domains by enabling collaboration among multiple agents—ranging from generative AI models to orchestrated tools—to solve complex, long-term tasks. However, the traditional development of these systems often requires substantial technical expertise, making it inaccessible for non-developers. The introduction of no-code platforms aims to shift this paradigm, allowing users without formal programming knowledge to design, debug, and deploy multi-agent systems. This review synthesizes current literature concerning no-code tools developed for building multi-agent AI systems, highlighting recent advancements and emerging trends.\n", + "\n", + "### No-Code Development Tools\n", + "\n", + "#### AutoGen Studio\n", + "\n", + "One of the prominent no-code tools is **AutoGen Studio**, developed by Dibia et al. (2024). This tool provides a web interface and a declarative specification method utilizing JSON, enabling rapid prototyping, debugging, and evaluating multi-agent workflows. The drag-and-drop capabilities streamline the design process, making complex interactions between agents more manageable. The framework operates on four primary design principles that cater specifically to no-code development, contributing to an accessible pathway for users to harness multi-agent frameworks for various applications (Dibia et al., 2024).\n", + "\n", + "#### AI2Apps Visual IDE\n", + "\n", + "Another notable tool is **AI2Apps**, described by Pang et al. (2024). It serves as a Visual Integrated Development Environment that incorporates a comprehensive set of tools from prototyping to deployment. The platform's user-friendly interface allows for the visualization of code through drag-and-drop components, facilitating smoother integration of different agents. An extension system enhances the platform's capabilities, showcasing the potential for customization and scalability in agent application development. The reported efficiency improvements in token consumption and API calls indicate substantial benefits in user-centric design (Pang et al., 2024).\n", + "\n", + "### Performance Enhancements in Multi-Agent Configurations\n", + "\n", + "Hymel et al. (2024) examined the collaborative performance of commercially available AI tools, demonstrating a measurable improvement when integrating multiple agents in a shared configuration. Their experiments showcased how cooperation between tools like Crowdbotics PRD AI and GitHub Copilot significantly improved task success rates, illustrating the practical benefits of employing no-code tools in multi-agent environments. This synergy reflects the critical need for frameworks that inherently support such integrations, especially through no-code mechanisms, to enhance user experience and productivity (Hymel et al., 2024).\n", + "\n", + "### Trust and Usability in AI Agents\n", + "\n", + "The concept of trust in AI, particularly in LLM-based automation agents, has gained attention. Schwartz et al. (2023) addressed the challenges and considerations unique to this new generation of agents, highlighting how no-code platforms ease access and usability for non-technical users. The paper emphasizes the need for further research into the trust factors integral to effective multi-agent systems, advocating for a user-centric approach in the design and evaluation of these no-code tools (Schwartz et al., 2023).\n", + "\n", + "### Full-Pipeline AutoML with Multi-Agent Systems\n", + "\n", + "The **AutoML-Agent** framework proposed by Trirat et al. (2024) brings another layer of innovation to the no-code landscape. This framework enhances existing automated machine learning processes by using multiple specialized agents that collaboratively manage the full AI development pipeline from data retrieval to model deployment. The novelty lies in its retrieval-augmented planning strategy, which allows for efficient task decomposition and parallel execution, optimizing the overall development experience for non-experts (Trirat et al., 2024).\n", + "\n", + "### Conclusion\n", + "\n", + "The literature presents a growing array of no-code tools designed to democratize the development of multi-agent systems. Innovations such as AutoGen Studio, AI2Apps, and collaborative frameworks like AutoML-Agent highlight a trend towards user-centric, efficient design that encourages participation beyond technical boundaries. Future research should continue to explore aspects of trust, usability, and integration to further refine these tools and expand their applicability across various domains.\n", + "\n", + "### References\n", + "\n", + "- Dibia, V., Chen, J., Bansal, G., Syed, S., Fourney, A., Zhu, E., Wang, C., & Amershi, S. (2024). AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems. *arXiv:2408.15247*.\n", + "- Hymel, C., Peng, S., Xu, K., & Ranganathan, C. (2024). Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration. *arXiv:2410.22129*.\n", + "- Pang, X., Li, Z., Chen, J., Cheng, Y., Xu, Y., & Qi, Y. (2024). AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications. *arXiv:2404.04902*.\n", + "- Schwartz, S., Yaeli, A., & Shlomov, S. (2023). Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges. *arXiv:2308.05391*.\n", + "- Trirat, P., Jeong, W., & Hwang, S. J. (2024). AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML. *arXiv:2410.02958*.\n", + "\n", + "TERMINATE\n", + "[Prompt tokens: 2381, Completion tokens: 1090]\n", + "---------- Summary ----------\n", + "Number of messages: 8\n", + "Finish reason: Text 'TERMINATE' mentioned\n", + "Total prompt tokens: 3223\n", + "Total completion tokens: 1147\n", + "Duration: 17.06 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a literature review on no code tools for building multi agent ai systems', type='TextMessage'), ToolCallRequestEvent(source='Google_Search_Agent', models_usage=RequestUsage(prompt_tokens=123, completion_tokens=29), content=[FunctionCall(id='call_bNGwWFsfeTwDhtIpsI6GYISR', arguments='{\"query\":\"no code tools for building multi agent AI systems literature review\",\"num_results\":3}', name='google_search')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='Google_Search_Agent', models_usage=None, content=[FunctionExecutionResult(content='[{\\'title\\': \\'Literature Review — AutoGen\\', \\'link\\': \\'https://microsoft.github.io/autogen/dev//user-guide/agentchat-user-guide/examples/literature-review.html\\', \\'snippet\\': \\'run( task=\"Write a literature review on no code tools for building multi agent ai systems\", ) ... ### Conclusion No-code tools for building multi-agent AI systems\\\\xa0...\\', \\'body\\': \\'Literature Review — AutoGen Skip to main content Back to top Ctrl + K AutoGen 0.4 is a work in progress. Go here to find the 0.2 documentation. User Guide Packages API Reference Twitter GitHub PyPI User Guide Packages API Reference Twitter GitHub PyPI AgentChat Installation Quickstart Tutorial Models Messages Agents Teams Selector Group Chat Swarm Termination Custom Agents Managing State Examples Travel Planning Company Research Literature Review Core Quick Start Core Concepts Agent and\\'}, {\\'title\\': \\'Vertex AI Agent Builder | Google Cloud\\', \\'link\\': \\'https://cloud.google.com/products/agent-builder\\', \\'snippet\\': \\'Build and deploy enterprise ready generative AI experiences · Product highlights · Easily build no code conversational AI agents · Ground in Google search and/or\\\\xa0...\\', \\'body\\': \\'Vertex AI Agent Builder | Google Cloud Page Contents Vertex AI Agent Builder is making generative AI more reliable for the enterprise. Read the blog. Vertex AI Agent Builder Build and deploy enterprise ready generative AI experiences Create AI agents and applications using natural language or a code-first approach. Easily ground your agents or apps in enterprise data with a range of options. Vertex AI Agent Builder gathers all the surfaces and tools that developers need to build their AI agents\\'}, {\\'title\\': \\'AI tools I have found useful w/ research. What do you guys think ...\\', \\'link\\': \\'https://www.reddit.com/r/PhD/comments/14d6g09/ai_tools_i_have_found_useful_w_research_what_do/\\', \\'snippet\\': \"Jun 19, 2023 ... Need help deciding on the best ones, and to identify ones I\\'ve missed: ASSISTANTS (chatbots, multi-purpose) Chat with Open Large Language Models.\", \\'body\\': \\'Reddit - Dive into anything Skip to main content Open menu Open navigation Go to Reddit Home r/PhD A chip A close button Get app Get the Reddit app Log In Log in to Reddit Expand user menu Open settings menu Log In / Sign Up Advertise on Reddit Shop Collectible Avatars Get the Reddit app Scan this QR code to download the app now Or check it out in the app stores Go to PhD r/PhD r/PhD A subreddit dedicated to PhDs. Members Online • [deleted] ADMIN MOD AI tools I have found useful w/ research.\\'}]', call_id='call_bNGwWFsfeTwDhtIpsI6GYISR')], type='ToolCallExecutionEvent'), TextMessage(source='Google_Search_Agent', models_usage=None, content='Tool calls:\\ngoogle_search({\"query\":\"no code tools for building multi agent AI systems literature review\",\"num_results\":3}) = [{\\'title\\': \\'Literature Review — AutoGen\\', \\'link\\': \\'https://microsoft.github.io/autogen/dev//user-guide/agentchat-user-guide/examples/literature-review.html\\', \\'snippet\\': \\'run( task=\"Write a literature review on no code tools for building multi agent ai systems\", ) ... ### Conclusion No-code tools for building multi-agent AI systems\\\\xa0...\\', \\'body\\': \\'Literature Review — AutoGen Skip to main content Back to top Ctrl + K AutoGen 0.4 is a work in progress. Go here to find the 0.2 documentation. User Guide Packages API Reference Twitter GitHub PyPI User Guide Packages API Reference Twitter GitHub PyPI AgentChat Installation Quickstart Tutorial Models Messages Agents Teams Selector Group Chat Swarm Termination Custom Agents Managing State Examples Travel Planning Company Research Literature Review Core Quick Start Core Concepts Agent and\\'}, {\\'title\\': \\'Vertex AI Agent Builder | Google Cloud\\', \\'link\\': \\'https://cloud.google.com/products/agent-builder\\', \\'snippet\\': \\'Build and deploy enterprise ready generative AI experiences · Product highlights · Easily build no code conversational AI agents · Ground in Google search and/or\\\\xa0...\\', \\'body\\': \\'Vertex AI Agent Builder | Google Cloud Page Contents Vertex AI Agent Builder is making generative AI more reliable for the enterprise. Read the blog. Vertex AI Agent Builder Build and deploy enterprise ready generative AI experiences Create AI agents and applications using natural language or a code-first approach. Easily ground your agents or apps in enterprise data with a range of options. Vertex AI Agent Builder gathers all the surfaces and tools that developers need to build their AI agents\\'}, {\\'title\\': \\'AI tools I have found useful w/ research. What do you guys think ...\\', \\'link\\': \\'https://www.reddit.com/r/PhD/comments/14d6g09/ai_tools_i_have_found_useful_w_research_what_do/\\', \\'snippet\\': \"Jun 19, 2023 ... Need help deciding on the best ones, and to identify ones I\\'ve missed: ASSISTANTS (chatbots, multi-purpose) Chat with Open Large Language Models.\", \\'body\\': \\'Reddit - Dive into anything Skip to main content Open menu Open navigation Go to Reddit Home r/PhD A chip A close button Get app Get the Reddit app Log In Log in to Reddit Expand user menu Open settings menu Log In / Sign Up Advertise on Reddit Shop Collectible Avatars Get the Reddit app Scan this QR code to download the app now Or check it out in the app stores Go to PhD r/PhD r/PhD A subreddit dedicated to PhDs. Members Online • [deleted] ADMIN MOD AI tools I have found useful w/ research.\\'}]', type='TextMessage'), ToolCallRequestEvent(source='Arxiv_Search_Agent', models_usage=RequestUsage(prompt_tokens=719, completion_tokens=28), content=[FunctionCall(id='call_ZdmwQGTO03X23GeRn6fwDN8q', arguments='{\"query\":\"no code tools for building multi agent AI systems\",\"max_results\":5}', name='arxiv_search')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='Arxiv_Search_Agent', models_usage=None, content=[FunctionExecutionResult(content='[{\\'title\\': \\'AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems\\', \\'authors\\': [\\'Victor Dibia\\', \\'Jingya Chen\\', \\'Gagan Bansal\\', \\'Suff Syed\\', \\'Adam Fourney\\', \\'Erkang Zhu\\', \\'Chi Wang\\', \\'Saleema Amershi\\'], \\'published\\': \\'2024-08-09\\', \\'abstract\\': \\'Multi-agent systems, where multiple agents (generative AI models + tools)\\\\ncollaborate, are emerging as an effective pattern for solving long-running,\\\\ncomplex tasks in numerous domains. However, specifying their parameters (such\\\\nas models, tools, and orchestration mechanisms etc,.) and debugging them\\\\nremains challenging for most developers. To address this challenge, we present\\\\nAUTOGEN STUDIO, a no-code developer tool for rapidly prototyping, debugging,\\\\nand evaluating multi-agent workflows built upon the AUTOGEN framework. AUTOGEN\\\\nSTUDIO offers a web interface and a Python API for representing LLM-enabled\\\\nagents using a declarative (JSON-based) specification. It provides an intuitive\\\\ndrag-and-drop UI for agent workflow specification, interactive evaluation and\\\\ndebugging of workflows, and a gallery of reusable agent components. We\\\\nhighlight four design principles for no-code multi-agent developer tools and\\\\ncontribute an open-source implementation at\\\\nhttps://github.com/microsoft/autogen/tree/main/samples/apps/autogen-studio\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2408.15247v1\\'}, {\\'title\\': \\'Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration\\', \\'authors\\': [\\'Cory Hymel\\', \\'Sida Peng\\', \\'Kevin Xu\\', \\'Charath Ranganathan\\'], \\'published\\': \\'2024-10-29\\', \\'abstract\\': \\'In recent years, with the rapid advancement of large language models (LLMs),\\\\nmulti-agent systems have become increasingly more capable of practical\\\\napplication. At the same time, the software development industry has had a\\\\nnumber of new AI-powered tools developed that improve the software development\\\\nlifecycle (SDLC). Academically, much attention has been paid to the role of\\\\nmulti-agent systems to the SDLC. And, while single-agent systems have\\\\nfrequently been examined in real-world applications, we have seen comparatively\\\\nfew real-world examples of publicly available commercial tools working together\\\\nin a multi-agent system with measurable improvements. In this experiment we\\\\ntest context sharing between Crowdbotics PRD AI, a tool for generating software\\\\nrequirements using AI, and GitHub Copilot, an AI pair-programming tool. By\\\\nsharing business requirements from PRD AI, we improve the code suggestion\\\\ncapabilities of GitHub Copilot by 13.8% and developer task success rate by\\\\n24.5% -- demonstrating a real-world example of commercially-available AI\\\\nsystems working together with improved outcomes.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.22129v1\\'}, {\\'title\\': \\'AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML\\', \\'authors\\': [\\'Patara Trirat\\', \\'Wonyong Jeong\\', \\'Sung Ju Hwang\\'], \\'published\\': \\'2024-10-03\\', \\'abstract\\': \"Automated machine learning (AutoML) accelerates AI development by automating\\\\ntasks in the development pipeline, such as optimal model search and\\\\nhyperparameter tuning. Existing AutoML systems often require technical\\\\nexpertise to set up complex tools, which is in general time-consuming and\\\\nrequires a large amount of human effort. Therefore, recent works have started\\\\nexploiting large language models (LLM) to lessen such burden and increase the\\\\nusability of AutoML frameworks via a natural language interface, allowing\\\\nnon-expert users to build their data-driven solutions. These methods, however,\\\\nare usually designed only for a particular process in the AI development\\\\npipeline and do not efficiently use the inherent capacity of the LLMs. This\\\\npaper proposes AutoML-Agent, a novel multi-agent framework tailored for\\\\nfull-pipeline AutoML, i.e., from data retrieval to model deployment.\\\\nAutoML-Agent takes user\\'s task descriptions, facilitates collaboration between\\\\nspecialized LLM agents, and delivers deployment-ready models. Unlike existing\\\\nwork, instead of devising a single plan, we introduce a retrieval-augmented\\\\nplanning strategy to enhance exploration to search for more optimal plans. We\\\\nalso decompose each plan into sub-tasks (e.g., data preprocessing and neural\\\\nnetwork design) each of which is solved by a specialized agent we build via\\\\nprompting executing in parallel, making the search process more efficient.\\\\nMoreover, we propose a multi-stage verification to verify executed results and\\\\nguide the code generation LLM in implementing successful solutions. Extensive\\\\nexperiments on seven downstream tasks using fourteen datasets show that\\\\nAutoML-Agent achieves a higher success rate in automating the full AutoML\\\\nprocess, yielding systems with good performance throughout the diverse domains.\", \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.02958v1\\'}, {\\'title\\': \\'Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges\\', \\'authors\\': [\\'Sivan Schwartz\\', \\'Avi Yaeli\\', \\'Segev Shlomov\\'], \\'published\\': \\'2023-08-10\\', \\'abstract\\': \\'Trust in AI agents has been extensively studied in the literature, resulting\\\\nin significant advancements in our understanding of this field. However, the\\\\nrapid advancements in Large Language Models (LLMs) and the emergence of\\\\nLLM-based AI agent frameworks pose new challenges and opportunities for further\\\\nresearch. In the field of process automation, a new generation of AI-based\\\\nagents has emerged, enabling the execution of complex tasks. At the same time,\\\\nthe process of building automation has become more accessible to business users\\\\nvia user-friendly no-code tools and training mechanisms. This paper explores\\\\nthese new challenges and opportunities, analyzes the main aspects of trust in\\\\nAI agents discussed in existing literature, and identifies specific\\\\nconsiderations and challenges relevant to this new generation of automation\\\\nagents. We also evaluate how nascent products in this category address these\\\\nconsiderations. Finally, we highlight several challenges that the research\\\\ncommunity should address in this evolving landscape.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2308.05391v1\\'}, {\\'title\\': \\'AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications\\', \\'authors\\': [\\'Xin Pang\\', \\'Zhucong Li\\', \\'Jiaxiang Chen\\', \\'Yuan Cheng\\', \\'Yinghui Xu\\', \\'Yuan Qi\\'], \\'published\\': \\'2024-04-07\\', \\'abstract\\': \\'We introduce AI2Apps, a Visual Integrated Development Environment (Visual\\\\nIDE) with full-cycle capabilities that accelerates developers to build\\\\ndeployable LLM-based AI agent Applications. This Visual IDE prioritizes both\\\\nthe Integrity of its development tools and the Visuality of its components,\\\\nensuring a smooth and efficient building experience.On one hand, AI2Apps\\\\nintegrates a comprehensive development toolkit ranging from a prototyping\\\\ncanvas and AI-assisted code editor to agent debugger, management system, and\\\\ndeployment tools all within a web-based graphical user interface. On the other\\\\nhand, AI2Apps visualizes reusable front-end and back-end code as intuitive\\\\ndrag-and-drop components. Furthermore, a plugin system named AI2Apps Extension\\\\n(AAE) is designed for Extensibility, showcasing how a new plugin with 20\\\\ncomponents enables web agent to mimic human-like browsing behavior. Our case\\\\nstudy demonstrates substantial efficiency improvements, with AI2Apps reducing\\\\ntoken consumption and API calls when debugging a specific sophisticated\\\\nmultimodal agent by approximately 90% and 80%, respectively. The AI2Apps,\\\\nincluding an online demo, open-source code, and a screencast video, is now\\\\npublicly accessible.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2404.04902v1\\'}]', call_id='call_ZdmwQGTO03X23GeRn6fwDN8q')], type='ToolCallExecutionEvent'), TextMessage(source='Arxiv_Search_Agent', models_usage=None, content='Tool calls:\\narxiv_search({\"query\":\"no code tools for building multi agent AI systems\",\"max_results\":5}) = [{\\'title\\': \\'AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems\\', \\'authors\\': [\\'Victor Dibia\\', \\'Jingya Chen\\', \\'Gagan Bansal\\', \\'Suff Syed\\', \\'Adam Fourney\\', \\'Erkang Zhu\\', \\'Chi Wang\\', \\'Saleema Amershi\\'], \\'published\\': \\'2024-08-09\\', \\'abstract\\': \\'Multi-agent systems, where multiple agents (generative AI models + tools)\\\\ncollaborate, are emerging as an effective pattern for solving long-running,\\\\ncomplex tasks in numerous domains. However, specifying their parameters (such\\\\nas models, tools, and orchestration mechanisms etc,.) and debugging them\\\\nremains challenging for most developers. To address this challenge, we present\\\\nAUTOGEN STUDIO, a no-code developer tool for rapidly prototyping, debugging,\\\\nand evaluating multi-agent workflows built upon the AUTOGEN framework. AUTOGEN\\\\nSTUDIO offers a web interface and a Python API for representing LLM-enabled\\\\nagents using a declarative (JSON-based) specification. It provides an intuitive\\\\ndrag-and-drop UI for agent workflow specification, interactive evaluation and\\\\ndebugging of workflows, and a gallery of reusable agent components. We\\\\nhighlight four design principles for no-code multi-agent developer tools and\\\\ncontribute an open-source implementation at\\\\nhttps://github.com/microsoft/autogen/tree/main/samples/apps/autogen-studio\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2408.15247v1\\'}, {\\'title\\': \\'Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration\\', \\'authors\\': [\\'Cory Hymel\\', \\'Sida Peng\\', \\'Kevin Xu\\', \\'Charath Ranganathan\\'], \\'published\\': \\'2024-10-29\\', \\'abstract\\': \\'In recent years, with the rapid advancement of large language models (LLMs),\\\\nmulti-agent systems have become increasingly more capable of practical\\\\napplication. At the same time, the software development industry has had a\\\\nnumber of new AI-powered tools developed that improve the software development\\\\nlifecycle (SDLC). Academically, much attention has been paid to the role of\\\\nmulti-agent systems to the SDLC. And, while single-agent systems have\\\\nfrequently been examined in real-world applications, we have seen comparatively\\\\nfew real-world examples of publicly available commercial tools working together\\\\nin a multi-agent system with measurable improvements. In this experiment we\\\\ntest context sharing between Crowdbotics PRD AI, a tool for generating software\\\\nrequirements using AI, and GitHub Copilot, an AI pair-programming tool. By\\\\nsharing business requirements from PRD AI, we improve the code suggestion\\\\ncapabilities of GitHub Copilot by 13.8% and developer task success rate by\\\\n24.5% -- demonstrating a real-world example of commercially-available AI\\\\nsystems working together with improved outcomes.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.22129v1\\'}, {\\'title\\': \\'AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML\\', \\'authors\\': [\\'Patara Trirat\\', \\'Wonyong Jeong\\', \\'Sung Ju Hwang\\'], \\'published\\': \\'2024-10-03\\', \\'abstract\\': \"Automated machine learning (AutoML) accelerates AI development by automating\\\\ntasks in the development pipeline, such as optimal model search and\\\\nhyperparameter tuning. Existing AutoML systems often require technical\\\\nexpertise to set up complex tools, which is in general time-consuming and\\\\nrequires a large amount of human effort. Therefore, recent works have started\\\\nexploiting large language models (LLM) to lessen such burden and increase the\\\\nusability of AutoML frameworks via a natural language interface, allowing\\\\nnon-expert users to build their data-driven solutions. These methods, however,\\\\nare usually designed only for a particular process in the AI development\\\\npipeline and do not efficiently use the inherent capacity of the LLMs. This\\\\npaper proposes AutoML-Agent, a novel multi-agent framework tailored for\\\\nfull-pipeline AutoML, i.e., from data retrieval to model deployment.\\\\nAutoML-Agent takes user\\'s task descriptions, facilitates collaboration between\\\\nspecialized LLM agents, and delivers deployment-ready models. Unlike existing\\\\nwork, instead of devising a single plan, we introduce a retrieval-augmented\\\\nplanning strategy to enhance exploration to search for more optimal plans. We\\\\nalso decompose each plan into sub-tasks (e.g., data preprocessing and neural\\\\nnetwork design) each of which is solved by a specialized agent we build via\\\\nprompting executing in parallel, making the search process more efficient.\\\\nMoreover, we propose a multi-stage verification to verify executed results and\\\\nguide the code generation LLM in implementing successful solutions. Extensive\\\\nexperiments on seven downstream tasks using fourteen datasets show that\\\\nAutoML-Agent achieves a higher success rate in automating the full AutoML\\\\nprocess, yielding systems with good performance throughout the diverse domains.\", \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.02958v1\\'}, {\\'title\\': \\'Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges\\', \\'authors\\': [\\'Sivan Schwartz\\', \\'Avi Yaeli\\', \\'Segev Shlomov\\'], \\'published\\': \\'2023-08-10\\', \\'abstract\\': \\'Trust in AI agents has been extensively studied in the literature, resulting\\\\nin significant advancements in our understanding of this field. However, the\\\\nrapid advancements in Large Language Models (LLMs) and the emergence of\\\\nLLM-based AI agent frameworks pose new challenges and opportunities for further\\\\nresearch. In the field of process automation, a new generation of AI-based\\\\nagents has emerged, enabling the execution of complex tasks. At the same time,\\\\nthe process of building automation has become more accessible to business users\\\\nvia user-friendly no-code tools and training mechanisms. This paper explores\\\\nthese new challenges and opportunities, analyzes the main aspects of trust in\\\\nAI agents discussed in existing literature, and identifies specific\\\\nconsiderations and challenges relevant to this new generation of automation\\\\nagents. We also evaluate how nascent products in this category address these\\\\nconsiderations. Finally, we highlight several challenges that the research\\\\ncommunity should address in this evolving landscape.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2308.05391v1\\'}, {\\'title\\': \\'AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications\\', \\'authors\\': [\\'Xin Pang\\', \\'Zhucong Li\\', \\'Jiaxiang Chen\\', \\'Yuan Cheng\\', \\'Yinghui Xu\\', \\'Yuan Qi\\'], \\'published\\': \\'2024-04-07\\', \\'abstract\\': \\'We introduce AI2Apps, a Visual Integrated Development Environment (Visual\\\\nIDE) with full-cycle capabilities that accelerates developers to build\\\\ndeployable LLM-based AI agent Applications. This Visual IDE prioritizes both\\\\nthe Integrity of its development tools and the Visuality of its components,\\\\nensuring a smooth and efficient building experience.On one hand, AI2Apps\\\\nintegrates a comprehensive development toolkit ranging from a prototyping\\\\ncanvas and AI-assisted code editor to agent debugger, management system, and\\\\ndeployment tools all within a web-based graphical user interface. On the other\\\\nhand, AI2Apps visualizes reusable front-end and back-end code as intuitive\\\\ndrag-and-drop components. Furthermore, a plugin system named AI2Apps Extension\\\\n(AAE) is designed for Extensibility, showcasing how a new plugin with 20\\\\ncomponents enables web agent to mimic human-like browsing behavior. Our case\\\\nstudy demonstrates substantial efficiency improvements, with AI2Apps reducing\\\\ntoken consumption and API calls when debugging a specific sophisticated\\\\nmultimodal agent by approximately 90% and 80%, respectively. The AI2Apps,\\\\nincluding an online demo, open-source code, and a screencast video, is now\\\\npublicly accessible.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2404.04902v1\\'}]', type='TextMessage'), TextMessage(source='Report_Agent', models_usage=RequestUsage(prompt_tokens=2381, completion_tokens=1090), content=\"## Literature Review on No-Code Tools for Building Multi-Agent AI Systems\\n\\n### Introduction\\n\\nThe emergence of multi-agent systems (MAS) has transformed various domains by enabling collaboration among multiple agents—ranging from generative AI models to orchestrated tools—to solve complex, long-term tasks. However, the traditional development of these systems often requires substantial technical expertise, making it inaccessible for non-developers. The introduction of no-code platforms aims to shift this paradigm, allowing users without formal programming knowledge to design, debug, and deploy multi-agent systems. This review synthesizes current literature concerning no-code tools developed for building multi-agent AI systems, highlighting recent advancements and emerging trends.\\n\\n### No-Code Development Tools\\n\\n#### AutoGen Studio\\n\\nOne of the prominent no-code tools is **AutoGen Studio**, developed by Dibia et al. (2024). This tool provides a web interface and a declarative specification method utilizing JSON, enabling rapid prototyping, debugging, and evaluating multi-agent workflows. The drag-and-drop capabilities streamline the design process, making complex interactions between agents more manageable. The framework operates on four primary design principles that cater specifically to no-code development, contributing to an accessible pathway for users to harness multi-agent frameworks for various applications (Dibia et al., 2024).\\n\\n#### AI2Apps Visual IDE\\n\\nAnother notable tool is **AI2Apps**, described by Pang et al. (2024). It serves as a Visual Integrated Development Environment that incorporates a comprehensive set of tools from prototyping to deployment. The platform's user-friendly interface allows for the visualization of code through drag-and-drop components, facilitating smoother integration of different agents. An extension system enhances the platform's capabilities, showcasing the potential for customization and scalability in agent application development. The reported efficiency improvements in token consumption and API calls indicate substantial benefits in user-centric design (Pang et al., 2024).\\n\\n### Performance Enhancements in Multi-Agent Configurations\\n\\nHymel et al. (2024) examined the collaborative performance of commercially available AI tools, demonstrating a measurable improvement when integrating multiple agents in a shared configuration. Their experiments showcased how cooperation between tools like Crowdbotics PRD AI and GitHub Copilot significantly improved task success rates, illustrating the practical benefits of employing no-code tools in multi-agent environments. This synergy reflects the critical need for frameworks that inherently support such integrations, especially through no-code mechanisms, to enhance user experience and productivity (Hymel et al., 2024).\\n\\n### Trust and Usability in AI Agents\\n\\nThe concept of trust in AI, particularly in LLM-based automation agents, has gained attention. Schwartz et al. (2023) addressed the challenges and considerations unique to this new generation of agents, highlighting how no-code platforms ease access and usability for non-technical users. The paper emphasizes the need for further research into the trust factors integral to effective multi-agent systems, advocating for a user-centric approach in the design and evaluation of these no-code tools (Schwartz et al., 2023).\\n\\n### Full-Pipeline AutoML with Multi-Agent Systems\\n\\nThe **AutoML-Agent** framework proposed by Trirat et al. (2024) brings another layer of innovation to the no-code landscape. This framework enhances existing automated machine learning processes by using multiple specialized agents that collaboratively manage the full AI development pipeline from data retrieval to model deployment. The novelty lies in its retrieval-augmented planning strategy, which allows for efficient task decomposition and parallel execution, optimizing the overall development experience for non-experts (Trirat et al., 2024).\\n\\n### Conclusion\\n\\nThe literature presents a growing array of no-code tools designed to democratize the development of multi-agent systems. Innovations such as AutoGen Studio, AI2Apps, and collaborative frameworks like AutoML-Agent highlight a trend towards user-centric, efficient design that encourages participation beyond technical boundaries. Future research should continue to explore aspects of trust, usability, and integration to further refine these tools and expand their applicability across various domains.\\n\\n### References\\n\\n- Dibia, V., Chen, J., Bansal, G., Syed, S., Fourney, A., Zhu, E., Wang, C., & Amershi, S. (2024). AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems. *arXiv:2408.15247*.\\n- Hymel, C., Peng, S., Xu, K., & Ranganathan, C. (2024). Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration. *arXiv:2410.22129*.\\n- Pang, X., Li, Z., Chen, J., Cheng, Y., Xu, Y., & Qi, Y. (2024). AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications. *arXiv:2404.04902*.\\n- Schwartz, S., Yaeli, A., & Shlomov, S. (2023). Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges. *arXiv:2308.05391*.\\n- Trirat, P., Jeong, W., & Hwang, S. J. (2024). AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML. *arXiv:2410.02958*.\\n\\nTERMINATE\", type='TextMessage')], stop_reason=\"Text 'TERMINATE' mentioned\")" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "await Console(\n", + " team.run_stream(\n", + " task=\"Write a literature review on no code tools for building multi agent ai systems\",\n", + " )\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } }, - { - "data": { - "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a literature review on no code tools for building multi agent ai systems', type='TextMessage'), ToolCallMessage(source='Google_Search_Agent', models_usage=RequestUsage(prompt_tokens=123, completion_tokens=29), content=[FunctionCall(id='call_bNGwWFsfeTwDhtIpsI6GYISR', arguments='{\"query\":\"no code tools for building multi agent AI systems literature review\",\"num_results\":3}', name='google_search')], type='ToolCallMessage'), ToolCallResultMessage(source='Google_Search_Agent', models_usage=None, content=[FunctionExecutionResult(content='[{\\'title\\': \\'Literature Review — AutoGen\\', \\'link\\': \\'https://microsoft.github.io/autogen/dev//user-guide/agentchat-user-guide/examples/literature-review.html\\', \\'snippet\\': \\'run( task=\"Write a literature review on no code tools for building multi agent ai systems\", ) ... ### Conclusion No-code tools for building multi-agent AI systems\\\\xa0...\\', \\'body\\': \\'Literature Review — AutoGen Skip to main content Back to top Ctrl + K AutoGen 0.4 is a work in progress. Go here to find the 0.2 documentation. User Guide Packages API Reference Twitter GitHub PyPI User Guide Packages API Reference Twitter GitHub PyPI AgentChat Installation Quickstart Tutorial Models Messages Agents Teams Selector Group Chat Swarm Termination Custom Agents Managing State Examples Travel Planning Company Research Literature Review Core Quick Start Core Concepts Agent and\\'}, {\\'title\\': \\'Vertex AI Agent Builder | Google Cloud\\', \\'link\\': \\'https://cloud.google.com/products/agent-builder\\', \\'snippet\\': \\'Build and deploy enterprise ready generative AI experiences · Product highlights · Easily build no code conversational AI agents · Ground in Google search and/or\\\\xa0...\\', \\'body\\': \\'Vertex AI Agent Builder | Google Cloud Page Contents Vertex AI Agent Builder is making generative AI more reliable for the enterprise. Read the blog. Vertex AI Agent Builder Build and deploy enterprise ready generative AI experiences Create AI agents and applications using natural language or a code-first approach. Easily ground your agents or apps in enterprise data with a range of options. Vertex AI Agent Builder gathers all the surfaces and tools that developers need to build their AI agents\\'}, {\\'title\\': \\'AI tools I have found useful w/ research. What do you guys think ...\\', \\'link\\': \\'https://www.reddit.com/r/PhD/comments/14d6g09/ai_tools_i_have_found_useful_w_research_what_do/\\', \\'snippet\\': \"Jun 19, 2023 ... Need help deciding on the best ones, and to identify ones I\\'ve missed: ASSISTANTS (chatbots, multi-purpose) Chat with Open Large Language Models.\", \\'body\\': \\'Reddit - Dive into anything Skip to main content Open menu Open navigation Go to Reddit Home r/PhD A chip A close button Get app Get the Reddit app Log In Log in to Reddit Expand user menu Open settings menu Log In / Sign Up Advertise on Reddit Shop Collectible Avatars Get the Reddit app Scan this QR code to download the app now Or check it out in the app stores Go to PhD r/PhD r/PhD A subreddit dedicated to PhDs. Members Online • [deleted] ADMIN MOD AI tools I have found useful w/ research.\\'}]', call_id='call_bNGwWFsfeTwDhtIpsI6GYISR')], type='ToolCallResultMessage'), TextMessage(source='Google_Search_Agent', models_usage=None, content='Tool calls:\\ngoogle_search({\"query\":\"no code tools for building multi agent AI systems literature review\",\"num_results\":3}) = [{\\'title\\': \\'Literature Review — AutoGen\\', \\'link\\': \\'https://microsoft.github.io/autogen/dev//user-guide/agentchat-user-guide/examples/literature-review.html\\', \\'snippet\\': \\'run( task=\"Write a literature review on no code tools for building multi agent ai systems\", ) ... ### Conclusion No-code tools for building multi-agent AI systems\\\\xa0...\\', \\'body\\': \\'Literature Review — AutoGen Skip to main content Back to top Ctrl + K AutoGen 0.4 is a work in progress. Go here to find the 0.2 documentation. User Guide Packages API Reference Twitter GitHub PyPI User Guide Packages API Reference Twitter GitHub PyPI AgentChat Installation Quickstart Tutorial Models Messages Agents Teams Selector Group Chat Swarm Termination Custom Agents Managing State Examples Travel Planning Company Research Literature Review Core Quick Start Core Concepts Agent and\\'}, {\\'title\\': \\'Vertex AI Agent Builder | Google Cloud\\', \\'link\\': \\'https://cloud.google.com/products/agent-builder\\', \\'snippet\\': \\'Build and deploy enterprise ready generative AI experiences · Product highlights · Easily build no code conversational AI agents · Ground in Google search and/or\\\\xa0...\\', \\'body\\': \\'Vertex AI Agent Builder | Google Cloud Page Contents Vertex AI Agent Builder is making generative AI more reliable for the enterprise. Read the blog. Vertex AI Agent Builder Build and deploy enterprise ready generative AI experiences Create AI agents and applications using natural language or a code-first approach. Easily ground your agents or apps in enterprise data with a range of options. Vertex AI Agent Builder gathers all the surfaces and tools that developers need to build their AI agents\\'}, {\\'title\\': \\'AI tools I have found useful w/ research. What do you guys think ...\\', \\'link\\': \\'https://www.reddit.com/r/PhD/comments/14d6g09/ai_tools_i_have_found_useful_w_research_what_do/\\', \\'snippet\\': \"Jun 19, 2023 ... Need help deciding on the best ones, and to identify ones I\\'ve missed: ASSISTANTS (chatbots, multi-purpose) Chat with Open Large Language Models.\", \\'body\\': \\'Reddit - Dive into anything Skip to main content Open menu Open navigation Go to Reddit Home r/PhD A chip A close button Get app Get the Reddit app Log In Log in to Reddit Expand user menu Open settings menu Log In / Sign Up Advertise on Reddit Shop Collectible Avatars Get the Reddit app Scan this QR code to download the app now Or check it out in the app stores Go to PhD r/PhD r/PhD A subreddit dedicated to PhDs. Members Online • [deleted] ADMIN MOD AI tools I have found useful w/ research.\\'}]', type='TextMessage'), ToolCallMessage(source='Arxiv_Search_Agent', models_usage=RequestUsage(prompt_tokens=719, completion_tokens=28), content=[FunctionCall(id='call_ZdmwQGTO03X23GeRn6fwDN8q', arguments='{\"query\":\"no code tools for building multi agent AI systems\",\"max_results\":5}', name='arxiv_search')], type='ToolCallMessage'), ToolCallResultMessage(source='Arxiv_Search_Agent', models_usage=None, content=[FunctionExecutionResult(content='[{\\'title\\': \\'AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems\\', \\'authors\\': [\\'Victor Dibia\\', \\'Jingya Chen\\', \\'Gagan Bansal\\', \\'Suff Syed\\', \\'Adam Fourney\\', \\'Erkang Zhu\\', \\'Chi Wang\\', \\'Saleema Amershi\\'], \\'published\\': \\'2024-08-09\\', \\'abstract\\': \\'Multi-agent systems, where multiple agents (generative AI models + tools)\\\\ncollaborate, are emerging as an effective pattern for solving long-running,\\\\ncomplex tasks in numerous domains. However, specifying their parameters (such\\\\nas models, tools, and orchestration mechanisms etc,.) and debugging them\\\\nremains challenging for most developers. To address this challenge, we present\\\\nAUTOGEN STUDIO, a no-code developer tool for rapidly prototyping, debugging,\\\\nand evaluating multi-agent workflows built upon the AUTOGEN framework. AUTOGEN\\\\nSTUDIO offers a web interface and a Python API for representing LLM-enabled\\\\nagents using a declarative (JSON-based) specification. It provides an intuitive\\\\ndrag-and-drop UI for agent workflow specification, interactive evaluation and\\\\ndebugging of workflows, and a gallery of reusable agent components. We\\\\nhighlight four design principles for no-code multi-agent developer tools and\\\\ncontribute an open-source implementation at\\\\nhttps://github.com/microsoft/autogen/tree/main/samples/apps/autogen-studio\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2408.15247v1\\'}, {\\'title\\': \\'Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration\\', \\'authors\\': [\\'Cory Hymel\\', \\'Sida Peng\\', \\'Kevin Xu\\', \\'Charath Ranganathan\\'], \\'published\\': \\'2024-10-29\\', \\'abstract\\': \\'In recent years, with the rapid advancement of large language models (LLMs),\\\\nmulti-agent systems have become increasingly more capable of practical\\\\napplication. At the same time, the software development industry has had a\\\\nnumber of new AI-powered tools developed that improve the software development\\\\nlifecycle (SDLC). Academically, much attention has been paid to the role of\\\\nmulti-agent systems to the SDLC. And, while single-agent systems have\\\\nfrequently been examined in real-world applications, we have seen comparatively\\\\nfew real-world examples of publicly available commercial tools working together\\\\nin a multi-agent system with measurable improvements. In this experiment we\\\\ntest context sharing between Crowdbotics PRD AI, a tool for generating software\\\\nrequirements using AI, and GitHub Copilot, an AI pair-programming tool. By\\\\nsharing business requirements from PRD AI, we improve the code suggestion\\\\ncapabilities of GitHub Copilot by 13.8% and developer task success rate by\\\\n24.5% -- demonstrating a real-world example of commercially-available AI\\\\nsystems working together with improved outcomes.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.22129v1\\'}, {\\'title\\': \\'AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML\\', \\'authors\\': [\\'Patara Trirat\\', \\'Wonyong Jeong\\', \\'Sung Ju Hwang\\'], \\'published\\': \\'2024-10-03\\', \\'abstract\\': \"Automated machine learning (AutoML) accelerates AI development by automating\\\\ntasks in the development pipeline, such as optimal model search and\\\\nhyperparameter tuning. Existing AutoML systems often require technical\\\\nexpertise to set up complex tools, which is in general time-consuming and\\\\nrequires a large amount of human effort. Therefore, recent works have started\\\\nexploiting large language models (LLM) to lessen such burden and increase the\\\\nusability of AutoML frameworks via a natural language interface, allowing\\\\nnon-expert users to build their data-driven solutions. These methods, however,\\\\nare usually designed only for a particular process in the AI development\\\\npipeline and do not efficiently use the inherent capacity of the LLMs. This\\\\npaper proposes AutoML-Agent, a novel multi-agent framework tailored for\\\\nfull-pipeline AutoML, i.e., from data retrieval to model deployment.\\\\nAutoML-Agent takes user\\'s task descriptions, facilitates collaboration between\\\\nspecialized LLM agents, and delivers deployment-ready models. Unlike existing\\\\nwork, instead of devising a single plan, we introduce a retrieval-augmented\\\\nplanning strategy to enhance exploration to search for more optimal plans. We\\\\nalso decompose each plan into sub-tasks (e.g., data preprocessing and neural\\\\nnetwork design) each of which is solved by a specialized agent we build via\\\\nprompting executing in parallel, making the search process more efficient.\\\\nMoreover, we propose a multi-stage verification to verify executed results and\\\\nguide the code generation LLM in implementing successful solutions. Extensive\\\\nexperiments on seven downstream tasks using fourteen datasets show that\\\\nAutoML-Agent achieves a higher success rate in automating the full AutoML\\\\nprocess, yielding systems with good performance throughout the diverse domains.\", \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.02958v1\\'}, {\\'title\\': \\'Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges\\', \\'authors\\': [\\'Sivan Schwartz\\', \\'Avi Yaeli\\', \\'Segev Shlomov\\'], \\'published\\': \\'2023-08-10\\', \\'abstract\\': \\'Trust in AI agents has been extensively studied in the literature, resulting\\\\nin significant advancements in our understanding of this field. However, the\\\\nrapid advancements in Large Language Models (LLMs) and the emergence of\\\\nLLM-based AI agent frameworks pose new challenges and opportunities for further\\\\nresearch. In the field of process automation, a new generation of AI-based\\\\nagents has emerged, enabling the execution of complex tasks. At the same time,\\\\nthe process of building automation has become more accessible to business users\\\\nvia user-friendly no-code tools and training mechanisms. This paper explores\\\\nthese new challenges and opportunities, analyzes the main aspects of trust in\\\\nAI agents discussed in existing literature, and identifies specific\\\\nconsiderations and challenges relevant to this new generation of automation\\\\nagents. We also evaluate how nascent products in this category address these\\\\nconsiderations. Finally, we highlight several challenges that the research\\\\ncommunity should address in this evolving landscape.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2308.05391v1\\'}, {\\'title\\': \\'AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications\\', \\'authors\\': [\\'Xin Pang\\', \\'Zhucong Li\\', \\'Jiaxiang Chen\\', \\'Yuan Cheng\\', \\'Yinghui Xu\\', \\'Yuan Qi\\'], \\'published\\': \\'2024-04-07\\', \\'abstract\\': \\'We introduce AI2Apps, a Visual Integrated Development Environment (Visual\\\\nIDE) with full-cycle capabilities that accelerates developers to build\\\\ndeployable LLM-based AI agent Applications. This Visual IDE prioritizes both\\\\nthe Integrity of its development tools and the Visuality of its components,\\\\nensuring a smooth and efficient building experience.On one hand, AI2Apps\\\\nintegrates a comprehensive development toolkit ranging from a prototyping\\\\ncanvas and AI-assisted code editor to agent debugger, management system, and\\\\ndeployment tools all within a web-based graphical user interface. On the other\\\\nhand, AI2Apps visualizes reusable front-end and back-end code as intuitive\\\\ndrag-and-drop components. Furthermore, a plugin system named AI2Apps Extension\\\\n(AAE) is designed for Extensibility, showcasing how a new plugin with 20\\\\ncomponents enables web agent to mimic human-like browsing behavior. Our case\\\\nstudy demonstrates substantial efficiency improvements, with AI2Apps reducing\\\\ntoken consumption and API calls when debugging a specific sophisticated\\\\nmultimodal agent by approximately 90% and 80%, respectively. The AI2Apps,\\\\nincluding an online demo, open-source code, and a screencast video, is now\\\\npublicly accessible.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2404.04902v1\\'}]', call_id='call_ZdmwQGTO03X23GeRn6fwDN8q')], type='ToolCallResultMessage'), TextMessage(source='Arxiv_Search_Agent', models_usage=None, content='Tool calls:\\narxiv_search({\"query\":\"no code tools for building multi agent AI systems\",\"max_results\":5}) = [{\\'title\\': \\'AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems\\', \\'authors\\': [\\'Victor Dibia\\', \\'Jingya Chen\\', \\'Gagan Bansal\\', \\'Suff Syed\\', \\'Adam Fourney\\', \\'Erkang Zhu\\', \\'Chi Wang\\', \\'Saleema Amershi\\'], \\'published\\': \\'2024-08-09\\', \\'abstract\\': \\'Multi-agent systems, where multiple agents (generative AI models + tools)\\\\ncollaborate, are emerging as an effective pattern for solving long-running,\\\\ncomplex tasks in numerous domains. However, specifying their parameters (such\\\\nas models, tools, and orchestration mechanisms etc,.) and debugging them\\\\nremains challenging for most developers. To address this challenge, we present\\\\nAUTOGEN STUDIO, a no-code developer tool for rapidly prototyping, debugging,\\\\nand evaluating multi-agent workflows built upon the AUTOGEN framework. AUTOGEN\\\\nSTUDIO offers a web interface and a Python API for representing LLM-enabled\\\\nagents using a declarative (JSON-based) specification. It provides an intuitive\\\\ndrag-and-drop UI for agent workflow specification, interactive evaluation and\\\\ndebugging of workflows, and a gallery of reusable agent components. We\\\\nhighlight four design principles for no-code multi-agent developer tools and\\\\ncontribute an open-source implementation at\\\\nhttps://github.com/microsoft/autogen/tree/main/samples/apps/autogen-studio\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2408.15247v1\\'}, {\\'title\\': \\'Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration\\', \\'authors\\': [\\'Cory Hymel\\', \\'Sida Peng\\', \\'Kevin Xu\\', \\'Charath Ranganathan\\'], \\'published\\': \\'2024-10-29\\', \\'abstract\\': \\'In recent years, with the rapid advancement of large language models (LLMs),\\\\nmulti-agent systems have become increasingly more capable of practical\\\\napplication. At the same time, the software development industry has had a\\\\nnumber of new AI-powered tools developed that improve the software development\\\\nlifecycle (SDLC). Academically, much attention has been paid to the role of\\\\nmulti-agent systems to the SDLC. And, while single-agent systems have\\\\nfrequently been examined in real-world applications, we have seen comparatively\\\\nfew real-world examples of publicly available commercial tools working together\\\\nin a multi-agent system with measurable improvements. In this experiment we\\\\ntest context sharing between Crowdbotics PRD AI, a tool for generating software\\\\nrequirements using AI, and GitHub Copilot, an AI pair-programming tool. By\\\\nsharing business requirements from PRD AI, we improve the code suggestion\\\\ncapabilities of GitHub Copilot by 13.8% and developer task success rate by\\\\n24.5% -- demonstrating a real-world example of commercially-available AI\\\\nsystems working together with improved outcomes.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.22129v1\\'}, {\\'title\\': \\'AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML\\', \\'authors\\': [\\'Patara Trirat\\', \\'Wonyong Jeong\\', \\'Sung Ju Hwang\\'], \\'published\\': \\'2024-10-03\\', \\'abstract\\': \"Automated machine learning (AutoML) accelerates AI development by automating\\\\ntasks in the development pipeline, such as optimal model search and\\\\nhyperparameter tuning. Existing AutoML systems often require technical\\\\nexpertise to set up complex tools, which is in general time-consuming and\\\\nrequires a large amount of human effort. Therefore, recent works have started\\\\nexploiting large language models (LLM) to lessen such burden and increase the\\\\nusability of AutoML frameworks via a natural language interface, allowing\\\\nnon-expert users to build their data-driven solutions. These methods, however,\\\\nare usually designed only for a particular process in the AI development\\\\npipeline and do not efficiently use the inherent capacity of the LLMs. This\\\\npaper proposes AutoML-Agent, a novel multi-agent framework tailored for\\\\nfull-pipeline AutoML, i.e., from data retrieval to model deployment.\\\\nAutoML-Agent takes user\\'s task descriptions, facilitates collaboration between\\\\nspecialized LLM agents, and delivers deployment-ready models. Unlike existing\\\\nwork, instead of devising a single plan, we introduce a retrieval-augmented\\\\nplanning strategy to enhance exploration to search for more optimal plans. We\\\\nalso decompose each plan into sub-tasks (e.g., data preprocessing and neural\\\\nnetwork design) each of which is solved by a specialized agent we build via\\\\nprompting executing in parallel, making the search process more efficient.\\\\nMoreover, we propose a multi-stage verification to verify executed results and\\\\nguide the code generation LLM in implementing successful solutions. Extensive\\\\nexperiments on seven downstream tasks using fourteen datasets show that\\\\nAutoML-Agent achieves a higher success rate in automating the full AutoML\\\\nprocess, yielding systems with good performance throughout the diverse domains.\", \\'pdf_url\\': \\'http://arxiv.org/pdf/2410.02958v1\\'}, {\\'title\\': \\'Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges\\', \\'authors\\': [\\'Sivan Schwartz\\', \\'Avi Yaeli\\', \\'Segev Shlomov\\'], \\'published\\': \\'2023-08-10\\', \\'abstract\\': \\'Trust in AI agents has been extensively studied in the literature, resulting\\\\nin significant advancements in our understanding of this field. However, the\\\\nrapid advancements in Large Language Models (LLMs) and the emergence of\\\\nLLM-based AI agent frameworks pose new challenges and opportunities for further\\\\nresearch. In the field of process automation, a new generation of AI-based\\\\nagents has emerged, enabling the execution of complex tasks. At the same time,\\\\nthe process of building automation has become more accessible to business users\\\\nvia user-friendly no-code tools and training mechanisms. This paper explores\\\\nthese new challenges and opportunities, analyzes the main aspects of trust in\\\\nAI agents discussed in existing literature, and identifies specific\\\\nconsiderations and challenges relevant to this new generation of automation\\\\nagents. We also evaluate how nascent products in this category address these\\\\nconsiderations. Finally, we highlight several challenges that the research\\\\ncommunity should address in this evolving landscape.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2308.05391v1\\'}, {\\'title\\': \\'AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications\\', \\'authors\\': [\\'Xin Pang\\', \\'Zhucong Li\\', \\'Jiaxiang Chen\\', \\'Yuan Cheng\\', \\'Yinghui Xu\\', \\'Yuan Qi\\'], \\'published\\': \\'2024-04-07\\', \\'abstract\\': \\'We introduce AI2Apps, a Visual Integrated Development Environment (Visual\\\\nIDE) with full-cycle capabilities that accelerates developers to build\\\\ndeployable LLM-based AI agent Applications. This Visual IDE prioritizes both\\\\nthe Integrity of its development tools and the Visuality of its components,\\\\nensuring a smooth and efficient building experience.On one hand, AI2Apps\\\\nintegrates a comprehensive development toolkit ranging from a prototyping\\\\ncanvas and AI-assisted code editor to agent debugger, management system, and\\\\ndeployment tools all within a web-based graphical user interface. On the other\\\\nhand, AI2Apps visualizes reusable front-end and back-end code as intuitive\\\\ndrag-and-drop components. Furthermore, a plugin system named AI2Apps Extension\\\\n(AAE) is designed for Extensibility, showcasing how a new plugin with 20\\\\ncomponents enables web agent to mimic human-like browsing behavior. Our case\\\\nstudy demonstrates substantial efficiency improvements, with AI2Apps reducing\\\\ntoken consumption and API calls when debugging a specific sophisticated\\\\nmultimodal agent by approximately 90% and 80%, respectively. The AI2Apps,\\\\nincluding an online demo, open-source code, and a screencast video, is now\\\\npublicly accessible.\\', \\'pdf_url\\': \\'http://arxiv.org/pdf/2404.04902v1\\'}]', type='TextMessage'), TextMessage(source='Report_Agent', models_usage=RequestUsage(prompt_tokens=2381, completion_tokens=1090), content=\"## Literature Review on No-Code Tools for Building Multi-Agent AI Systems\\n\\n### Introduction\\n\\nThe emergence of multi-agent systems (MAS) has transformed various domains by enabling collaboration among multiple agents—ranging from generative AI models to orchestrated tools—to solve complex, long-term tasks. However, the traditional development of these systems often requires substantial technical expertise, making it inaccessible for non-developers. The introduction of no-code platforms aims to shift this paradigm, allowing users without formal programming knowledge to design, debug, and deploy multi-agent systems. This review synthesizes current literature concerning no-code tools developed for building multi-agent AI systems, highlighting recent advancements and emerging trends.\\n\\n### No-Code Development Tools\\n\\n#### AutoGen Studio\\n\\nOne of the prominent no-code tools is **AutoGen Studio**, developed by Dibia et al. (2024). This tool provides a web interface and a declarative specification method utilizing JSON, enabling rapid prototyping, debugging, and evaluating multi-agent workflows. The drag-and-drop capabilities streamline the design process, making complex interactions between agents more manageable. The framework operates on four primary design principles that cater specifically to no-code development, contributing to an accessible pathway for users to harness multi-agent frameworks for various applications (Dibia et al., 2024).\\n\\n#### AI2Apps Visual IDE\\n\\nAnother notable tool is **AI2Apps**, described by Pang et al. (2024). It serves as a Visual Integrated Development Environment that incorporates a comprehensive set of tools from prototyping to deployment. The platform's user-friendly interface allows for the visualization of code through drag-and-drop components, facilitating smoother integration of different agents. An extension system enhances the platform's capabilities, showcasing the potential for customization and scalability in agent application development. The reported efficiency improvements in token consumption and API calls indicate substantial benefits in user-centric design (Pang et al., 2024).\\n\\n### Performance Enhancements in Multi-Agent Configurations\\n\\nHymel et al. (2024) examined the collaborative performance of commercially available AI tools, demonstrating a measurable improvement when integrating multiple agents in a shared configuration. Their experiments showcased how cooperation between tools like Crowdbotics PRD AI and GitHub Copilot significantly improved task success rates, illustrating the practical benefits of employing no-code tools in multi-agent environments. This synergy reflects the critical need for frameworks that inherently support such integrations, especially through no-code mechanisms, to enhance user experience and productivity (Hymel et al., 2024).\\n\\n### Trust and Usability in AI Agents\\n\\nThe concept of trust in AI, particularly in LLM-based automation agents, has gained attention. Schwartz et al. (2023) addressed the challenges and considerations unique to this new generation of agents, highlighting how no-code platforms ease access and usability for non-technical users. The paper emphasizes the need for further research into the trust factors integral to effective multi-agent systems, advocating for a user-centric approach in the design and evaluation of these no-code tools (Schwartz et al., 2023).\\n\\n### Full-Pipeline AutoML with Multi-Agent Systems\\n\\nThe **AutoML-Agent** framework proposed by Trirat et al. (2024) brings another layer of innovation to the no-code landscape. This framework enhances existing automated machine learning processes by using multiple specialized agents that collaboratively manage the full AI development pipeline from data retrieval to model deployment. The novelty lies in its retrieval-augmented planning strategy, which allows for efficient task decomposition and parallel execution, optimizing the overall development experience for non-experts (Trirat et al., 2024).\\n\\n### Conclusion\\n\\nThe literature presents a growing array of no-code tools designed to democratize the development of multi-agent systems. Innovations such as AutoGen Studio, AI2Apps, and collaborative frameworks like AutoML-Agent highlight a trend towards user-centric, efficient design that encourages participation beyond technical boundaries. Future research should continue to explore aspects of trust, usability, and integration to further refine these tools and expand their applicability across various domains.\\n\\n### References\\n\\n- Dibia, V., Chen, J., Bansal, G., Syed, S., Fourney, A., Zhu, E., Wang, C., & Amershi, S. (2024). AutoGen Studio: A No-Code Developer Tool for Building and Debugging Multi-Agent Systems. *arXiv:2408.15247*.\\n- Hymel, C., Peng, S., Xu, K., & Ranganathan, C. (2024). Improving Performance of Commercially Available AI Products in a Multi-Agent Configuration. *arXiv:2410.22129*.\\n- Pang, X., Li, Z., Chen, J., Cheng, Y., Xu, Y., & Qi, Y. (2024). AI2Apps: A Visual IDE for Building LLM-based AI Agent Applications. *arXiv:2404.04902*.\\n- Schwartz, S., Yaeli, A., & Shlomov, S. (2023). Enhancing Trust in LLM-Based AI Automation Agents: New Considerations and Future Challenges. *arXiv:2308.05391*.\\n- Trirat, P., Jeong, W., & Hwang, S. J. (2024). AutoML-Agent: A Multi-Agent LLM Framework for Full-Pipeline AutoML. *arXiv:2410.02958*.\\n\\nTERMINATE\", type='TextMessage')], stop_reason=\"Text 'TERMINATE' mentioned\")" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "await Console(\n", - " team.run_stream(\n", - " task=\"Write a literature review on no code tools for building multi agent ai systems\",\n", - " )\n", - ")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/travel-planning.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/travel-planning.ipynb index beb4ceac6ee3..5a099b1d8a7a 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/travel-planning.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/examples/travel-planning.ipynb @@ -64,7 +64,7 @@ " \"travel_summary_agent\",\n", " model_client=OpenAIChatCompletionClient(model=\"gpt-4o\"),\n", " description=\"A helpful assistant that can summarize the travel plan.\",\n", - " system_message=\"You are a helpful assistant that can take in all of the suggestions and advice from the other agents and provide a detailed tfinal travel plan. You must ensure th b at the final plan is integrated and complete. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN. When the plan is complete and all perspectives are integrated, you can respond with TERMINATE.\",\n", + " system_message=\"You are a helpful assistant that can take in all of the suggestions and advice from the other agents and provide a detailed final travel plan. You must ensure that the final plan is integrated and complete. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN. When the plan is complete and all perspectives are integrated, you can respond with TERMINATE.\",\n", ")" ] }, diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/index.md b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/index.md index 02a0b95178ae..d5df491a69a8 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/index.md +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/index.md @@ -14,8 +14,7 @@ For advanced users, [`autogen-core`](../core-user-guide/index.md)'s event-driven programming model provides more flexibility and control over the underlying components. AgentChat provides intuitive defaults, such as **Agents** with preset -behaviors and **Teams** with predefined [multi-agent design patterns](../core-user-guide/design-patterns/index.md). - +behaviors and **Teams** with predefined [multi-agent design patterns](../core-user-guide/design-patterns/intro.md). ::::{grid} 2 2 2 2 :gutter: 3 @@ -51,6 +50,29 @@ Sample code and use cases installation quickstart -tutorial/index +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Tutorial + +tutorial/models +tutorial/messages +tutorial/agents +tutorial/teams +tutorial/selector-group-chat +tutorial/swarm +tutorial/termination +tutorial/custom-agents +tutorial/state +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: More + examples/index ``` + diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/quickstart.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/quickstart.ipynb index e9145a13ee3f..c02561d3e332 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/quickstart.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/quickstart.ipynb @@ -124,7 +124,7 @@ "source": [ "## What's Next?\n", "\n", - "Now that you have a basic understanding of how to define an agent and a team, consider following the [tutorial](./tutorial/index) for a walkthrough on other features of AgentChat.\n", + "Now that you have a basic understanding of how to define an agent and a team, consider following the [tutorial](./tutorial/models) for a walkthrough on other features of AgentChat.\n", "\n" ] } @@ -145,7 +145,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.6" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/agents.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/agents.ipynb index 90f3f169ca61..7f5edd8e4d03 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/agents.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/agents.ipynb @@ -12,7 +12,7 @@ "- {py:attr}`~autogen_agentchat.agents.BaseChatAgent.name`: The unique name of the agent.\n", "- {py:attr}`~autogen_agentchat.agents.BaseChatAgent.description`: The description of the agent in text.\n", "- {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages`: Send the agent a sequence of {py:class}`~autogen_agentchat.messages.ChatMessage` get a {py:class}`~autogen_agentchat.base.Response`.\n", - "- {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages_stream`: Same as {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages` but returns an iterator of {py:class}`~autogen_agentchat.messages.AgentMessage` followed by a {py:class}`~autogen_agentchat.base.Response` as the last item.\n", + "- {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages_stream`: Same as {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages` but returns an iterator of {py:class}`~autogen_agentchat.messages.AgentEvent` or {py:class}`~autogen_agentchat.messages.ChatMessage` followed by a {py:class}`~autogen_agentchat.base.Response` as the last item.\n", "- {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_reset`: Reset the agent to its initial state.\n", "\n", "See {py:mod}`autogen_agentchat.messages` for more information on AgentChat message types.\n", @@ -74,7 +74,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "[ToolCallMessage(source='assistant', models_usage=RequestUsage(prompt_tokens=61, completion_tokens=15), content=[FunctionCall(id='call_hqVC7UJUPhKaiJwgVKkg66ak', arguments='{\"query\":\"AutoGen\"}', name='web_search')]), ToolCallResultMessage(source='assistant', models_usage=None, content=[FunctionExecutionResult(content='AutoGen is a programming framework for building multi-agent applications.', call_id='call_hqVC7UJUPhKaiJwgVKkg66ak')])]\n", + "[ToolCallRequestEvent(source='assistant', models_usage=RequestUsage(prompt_tokens=61, completion_tokens=15), content=[FunctionCall(id='call_hqVC7UJUPhKaiJwgVKkg66ak', arguments='{\"query\":\"AutoGen\"}', name='web_search')]), ToolCallExecutionEvent(source='assistant', models_usage=None, content=[FunctionExecutionResult(content='AutoGen is a programming framework for building multi-agent applications.', call_id='call_hqVC7UJUPhKaiJwgVKkg66ak')])]\n", "source='assistant' models_usage=RequestUsage(prompt_tokens=92, completion_tokens=14) content='AutoGen is a programming framework designed for building multi-agent applications.'\n" ] } @@ -103,6 +103,13 @@ "as well as a list of inner messages in the {py:attr}`~autogen_agentchat.base.Response.inner_messages` attribute,\n", "which stores the agent's \"thought process\" that led to the final response.\n", "\n", + "```{note}\n", + "It is important to note that {py:meth}`~autogen_agentchat.agents.AssistantAgent.on_messages`\n", + "will update the internal state of the agent -- it will add the messages to the agent's\n", + "history. So you should not repeatedly call this method with the same messages if you want to\n", + "carry on a conversation with the agent.\n", + "```\n", + "\n", "## User Proxy Agent\n", "\n", "{py:class}`~autogen_agentchat.agents.UserProxyAgent` is a built-in agent that\n", diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/custom-agents.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/custom-agents.ipynb index a3282aaf1e95..cd26728f8f54 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/custom-agents.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/custom-agents.ipynb @@ -1,166 +1,313 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Custom Agents\n", - "\n", - "You may have agents with behaviors that do not fall into a preset. \n", - "In such cases, you can build custom agents.\n", - "\n", - "All agents in AgentChat inherit from {py:class}`~autogen_agentchat.agents.BaseChatAgent` \n", - "class and implement the following abstract methods and attributes:\n", - "\n", - "- {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages`: The abstract method that defines the behavior of the agent in response to messages. This method is called when the agent is asked to provide a response in {py:meth}`~autogen_agentchat.agents.BaseChatAgent.run`. It returns a {py:class}`~autogen_agentchat.base.Response` object.\n", - "- {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_reset`: The abstract method that resets the agent to its initial state. This method is called when the agent is asked to reset itself.\n", - "- {py:attr}`~autogen_agentchat.agents.BaseChatAgent.produced_message_types`: The list of possible {py:class}`~autogen_agentchat.messages.ChatMessage` message types the agent can produce in its response.\n", - "\n", - "Optionally, you can implement the the {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages_stream` method to stream messages as they are generated by the agent. If this method is not implemented, the agent\n", - "uses the default implementation of {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages_stream`\n", - "that calls the {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages` method and\n", - "yields all messages in the response." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## CountDownAgent\n", - "\n", - "In this example, we create a simple agent that counts down from a given number to zero,\n", - "and produces a stream of messages with the current count." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import AsyncGenerator, List, Sequence\n", - "\n", - "from autogen_agentchat.agents import BaseChatAgent\n", - "from autogen_agentchat.base import Response\n", - "from autogen_agentchat.messages import AgentMessage, ChatMessage, TextMessage\n", - "from autogen_core import CancellationToken\n", - "\n", - "\n", - "class CountDownAgent(BaseChatAgent):\n", - " def __init__(self, name: str, count: int = 3):\n", - " super().__init__(name, \"A simple agent that counts down.\")\n", - " self._count = count\n", - "\n", - " @property\n", - " def produced_message_types(self) -> List[type[ChatMessage]]:\n", - " return [TextMessage]\n", - "\n", - " async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:\n", - " # Calls the on_messages_stream.\n", - " response: Response | None = None\n", - " async for message in self.on_messages_stream(messages, cancellation_token):\n", - " if isinstance(message, Response):\n", - " response = message\n", - " assert response is not None\n", - " return response\n", - "\n", - " async def on_messages_stream(\n", - " self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken\n", - " ) -> AsyncGenerator[AgentMessage | Response, None]:\n", - " inner_messages: List[AgentMessage] = []\n", - " for i in range(self._count, 0, -1):\n", - " msg = TextMessage(content=f\"{i}...\", source=self.name)\n", - " inner_messages.append(msg)\n", - " yield msg\n", - " # The response is returned at the end of the stream.\n", - " # It contains the final message and all the inner messages.\n", - " yield Response(chat_message=TextMessage(content=\"Done!\", source=self.name), inner_messages=inner_messages)\n", - "\n", - " async def on_reset(self, cancellation_token: CancellationToken) -> None:\n", - " pass\n", - "\n", - "\n", - "async def run_countdown_agent() -> None:\n", - " # Create a countdown agent.\n", - " countdown_agent = CountDownAgent(\"countdown\")\n", - "\n", - " # Run the agent with a given task and stream the response.\n", - " async for message in countdown_agent.on_messages_stream([], CancellationToken()):\n", - " if isinstance(message, Response):\n", - " print(message.chat_message.content)\n", - " else:\n", - " print(message.content)\n", - "\n", - "\n", - "# Use asyncio.run(run_countdown_agent()) when running in a script.\n", - "await run_countdown_agent()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## UserProxyAgent \n", - "\n", - "A common use case for building a custom agent is to create an agent that acts as a proxy for the user.\n", - "\n", - "In the example below we show how to implement a `UserProxyAgent` - an agent that asks the user to enter\n", - "some text through console and then returns that message as a response." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import asyncio\n", - "from typing import List, Sequence\n", - "\n", - "from autogen_agentchat.agents import BaseChatAgent\n", - "from autogen_agentchat.base import Response\n", - "from autogen_agentchat.messages import ChatMessage\n", - "from autogen_core import CancellationToken\n", - "\n", - "\n", - "class UserProxyAgent(BaseChatAgent):\n", - " def __init__(self, name: str) -> None:\n", - " super().__init__(name, \"A human user.\")\n", - "\n", - " @property\n", - " def produced_message_types(self) -> List[type[ChatMessage]]:\n", - " return [TextMessage]\n", - "\n", - " async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:\n", - " user_input = await asyncio.get_event_loop().run_in_executor(None, input, \"Enter your response: \")\n", - " return Response(chat_message=TextMessage(content=user_input, source=self.name))\n", - "\n", - " async def on_reset(self, cancellation_token: CancellationToken) -> None:\n", - " pass\n", - "\n", - "\n", - "async def run_user_proxy_agent() -> None:\n", - " user_proxy_agent = UserProxyAgent(name=\"user_proxy_agent\")\n", - " response = await user_proxy_agent.on_messages([], CancellationToken())\n", - " print(response.chat_message.content)\n", - "\n", - "\n", - "# Use asyncio.run(run_user_proxy_agent()) when running in a script.\n", - "await run_user_proxy_agent()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Custom Agents\n", + "\n", + "You may have agents with behaviors that do not fall into a preset. \n", + "In such cases, you can build custom agents.\n", + "\n", + "All agents in AgentChat inherit from {py:class}`~autogen_agentchat.agents.BaseChatAgent` \n", + "class and implement the following abstract methods and attributes:\n", + "\n", + "- {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages`: The abstract method that defines the behavior of the agent in response to messages. This method is called when the agent is asked to provide a response in {py:meth}`~autogen_agentchat.agents.BaseChatAgent.run`. It returns a {py:class}`~autogen_agentchat.base.Response` object.\n", + "- {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_reset`: The abstract method that resets the agent to its initial state. This method is called when the agent is asked to reset itself.\n", + "- {py:attr}`~autogen_agentchat.agents.BaseChatAgent.produced_message_types`: The list of possible {py:class}`~autogen_agentchat.messages.ChatMessage` message types the agent can produce in its response.\n", + "\n", + "Optionally, you can implement the the {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages_stream` method to stream messages as they are generated by the agent. If this method is not implemented, the agent\n", + "uses the default implementation of {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages_stream`\n", + "that calls the {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages` method and\n", + "yields all messages in the response." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CountDownAgent\n", + "\n", + "In this example, we create a simple agent that counts down from a given number to zero,\n", + "and produces a stream of messages with the current count." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3...\n", + "2...\n", + "1...\n", + "Done!\n" + ] + } + ], + "source": [ + "from typing import AsyncGenerator, List, Sequence\n", + "\n", + "from autogen_agentchat.agents import BaseChatAgent\n", + "from autogen_agentchat.base import Response\n", + "from autogen_agentchat.messages import AgentEvent, ChatMessage, TextMessage\n", + "from autogen_core import CancellationToken\n", + "\n", + "\n", + "class CountDownAgent(BaseChatAgent):\n", + " def __init__(self, name: str, count: int = 3):\n", + " super().__init__(name, \"A simple agent that counts down.\")\n", + " self._count = count\n", + "\n", + " @property\n", + " def produced_message_types(self) -> List[type[ChatMessage]]:\n", + " return [TextMessage]\n", + "\n", + " async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:\n", + " # Calls the on_messages_stream.\n", + " response: Response | None = None\n", + " async for message in self.on_messages_stream(messages, cancellation_token):\n", + " if isinstance(message, Response):\n", + " response = message\n", + " assert response is not None\n", + " return response\n", + "\n", + " async def on_messages_stream(\n", + " self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken\n", + " ) -> AsyncGenerator[AgentEvent | ChatMessage | Response, None]:\n", + " inner_messages: List[AgentEvent | ChatMessage] = []\n", + " for i in range(self._count, 0, -1):\n", + " msg = TextMessage(content=f\"{i}...\", source=self.name)\n", + " inner_messages.append(msg)\n", + " yield msg\n", + " # The response is returned at the end of the stream.\n", + " # It contains the final message and all the inner messages.\n", + " yield Response(chat_message=TextMessage(content=\"Done!\", source=self.name), inner_messages=inner_messages)\n", + "\n", + " async def on_reset(self, cancellation_token: CancellationToken) -> None:\n", + " pass\n", + "\n", + "\n", + "async def run_countdown_agent() -> None:\n", + " # Create a countdown agent.\n", + " countdown_agent = CountDownAgent(\"countdown\")\n", + "\n", + " # Run the agent with a given task and stream the response.\n", + " async for message in countdown_agent.on_messages_stream([], CancellationToken()):\n", + " if isinstance(message, Response):\n", + " print(message.chat_message.content)\n", + " else:\n", + " print(message.content)\n", + "\n", + "\n", + "# Use asyncio.run(run_countdown_agent()) when running in a script.\n", + "await run_countdown_agent()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## ArithmeticAgent\n", + "\n", + "In this example, we create an agent class that can perform simple arithmetic operations\n", + "on a given integer. Then, we will use different instances of this agent class\n", + "in a {py:class}`~autogen_agentchat.teams.SelectorGroupChat`\n", + "to transform a given integer into another integer by applying a sequence of arithmetic operations.\n", + "\n", + "The `ArithmeticAgent` class takes an `operator_func` that takes an integer and returns an integer,\n", + "after applying an arithmetic operation to the integer.\n", + "In its `on_messages` method, it applies the `operator_func` to the integer in the input message,\n", + "and returns a response with the result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Callable, List, Sequence\n", + "\n", + "from autogen_agentchat.agents import BaseChatAgent\n", + "from autogen_agentchat.base import Response\n", + "from autogen_agentchat.conditions import MaxMessageTermination\n", + "from autogen_agentchat.messages import ChatMessage\n", + "from autogen_agentchat.teams import SelectorGroupChat\n", + "from autogen_agentchat.ui import Console\n", + "from autogen_core import CancellationToken\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", + "\n", + "\n", + "class ArithmeticAgent(BaseChatAgent):\n", + " def __init__(self, name: str, description: str, operator_func: Callable[[int], int]) -> None:\n", + " super().__init__(name, description=description)\n", + " self._operator_func = operator_func\n", + " self._message_history: List[ChatMessage] = []\n", + "\n", + " @property\n", + " def produced_message_types(self) -> List[type[ChatMessage]]:\n", + " return [TextMessage]\n", + "\n", + " async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:\n", + " # Update the message history.\n", + " # NOTE: it is possible the messages is an empty list, which means the agent was selected previously.\n", + " self._message_history.extend(messages)\n", + " # Parse the number in the last message.\n", + " assert isinstance(self._message_history[-1], TextMessage)\n", + " number = int(self._message_history[-1].content)\n", + " # Apply the operator function to the number.\n", + " result = self._operator_func(number)\n", + " # Create a new message with the result.\n", + " response_message = TextMessage(content=str(result), source=self.name)\n", + " # Update the message history.\n", + " self._message_history.append(response_message)\n", + " # Return the response.\n", + " return Response(chat_message=response_message)\n", + "\n", + " async def on_reset(self, cancellation_token: CancellationToken) -> None:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{note}\n", + "The `on_messages` method may be called with an empty list of messages, in which\n", + "case it means the agent was called previously and is now being called again,\n", + "without any new messages from the caller. So it is important to keep a history\n", + "of the previous messages received by the agent, and use that history to generate\n", + "the response.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can create a {py:class}`~autogen_agentchat.teams.SelectorGroupChat` with 5 instances of `ArithmeticAgent`:\n", + "\n", + "- one that adds 1 to the input integer,\n", + "- one that subtracts 1 from the input integer,\n", + "- one that multiplies the input integer by 2,\n", + "- one that divides the input integer by 2 and rounds down to the nearest integer, and\n", + "- one that returns the input integer unchanged.\n", + "\n", + "We then create a {py:class}`~autogen_agentchat.teams.SelectorGroupChat` with these agents,\n", + "and set the appropriate selector settings:\n", + "\n", + "- allow the same agent to be selected consecutively to allow for repeated operations, and\n", + "- customize the selector prompt to tailor the model's response to the specific task." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Apply the operations to turn the given number into 25.\n", + "---------- user ----------\n", + "10\n", + "---------- multiply_agent ----------\n", + "20\n", + "---------- add_agent ----------\n", + "21\n", + "---------- multiply_agent ----------\n", + "42\n", + "---------- divide_agent ----------\n", + "21\n", + "---------- add_agent ----------\n", + "22\n", + "---------- add_agent ----------\n", + "23\n", + "---------- add_agent ----------\n", + "24\n", + "---------- add_agent ----------\n", + "25\n", + "---------- Summary ----------\n", + "Number of messages: 10\n", + "Finish reason: Maximum number of messages 10 reached, current message count: 10\n", + "Total prompt tokens: 0\n", + "Total completion tokens: 0\n", + "Duration: 2.40 seconds\n" + ] + } + ], + "source": [ + "async def run_number_agents() -> None:\n", + " # Create agents for number operations.\n", + " add_agent = ArithmeticAgent(\"add_agent\", \"Adds 1 to the number.\", lambda x: x + 1)\n", + " multiply_agent = ArithmeticAgent(\"multiply_agent\", \"Multiplies the number by 2.\", lambda x: x * 2)\n", + " subtract_agent = ArithmeticAgent(\"subtract_agent\", \"Subtracts 1 from the number.\", lambda x: x - 1)\n", + " divide_agent = ArithmeticAgent(\"divide_agent\", \"Divides the number by 2 and rounds down.\", lambda x: x // 2)\n", + " identity_agent = ArithmeticAgent(\"identity_agent\", \"Returns the number as is.\", lambda x: x)\n", + "\n", + " # The termination condition is to stop after 10 messages.\n", + " termination_condition = MaxMessageTermination(10)\n", + "\n", + " # Create a selector group chat.\n", + " selector_group_chat = SelectorGroupChat(\n", + " [add_agent, multiply_agent, subtract_agent, divide_agent, identity_agent],\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o\"),\n", + " termination_condition=termination_condition,\n", + " allow_repeated_speaker=True, # Allow the same agent to speak multiple times, necessary for this task.\n", + " selector_prompt=(\n", + " \"Available roles:\\n{roles}\\nTheir job descriptions:\\n{participants}\\n\"\n", + " \"Current conversation history:\\n{history}\\n\"\n", + " \"Please select the most appropriate role for the next message, and only return the role name.\"\n", + " ),\n", + " )\n", + "\n", + " # Run the selector group chat with a given task and stream the response.\n", + " task: List[ChatMessage] = [\n", + " TextMessage(content=\"Apply the operations to turn the given number into 25.\", source=\"user\"),\n", + " TextMessage(content=\"10\", source=\"user\"),\n", + " ]\n", + " stream = selector_group_chat.run_stream(task=task)\n", + " await Console(stream)\n", + "\n", + "\n", + "# Use asyncio.run(run_number_agents()) when running in a script.\n", + "await run_number_agents()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From the output, we can see that the agents have successfully transformed the input integer\n", + "from 10 to 25 by choosing appropriate agents that apply the arithmetic operations in sequence." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/index.md b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/index.md deleted file mode 100644 index a7c97d9de1bb..000000000000 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/index.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -myst: - html_meta: - "description lang=en": | - Tutorial for AutoGen AgentChat, a framework for building multi-agent applications with AI agents. ---- - -# Tutorial - -Get started with AgentChat through this comprehensive tutorial. - -::::{grid} 2 2 2 3 -:gutter: 3 - -:::{grid-item-card} {fas}`book-open;pst-color-primary` Models -:link: ./models.html - -Setting up model clients for agents and teams. -::: - -:::{grid-item-card} {fas}`message;pst-color-primary` Messages -:link: ./messages.html - -Understanding different types of messages. -::: - -:::{grid-item-card} {fas}`users;pst-color-primary` Agents -:link: ./agents.html - -Building agents that use models, tools, and code executors. -::: - -:::{grid-item-card} {fas}`users;pst-color-primary` Teams Intro -:link: ./teams.html - -Introduction to teams and task termination. -::: - -:::{grid-item-card} {fas}`users;pst-color-primary` Selector Group Chat -:link: ./selector-group-chat.html - -A smart team that uses a model-based strategy and custom selector. -::: - -:::{grid-item-card} {fas}`users;pst-color-primary` Swarm -:link: ./swarm.html - -A dynamic team that uses handoffs to pass tasks between agents. -::: - -:::{grid-item-card} {fas}`users;pst-color-primary` Custom Agents -:link: ./custom-agents.html - -How to build custom agents. -::: - -:::{grid-item-card} {fas}`database;pst-color-primary` State Management -:link: ./state.html - -How to manage state in agents and teams. -::: - -:::: - -```{toctree} -:maxdepth: 1 -:hidden: - -models -messages -agents -teams -selector-group-chat -swarm -termination -custom-agents -state -``` diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/messages.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/messages.ipynb index a7f383212785..25dc78641980 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/messages.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/messages.ipynb @@ -23,7 +23,7 @@ "At a high level, messages in AgentChat can be categorized into two types: agent-agent messages and an agent's internal events and messages.\n", "\n", "### Agent-Agent Messages\n", - "AgentChat supports many message types for agent-to-agent communication. The most common one is the {py:class}`~autogen_agentchat.messages.ChatMessage`. This message type allows both text and multimodal communication and subsumes other message types, such as {py:class}`~autogen_agentchat.messages.TextMessage` or {py:class}`~autogen_agentchat.messages.MultiModalMessage`.\n", + "AgentChat supports many message types for agent-to-agent communication. They belong to the union type {py:class}`~autogen_agentchat.messages.ChatMessage`. This message type allows both text and multimodal communication and subsumes other message types, such as {py:class}`~autogen_agentchat.messages.TextMessage` or {py:class}`~autogen_agentchat.messages.MultiModalMessage`.\n", "\n", "For example, the following code snippet demonstrates how to create a text message, which accepts a string content and a string source:" ] @@ -91,13 +91,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Internal Events and Messages\n", + "### Internal Events\n", "\n", - "AgentChat also supports the concept of `inner_messages` - messages that are internal to an agent. These messages are used to communicate events and information on actions _within_ the agent itself.\n", + "AgentChat also supports the concept of `events` - messages that are internal to an agent. These messages are used to communicate events and information on actions _within_ the agent itself, and belong to the union type {py:class}`~autogen_agentchat.messages.AgentEvent`.\n", "\n", - "Examples of these include {py:class}`~autogen_agentchat.messages.ToolCallMessage`, which indicates that a request was made to call a tool, and {py:class}`~autogen_agentchat.messages.ToolCallResultMessage`, which contains the results of tool calls.\n", + "Examples of these include {py:class}`~autogen_agentchat.messages.ToolCallRequestEvent`, which indicates that a request was made to call a tool, and {py:class}`~autogen_agentchat.messages.ToolCallExecutionEvent`, which contains the results of tool calls.\n", "\n", - "Typically, these messages are created by the agent itself and are contained in the {py:attr}`~autogen_agentchat.base.Response.inner_messages` field of the {py:class}`~autogen_agentchat.base.Response` returned from {py:class}`~autogen_agentchat.base.ChatAgent.on_messages`. If you are building a custom agent and have events that you want to communicate to other entities (e.g., a UI), you can include these in the {py:attr}`~autogen_agentchat.base.Response.inner_messages` field of the {py:class}`~autogen_agentchat.base.Response`. We will show examples of this in [Custom Agents](./custom-agents.ipynb).\n", + "Typically, events are created by the agent itself and are contained in the {py:attr}`~autogen_agentchat.base.Response.inner_messages` field of the {py:class}`~autogen_agentchat.base.Response` returned from {py:class}`~autogen_agentchat.base.ChatAgent.on_messages`. If you are building a custom agent and have events that you want to communicate to other entities (e.g., a UI), you can include these in the {py:attr}`~autogen_agentchat.base.Response.inner_messages` field of the {py:class}`~autogen_agentchat.base.Response`. We will show examples of this in [Custom Agents](./custom-agents.ipynb).\n", "\n", "\n", "You can read about the full set of messages supported in AgentChat in the {py:mod}`~autogen_agentchat.messages` module. \n", diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/models.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/models.ipynb index 0bafc7705971..3020cbe9f836 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/models.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/models.ipynb @@ -15,7 +15,7 @@ "source": [ "## OpenAI\n", "\n", - "To access OpenAI models, install the `openai` extension, which allows you to use the {py:class}`~autogen_ext.models.OpenAIChatCompletionClient`." + "To access OpenAI models, install the `openai` extension, which allows you to use the {py:class}`~autogen_ext.models.openai.OpenAIChatCompletionClient`." ] }, { @@ -85,7 +85,7 @@ "source": [ "```{note}\n", "You can use this client with models hosted on OpenAI-compatible endpoints, however, we have not tested this functionality.\n", - "See {py:class}`~autogen_ext.models.OpenAIChatCompletionClient` for more information.\n", + "See {py:class}`~autogen_ext.models.openai.OpenAIChatCompletionClient` for more information.\n", "```" ] }, @@ -95,7 +95,7 @@ "source": [ "## Azure OpenAI\n", "\n", - "Similarly, install the `azure` and `openai` extensions to use the {py:class}`~autogen_ext.models.AzureOpenAIChatCompletionClient`." + "Similarly, install the `azure` and `openai` extensions to use the {py:class}`~autogen_ext.models.openai.AzureOpenAIChatCompletionClient`." ] }, { @@ -179,7 +179,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.5" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/selector-group-chat.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/selector-group-chat.ipynb index 74110d180040..ae67661dc64d 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/selector-group-chat.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/selector-group-chat.ipynb @@ -1,504 +1,504 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Selector Group Chat" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "{py:class}`~autogen_agentchat.teams.SelectorGroupChat` implements a team where participants take turns broadcasting messages to all other members. A generative model (e.g., an LLM) selects the next speaker based on the shared context, enabling dynamic, context-aware collaboration.\n", - "\n", - "Key features include:\n", - "\n", - "- Model-based speaker selection\n", - "- Configurable participant roles and descriptions\n", - "- Prevention of consecutive turns by the same speaker (optional)\n", - "- Customizable selection prompting\n", - "- Customizable selection function to override the default model-based selection\n", - "\n", - "```{note}\n", - "{py:class}`~autogen_agentchat.teams.SelectorGroupChat` is a high-level API. For more control and customization, refer to the [Group Chat Pattern](../../core-user-guide/design-patterns/group-chat.ipynb) in the Core API documentation to implement your own group chat logic.\n", - "```\n", - "\n", - "## How Does it Work?\n", - "\n", - "{py:class}`~autogen_agentchat.teams.SelectorGroupChat` is a group chat similar to {py:class}`~autogen_agentchat.teams.RoundRobinGroupChat`,\n", - "but with a model-based next speaker selection mechanism.\n", - "When the team receives a task through {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run` or {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run_stream`,\n", - "the following steps are executed:\n", - "\n", - "1. The team analyzes the current conversation context, including the conversation history and participants' {py:attr}`~autogen_agentchat.base.ChatAgent.name` and {py:attr}`~autogen_agentchat.base.ChatAgent.description` attributes, to determine the next speaker using a model. You can override the model by providing a custom selection function.\n", - "2. The team prompts the selected speaker agent to provide a response, which is then **broadcasted** to all other participants.\n", - "3. The termination condition is checked to determine if the conversation should end, if not, the process repeats from step 1.\n", - "4. When the conversation ends, the team returns the {py:class}`~autogen_agentchat.base.TaskResult` containing the conversation history from this task.\n", - "\n", - "Once the team finishes the task, the conversation context is kept within the team and all participants, so the next task can continue from the previous conversation context.\n", - "You can reset the conversation context by calling {py:meth}`~autogen_agentchat.teams.BaseGroupChat.reset`.\n", - "\n", - "In this section, we will demonstrate how to use {py:class}`~autogen_agentchat.teams.SelectorGroupChat` with a simple example for a web search and data analysis task." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example: Web Search/Analysis" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Sequence\n", - "\n", - "from autogen_agentchat.agents import AssistantAgent\n", - "from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination\n", - "from autogen_agentchat.messages import AgentMessage\n", - "from autogen_agentchat.teams import SelectorGroupChat\n", - "from autogen_agentchat.ui import Console\n", - "from autogen_ext.models.openai import OpenAIChatCompletionClient" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Agents\n", - "\n", - "![Selector Group Chat](selector-group-chat.svg)\n", - "\n", - "This system uses three specialized agents:\n", - "\n", - "- **Planning Agent**: The strategic coordinator that breaks down complex tasks into manageable subtasks. \n", - "- **Web Search Agent**: An information retrieval specialist that interfaces with the `search_web_tool`.\n", - "- **Data Analyst Agent**: An agent specialist in performing calculations equipped with `percentage_change_tool`. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The tools `search_web_tool` and `percentage_change_tool` are external tools that the agents can use to perform their tasks." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "# Note: This example uses mock tools instead of real APIs for demonstration purposes\n", - "def search_web_tool(query: str) -> str:\n", - " if \"2006-2007\" in query:\n", - " return \"\"\"Here are the total points scored by Miami Heat players in the 2006-2007 season:\n", - " Udonis Haslem: 844 points\n", - " Dwayne Wade: 1397 points\n", - " James Posey: 550 points\n", - " ...\n", - " \"\"\"\n", - " elif \"2007-2008\" in query:\n", - " return \"The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\"\n", - " elif \"2008-2009\" in query:\n", - " return \"The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.\"\n", - " return \"No data found.\"\n", - "\n", - "\n", - "def percentage_change_tool(start: float, end: float) -> float:\n", - " return ((end - start) / start) * 100" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's create the specialized agents using the {py:class}`~autogen_agentchat.agents.AssistantAgent` class.\n", - "It is important to note that the agents' {py:attr}`~autogen_agentchat.base.ChatAgent.name` and {py:attr}`~autogen_agentchat.base.ChatAgent.description` attributes are used by the model to determine the next speaker,\n", - "so it is recommended to provide meaningful names and descriptions." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "model_client = OpenAIChatCompletionClient(model=\"gpt-4o\")\n", - "\n", - "planning_agent = AssistantAgent(\n", - " \"PlanningAgent\",\n", - " description=\"An agent for planning tasks, this agent should be the first to engage when given a new task.\",\n", - " model_client=model_client,\n", - " system_message=\"\"\"\n", - " You are a planning agent.\n", - " Your job is to break down complex tasks into smaller, manageable subtasks.\n", - " Your team members are:\n", - " Web search agent: Searches for information\n", - " Data analyst: Performs calculations\n", - "\n", - " You only plan and delegate tasks - you do not execute them yourself.\n", - "\n", - " When assigning tasks, use this format:\n", - " 1. : \n", - "\n", - " After all tasks are complete, summarize the findings and end with \"TERMINATE\".\n", - " \"\"\",\n", - ")\n", - "\n", - "web_search_agent = AssistantAgent(\n", - " \"WebSearchAgent\",\n", - " description=\"A web search agent.\",\n", - " tools=[search_web_tool],\n", - " model_client=model_client,\n", - " system_message=\"\"\"\n", - " You are a web search agent.\n", - " Your only tool is search_tool - use it to find information.\n", - " You make only one search call at a time.\n", - " Once you have the results, you never do calculations based on them.\n", - " \"\"\",\n", - ")\n", - "\n", - "data_analyst_agent = AssistantAgent(\n", - " \"DataAnalystAgent\",\n", - " description=\"A data analyst agent. Useful for performing calculations.\",\n", - " model_client=model_client,\n", - " tools=[percentage_change_tool],\n", - " system_message=\"\"\"\n", - " You are a data analyst.\n", - " Given the tasks you have been assigned, you should analyze the data and provide results using the tools provided.\n", - " \"\"\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Workflow\n", - "\n", - "1. The task is received by the {py:class}`~autogen_agentchat.teams.SelectorGroupChat` which, based on agent descriptions, selects the most appropriate agent to handle the initial task (typically the Planning Agent).\n", - "\n", - "2. The **Planning Agent** analyzes the task and breaks it down into subtasks, assigning each to the most appropriate agent using the format:\n", - " ` : `\n", - "\n", - "3. Based on the conversation context and agent descriptions, the {py:class}`~autogen_agent.teams.SelectorGroupChat` manager dynamically selects the next agent to handle their assigned subtask.\n", - "\n", - "4. The **Web Search Agent** performs searches one at a time, storing results in the shared conversation history.\n", - "\n", - "5. The **Data Analyst** processes the gathered information using available calculation tools when selected.\n", - "\n", - "6. The workflow continues with agents being dynamically selected until either:\n", - " - The Planning Agent determines all subtasks are complete and sends \"TERMINATE\"\n", - " - An alternative termination condition is met (e.g., a maximum number of messages)\n", - "\n", - "When defining your agents, make sure to include a helpful {py:attr}`~autogen_agentchat.base.ChatAgent.description` since this is used to decide which agent to select next." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's create the team with two termination conditions:\n", - "{py:class}`~autogen_agentchat.conditions.TextMentionTermination` to end the conversation when the Planning Agent sends \"TERMINATE\",\n", - "and {py:class}`~autogen_agentchat.conditions.MaxMessageTermination` to limit the conversation to 25 messages to avoid infinite loop." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "text_mention_termination = TextMentionTermination(\"TERMINATE\")\n", - "max_messages_termination = MaxMessageTermination(max_messages=25)\n", - "termination = text_mention_termination | max_messages_termination\n", - "\n", - "team = SelectorGroupChat(\n", - " [planning_agent, web_search_agent, data_analyst_agent],\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", - " termination_condition=termination,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we run the team with a task to find information about an NBA player." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?\n", - "---------- PlanningAgent ----------\n", - "To address this request, we will divide the task into manageable subtasks. \n", - "\n", - "1. Web search agent: Identify the Miami Heat player with the highest points in the 2006-2007 season.\n", - "2. Web search agent: Gather the total rebounds for the identified player during the 2007-2008 season.\n", - "3. Web search agent: Gather the total rebounds for the identified player during the 2008-2009 season.\n", - "4. Data analyst: Calculate the percentage change in total rebounds for the identified player between the 2007-2008 and 2008-2009 seasons.\n", - "[Prompt tokens: 159, Completion tokens: 122]\n", - "---------- WebSearchAgent ----------\n", - "[FunctionCall(id='call_xdYlGP2lsqDeWdSiOlwOBNiO', arguments='{\"query\":\"Miami Heat highest points player 2006-2007 season\"}', name='search_web_tool')]\n", - "[Prompt tokens: 271, Completion tokens: 26]\n", - "---------- WebSearchAgent ----------\n", - "[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', call_id='call_xdYlGP2lsqDeWdSiOlwOBNiO')]\n", - "---------- WebSearchAgent ----------\n", - "Tool calls:\n", - "search_web_tool({\"query\":\"Miami Heat highest points player 2006-2007 season\"}) = Here are the total points scored by Miami Heat players in the 2006-2007 season:\n", - " Udonis Haslem: 844 points\n", - " Dwayne Wade: 1397 points\n", - " James Posey: 550 points\n", - " ...\n", - " \n", - "---------- DataAnalystAgent ----------\n", - "[FunctionCall(id='call_asjxKtAVGfqrYl0jMpvwCrvV', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='WebSearchAgent'), FunctionCall(id='call_8L91Kizt0KU6RNwUgvNx7S0s', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='WebSearchAgent')]\n", - "[Prompt tokens: 345, Completion tokens: 68]\n", - "---------- DataAnalystAgent ----------\n", - "[FunctionExecutionResult(content=\"Error: The tool 'WebSearchAgent' is not available.\", call_id='call_asjxKtAVGfqrYl0jMpvwCrvV'), FunctionExecutionResult(content=\"Error: The tool 'WebSearchAgent' is not available.\", call_id='call_8L91Kizt0KU6RNwUgvNx7S0s')]\n", - "---------- DataAnalystAgent ----------\n", - "Tool calls:\n", - "WebSearchAgent({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = Error: The tool 'WebSearchAgent' is not available.\n", - "WebSearchAgent({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = Error: The tool 'WebSearchAgent' is not available.\n", - "---------- WebSearchAgent ----------\n", - "[FunctionCall(id='call_imvRJ2jhpPdovBbx8MFjlFVS', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_U30KVmFG1aeXPbqGJjDmJ6iJ', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')]\n", - "[Prompt tokens: 445, Completion tokens: 70]\n", - "---------- WebSearchAgent ----------\n", - "[FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.', call_id='call_imvRJ2jhpPdovBbx8MFjlFVS'), FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', call_id='call_U30KVmFG1aeXPbqGJjDmJ6iJ')]\n", - "---------- WebSearchAgent ----------\n", - "Tool calls:\n", - "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\n", - "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.\n", - "---------- DataAnalystAgent ----------\n", - "[FunctionCall(id='call_CtAnvcbitN0JiwBfiLVzb5Do', arguments='{\"start\":214,\"end\":398}', name='percentage_change_tool')]\n", - "[Prompt tokens: 562, Completion tokens: 20]\n", - "---------- DataAnalystAgent ----------\n", - "[FunctionExecutionResult(content='85.98130841121495', call_id='call_CtAnvcbitN0JiwBfiLVzb5Do')]\n", - "---------- DataAnalystAgent ----------\n", - "Tool calls:\n", - "percentage_change_tool({\"start\":214,\"end\":398}) = 85.98130841121495\n", - "---------- PlanningAgent ----------\n", - "Summary of Findings:\n", - "\n", - "1. Dwyane Wade was the Miami Heat player with the highest points in the 2006-2007 season, scoring a total of 1,397 points.\n", - "2. Dwyane Wade's total rebounds during the 2007-2008 season were 214.\n", - "3. Dwyane Wade's total rebounds during the 2008-2009 season were 398.\n", - "4. The percentage change in Dwyane Wade's total rebounds between the 2007-2008 and 2008-2009 seasons was approximately 85.98%.\n", - "\n", - "TERMINATE\n", - "[Prompt tokens: 590, Completion tokens: 122]\n", - "---------- Summary ----------\n", - "Number of messages: 15\n", - "Finish reason: Text 'TERMINATE' mentioned\n", - "Total prompt tokens: 2372\n", - "Total completion tokens: 428\n", - "Duration: 9.21 seconds\n" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Selector Group Chat" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "{py:class}`~autogen_agentchat.teams.SelectorGroupChat` implements a team where participants take turns broadcasting messages to all other members. A generative model (e.g., an LLM) selects the next speaker based on the shared context, enabling dynamic, context-aware collaboration.\n", + "\n", + "Key features include:\n", + "\n", + "- Model-based speaker selection\n", + "- Configurable participant roles and descriptions\n", + "- Prevention of consecutive turns by the same speaker (optional)\n", + "- Customizable selection prompting\n", + "- Customizable selection function to override the default model-based selection\n", + "\n", + "```{note}\n", + "{py:class}`~autogen_agentchat.teams.SelectorGroupChat` is a high-level API. For more control and customization, refer to the [Group Chat Pattern](../../core-user-guide/design-patterns/group-chat.ipynb) in the Core API documentation to implement your own group chat logic.\n", + "```\n", + "\n", + "## How Does it Work?\n", + "\n", + "{py:class}`~autogen_agentchat.teams.SelectorGroupChat` is a group chat similar to {py:class}`~autogen_agentchat.teams.RoundRobinGroupChat`,\n", + "but with a model-based next speaker selection mechanism.\n", + "When the team receives a task through {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run` or {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run_stream`,\n", + "the following steps are executed:\n", + "\n", + "1. The team analyzes the current conversation context, including the conversation history and participants' {py:attr}`~autogen_agentchat.base.ChatAgent.name` and {py:attr}`~autogen_agentchat.base.ChatAgent.description` attributes, to determine the next speaker using a model. You can override the model by providing a custom selection function.\n", + "2. The team prompts the selected speaker agent to provide a response, which is then **broadcasted** to all other participants.\n", + "3. The termination condition is checked to determine if the conversation should end, if not, the process repeats from step 1.\n", + "4. When the conversation ends, the team returns the {py:class}`~autogen_agentchat.base.TaskResult` containing the conversation history from this task.\n", + "\n", + "Once the team finishes the task, the conversation context is kept within the team and all participants, so the next task can continue from the previous conversation context.\n", + "You can reset the conversation context by calling {py:meth}`~autogen_agentchat.teams.BaseGroupChat.reset`.\n", + "\n", + "In this section, we will demonstrate how to use {py:class}`~autogen_agentchat.teams.SelectorGroupChat` with a simple example for a web search and data analysis task." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example: Web Search/Analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Sequence\n", + "\n", + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination\n", + "from autogen_agentchat.messages import AgentEvent, ChatMessage\n", + "from autogen_agentchat.teams import SelectorGroupChat\n", + "from autogen_agentchat.ui import Console\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Agents\n", + "\n", + "![Selector Group Chat](selector-group-chat.svg)\n", + "\n", + "This system uses three specialized agents:\n", + "\n", + "- **Planning Agent**: The strategic coordinator that breaks down complex tasks into manageable subtasks. \n", + "- **Web Search Agent**: An information retrieval specialist that interfaces with the `search_web_tool`.\n", + "- **Data Analyst Agent**: An agent specialist in performing calculations equipped with `percentage_change_tool`. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The tools `search_web_tool` and `percentage_change_tool` are external tools that the agents can use to perform their tasks." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Note: This example uses mock tools instead of real APIs for demonstration purposes\n", + "def search_web_tool(query: str) -> str:\n", + " if \"2006-2007\" in query:\n", + " return \"\"\"Here are the total points scored by Miami Heat players in the 2006-2007 season:\n", + " Udonis Haslem: 844 points\n", + " Dwayne Wade: 1397 points\n", + " James Posey: 550 points\n", + " ...\n", + " \"\"\"\n", + " elif \"2007-2008\" in query:\n", + " return \"The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\"\n", + " elif \"2008-2009\" in query:\n", + " return \"The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.\"\n", + " return \"No data found.\"\n", + "\n", + "\n", + "def percentage_change_tool(start: float, end: float) -> float:\n", + " return ((end - start) / start) * 100" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create the specialized agents using the {py:class}`~autogen_agentchat.agents.AssistantAgent` class.\n", + "It is important to note that the agents' {py:attr}`~autogen_agentchat.base.ChatAgent.name` and {py:attr}`~autogen_agentchat.base.ChatAgent.description` attributes are used by the model to determine the next speaker,\n", + "so it is recommended to provide meaningful names and descriptions." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "model_client = OpenAIChatCompletionClient(model=\"gpt-4o\")\n", + "\n", + "planning_agent = AssistantAgent(\n", + " \"PlanningAgent\",\n", + " description=\"An agent for planning tasks, this agent should be the first to engage when given a new task.\",\n", + " model_client=model_client,\n", + " system_message=\"\"\"\n", + " You are a planning agent.\n", + " Your job is to break down complex tasks into smaller, manageable subtasks.\n", + " Your team members are:\n", + " Web search agent: Searches for information\n", + " Data analyst: Performs calculations\n", + "\n", + " You only plan and delegate tasks - you do not execute them yourself.\n", + "\n", + " When assigning tasks, use this format:\n", + " 1. : \n", + "\n", + " After all tasks are complete, summarize the findings and end with \"TERMINATE\".\n", + " \"\"\",\n", + ")\n", + "\n", + "web_search_agent = AssistantAgent(\n", + " \"WebSearchAgent\",\n", + " description=\"A web search agent.\",\n", + " tools=[search_web_tool],\n", + " model_client=model_client,\n", + " system_message=\"\"\"\n", + " You are a web search agent.\n", + " Your only tool is search_tool - use it to find information.\n", + " You make only one search call at a time.\n", + " Once you have the results, you never do calculations based on them.\n", + " \"\"\",\n", + ")\n", + "\n", + "data_analyst_agent = AssistantAgent(\n", + " \"DataAnalystAgent\",\n", + " description=\"A data analyst agent. Useful for performing calculations.\",\n", + " model_client=model_client,\n", + " tools=[percentage_change_tool],\n", + " system_message=\"\"\"\n", + " You are a data analyst.\n", + " Given the tasks you have been assigned, you should analyze the data and provide results using the tools provided.\n", + " \"\"\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Workflow\n", + "\n", + "1. The task is received by the {py:class}`~autogen_agentchat.teams.SelectorGroupChat` which, based on agent descriptions, selects the most appropriate agent to handle the initial task (typically the Planning Agent).\n", + "\n", + "2. The **Planning Agent** analyzes the task and breaks it down into subtasks, assigning each to the most appropriate agent using the format:\n", + " ` : `\n", + "\n", + "3. Based on the conversation context and agent descriptions, the {py:class}`~autogen_agent.teams.SelectorGroupChat` manager dynamically selects the next agent to handle their assigned subtask.\n", + "\n", + "4. The **Web Search Agent** performs searches one at a time, storing results in the shared conversation history.\n", + "\n", + "5. The **Data Analyst** processes the gathered information using available calculation tools when selected.\n", + "\n", + "6. The workflow continues with agents being dynamically selected until either:\n", + " - The Planning Agent determines all subtasks are complete and sends \"TERMINATE\"\n", + " - An alternative termination condition is met (e.g., a maximum number of messages)\n", + "\n", + "When defining your agents, make sure to include a helpful {py:attr}`~autogen_agentchat.base.ChatAgent.description` since this is used to decide which agent to select next." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create the team with two termination conditions:\n", + "{py:class}`~autogen_agentchat.conditions.TextMentionTermination` to end the conversation when the Planning Agent sends \"TERMINATE\",\n", + "and {py:class}`~autogen_agentchat.conditions.MaxMessageTermination` to limit the conversation to 25 messages to avoid infinite loop." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "text_mention_termination = TextMentionTermination(\"TERMINATE\")\n", + "max_messages_termination = MaxMessageTermination(max_messages=25)\n", + "termination = text_mention_termination | max_messages_termination\n", + "\n", + "team = SelectorGroupChat(\n", + " [planning_agent, web_search_agent, data_analyst_agent],\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", + " termination_condition=termination,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we run the team with a task to find information about an NBA player." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?\n", + "---------- PlanningAgent ----------\n", + "To address this request, we will divide the task into manageable subtasks. \n", + "\n", + "1. Web search agent: Identify the Miami Heat player with the highest points in the 2006-2007 season.\n", + "2. Web search agent: Gather the total rebounds for the identified player during the 2007-2008 season.\n", + "3. Web search agent: Gather the total rebounds for the identified player during the 2008-2009 season.\n", + "4. Data analyst: Calculate the percentage change in total rebounds for the identified player between the 2007-2008 and 2008-2009 seasons.\n", + "[Prompt tokens: 159, Completion tokens: 122]\n", + "---------- WebSearchAgent ----------\n", + "[FunctionCall(id='call_xdYlGP2lsqDeWdSiOlwOBNiO', arguments='{\"query\":\"Miami Heat highest points player 2006-2007 season\"}', name='search_web_tool')]\n", + "[Prompt tokens: 271, Completion tokens: 26]\n", + "---------- WebSearchAgent ----------\n", + "[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', call_id='call_xdYlGP2lsqDeWdSiOlwOBNiO')]\n", + "---------- WebSearchAgent ----------\n", + "Tool calls:\n", + "search_web_tool({\"query\":\"Miami Heat highest points player 2006-2007 season\"}) = Here are the total points scored by Miami Heat players in the 2006-2007 season:\n", + " Udonis Haslem: 844 points\n", + " Dwayne Wade: 1397 points\n", + " James Posey: 550 points\n", + " ...\n", + " \n", + "---------- DataAnalystAgent ----------\n", + "[FunctionCall(id='call_asjxKtAVGfqrYl0jMpvwCrvV', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='WebSearchAgent'), FunctionCall(id='call_8L91Kizt0KU6RNwUgvNx7S0s', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='WebSearchAgent')]\n", + "[Prompt tokens: 345, Completion tokens: 68]\n", + "---------- DataAnalystAgent ----------\n", + "[FunctionExecutionResult(content=\"Error: The tool 'WebSearchAgent' is not available.\", call_id='call_asjxKtAVGfqrYl0jMpvwCrvV'), FunctionExecutionResult(content=\"Error: The tool 'WebSearchAgent' is not available.\", call_id='call_8L91Kizt0KU6RNwUgvNx7S0s')]\n", + "---------- DataAnalystAgent ----------\n", + "Tool calls:\n", + "WebSearchAgent({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = Error: The tool 'WebSearchAgent' is not available.\n", + "WebSearchAgent({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = Error: The tool 'WebSearchAgent' is not available.\n", + "---------- WebSearchAgent ----------\n", + "[FunctionCall(id='call_imvRJ2jhpPdovBbx8MFjlFVS', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_U30KVmFG1aeXPbqGJjDmJ6iJ', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')]\n", + "[Prompt tokens: 445, Completion tokens: 70]\n", + "---------- WebSearchAgent ----------\n", + "[FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.', call_id='call_imvRJ2jhpPdovBbx8MFjlFVS'), FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', call_id='call_U30KVmFG1aeXPbqGJjDmJ6iJ')]\n", + "---------- WebSearchAgent ----------\n", + "Tool calls:\n", + "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\n", + "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.\n", + "---------- DataAnalystAgent ----------\n", + "[FunctionCall(id='call_CtAnvcbitN0JiwBfiLVzb5Do', arguments='{\"start\":214,\"end\":398}', name='percentage_change_tool')]\n", + "[Prompt tokens: 562, Completion tokens: 20]\n", + "---------- DataAnalystAgent ----------\n", + "[FunctionExecutionResult(content='85.98130841121495', call_id='call_CtAnvcbitN0JiwBfiLVzb5Do')]\n", + "---------- DataAnalystAgent ----------\n", + "Tool calls:\n", + "percentage_change_tool({\"start\":214,\"end\":398}) = 85.98130841121495\n", + "---------- PlanningAgent ----------\n", + "Summary of Findings:\n", + "\n", + "1. Dwyane Wade was the Miami Heat player with the highest points in the 2006-2007 season, scoring a total of 1,397 points.\n", + "2. Dwyane Wade's total rebounds during the 2007-2008 season were 214.\n", + "3. Dwyane Wade's total rebounds during the 2008-2009 season were 398.\n", + "4. The percentage change in Dwyane Wade's total rebounds between the 2007-2008 and 2008-2009 seasons was approximately 85.98%.\n", + "\n", + "TERMINATE\n", + "[Prompt tokens: 590, Completion tokens: 122]\n", + "---------- Summary ----------\n", + "Number of messages: 15\n", + "Finish reason: Text 'TERMINATE' mentioned\n", + "Total prompt tokens: 2372\n", + "Total completion tokens: 428\n", + "Duration: 9.21 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=159, completion_tokens=122), content='To address this request, we will divide the task into manageable subtasks. \\n\\n1. Web search agent: Identify the Miami Heat player with the highest points in the 2006-2007 season.\\n2. Web search agent: Gather the total rebounds for the identified player during the 2007-2008 season.\\n3. Web search agent: Gather the total rebounds for the identified player during the 2008-2009 season.\\n4. Data analyst: Calculate the percentage change in total rebounds for the identified player between the 2007-2008 and 2008-2009 seasons.', type='TextMessage'), ToolCallRequestEvent(source='WebSearchAgent', models_usage=RequestUsage(prompt_tokens=271, completion_tokens=26), content=[FunctionCall(id='call_xdYlGP2lsqDeWdSiOlwOBNiO', arguments='{\"query\":\"Miami Heat highest points player 2006-2007 season\"}', name='search_web_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='WebSearchAgent', models_usage=None, content=[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', call_id='call_xdYlGP2lsqDeWdSiOlwOBNiO')], type='ToolCallExecutionEvent'), TextMessage(source='WebSearchAgent', models_usage=None, content='Tool calls:\\nsearch_web_tool({\"query\":\"Miami Heat highest points player 2006-2007 season\"}) = Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', type='TextMessage'), ToolCallRequestEvent(source='DataAnalystAgent', models_usage=RequestUsage(prompt_tokens=345, completion_tokens=68), content=[FunctionCall(id='call_asjxKtAVGfqrYl0jMpvwCrvV', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='WebSearchAgent'), FunctionCall(id='call_8L91Kizt0KU6RNwUgvNx7S0s', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='WebSearchAgent')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='DataAnalystAgent', models_usage=None, content=[FunctionExecutionResult(content=\"Error: The tool 'WebSearchAgent' is not available.\", call_id='call_asjxKtAVGfqrYl0jMpvwCrvV'), FunctionExecutionResult(content=\"Error: The tool 'WebSearchAgent' is not available.\", call_id='call_8L91Kizt0KU6RNwUgvNx7S0s')], type='ToolCallExecutionEvent'), TextMessage(source='DataAnalystAgent', models_usage=None, content='Tool calls:\\nWebSearchAgent({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = Error: The tool \\'WebSearchAgent\\' is not available.\\nWebSearchAgent({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = Error: The tool \\'WebSearchAgent\\' is not available.', type='TextMessage'), ToolCallRequestEvent(source='WebSearchAgent', models_usage=RequestUsage(prompt_tokens=445, completion_tokens=70), content=[FunctionCall(id='call_imvRJ2jhpPdovBbx8MFjlFVS', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_U30KVmFG1aeXPbqGJjDmJ6iJ', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='WebSearchAgent', models_usage=None, content=[FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.', call_id='call_imvRJ2jhpPdovBbx8MFjlFVS'), FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', call_id='call_U30KVmFG1aeXPbqGJjDmJ6iJ')], type='ToolCallExecutionEvent'), TextMessage(source='WebSearchAgent', models_usage=None, content='Tool calls:\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', type='TextMessage'), ToolCallRequestEvent(source='DataAnalystAgent', models_usage=RequestUsage(prompt_tokens=562, completion_tokens=20), content=[FunctionCall(id='call_CtAnvcbitN0JiwBfiLVzb5Do', arguments='{\"start\":214,\"end\":398}', name='percentage_change_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='DataAnalystAgent', models_usage=None, content=[FunctionExecutionResult(content='85.98130841121495', call_id='call_CtAnvcbitN0JiwBfiLVzb5Do')], type='ToolCallExecutionEvent'), TextMessage(source='DataAnalystAgent', models_usage=None, content='Tool calls:\\npercentage_change_tool({\"start\":214,\"end\":398}) = 85.98130841121495', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=590, completion_tokens=122), content=\"Summary of Findings:\\n\\n1. Dwyane Wade was the Miami Heat player with the highest points in the 2006-2007 season, scoring a total of 1,397 points.\\n2. Dwyane Wade's total rebounds during the 2007-2008 season were 214.\\n3. Dwyane Wade's total rebounds during the 2008-2009 season were 398.\\n4. The percentage change in Dwyane Wade's total rebounds between the 2007-2008 and 2008-2009 seasons was approximately 85.98%.\\n\\nTERMINATE\", type='TextMessage')], stop_reason=\"Text 'TERMINATE' mentioned\")" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "task = \"Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?\"\n", + "\n", + "# Use asyncio.run(...) if you are running this in a script.\n", + "await Console(team.run_stream(task=task))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, after the Web Search Agent conducts the necessary searches and the Data Analyst Agent completes the necessary calculations, we find that Dwayne Wade was the Miami Heat player with the highest points in the 2006-2007 season, and the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons is 85.98%!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Custom Selector Function" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Often times we want better control over the selection process. \n", + "To this end, we can set the `selector_func` argument with a custom selector function to override the default model-based selection.\n", + "For instance, we want the Planning Agent to speak immediately after any specialized agent to check the progress.\n", + "\n", + "```{note}\n", + "Returning `None` from the custom selector function will use the default model-based selection.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?\n", + "---------- PlanningAgent ----------\n", + "To address this query, we'll need to break it down into a few specific tasks:\n", + "\n", + "1. Web search agent: Identify the Miami Heat player with the highest points in the 2006-2007 NBA season.\n", + "2. Web search agent: Find the total number of rebounds by this player in the 2007-2008 NBA season.\n", + "3. Web search agent: Find the total number of rebounds by this player in the 2008-2009 NBA season.\n", + "4. Data analyst: Calculate the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons.\n", + "\n", + "Let's get started with these tasks.\n", + "[Prompt tokens: 159, Completion tokens: 132]\n", + "---------- WebSearchAgent ----------\n", + "[FunctionCall(id='call_TSUHOBKhpHmTNoYeJzwSP5V4', arguments='{\"query\":\"Miami Heat highest points player 2006-2007 season\"}', name='search_web_tool')]\n", + "[Prompt tokens: 281, Completion tokens: 26]\n", + "---------- WebSearchAgent ----------\n", + "[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', call_id='call_TSUHOBKhpHmTNoYeJzwSP5V4')]\n", + "---------- WebSearchAgent ----------\n", + "Tool calls:\n", + "search_web_tool({\"query\":\"Miami Heat highest points player 2006-2007 season\"}) = Here are the total points scored by Miami Heat players in the 2006-2007 season:\n", + " Udonis Haslem: 844 points\n", + " Dwayne Wade: 1397 points\n", + " James Posey: 550 points\n", + " ...\n", + " \n", + "---------- PlanningAgent ----------\n", + "1. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2007-2008 NBA season.\n", + "2. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2008-2009 NBA season.\n", + "[Prompt tokens: 382, Completion tokens: 54]\n", + "---------- DataAnalystAgent ----------\n", + "[FunctionCall(id='call_BkPBFkpuTG6c3eeoACrrRX7V', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_5LQquT7ZUAAQRf7gvckeTVdQ', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')]\n", + "[Prompt tokens: 416, Completion tokens: 68]\n", + "---------- DataAnalystAgent ----------\n", + "[FunctionExecutionResult(content=\"Error: The tool 'search_web_tool' is not available.\", call_id='call_BkPBFkpuTG6c3eeoACrrRX7V'), FunctionExecutionResult(content=\"Error: The tool 'search_web_tool' is not available.\", call_id='call_5LQquT7ZUAAQRf7gvckeTVdQ')]\n", + "---------- DataAnalystAgent ----------\n", + "Tool calls:\n", + "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = Error: The tool 'search_web_tool' is not available.\n", + "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = Error: The tool 'search_web_tool' is not available.\n", + "---------- PlanningAgent ----------\n", + "It seems there was a miscommunication in task assignment. Let me reassess and reassign the tasks correctly.\n", + "\n", + "1. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2007-2008 NBA season.\n", + "2. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2008-2009 NBA season.\n", + "[Prompt tokens: 525, Completion tokens: 76]\n", + "---------- WebSearchAgent ----------\n", + "[FunctionCall(id='call_buIWOtu1dJqPaxJmqMyuRkpj', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_qcnHKdoPsNAzMlPvoBvqmt8n', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')]\n", + "[Prompt tokens: 599, Completion tokens: 70]\n", + "---------- WebSearchAgent ----------\n", + "[FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.', call_id='call_buIWOtu1dJqPaxJmqMyuRkpj'), FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', call_id='call_qcnHKdoPsNAzMlPvoBvqmt8n')]\n", + "---------- WebSearchAgent ----------\n", + "Tool calls:\n", + "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\n", + "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.\n", + "---------- PlanningAgent ----------\n", + "With this information, we can proceed to calculate the percentage change in Dwyane Wade's total rebounds from the 2007-2008 season to the 2008-2009 season.\n", + "\n", + "1. Data analyst: Calculate the percentage change in Dwyane Wade's total rebounds between the 2007-2008 (214 rebounds) and the 2008-2009 (398 rebounds) NBA seasons.\n", + "[Prompt tokens: 711, Completion tokens: 83]\n", + "---------- DataAnalystAgent ----------\n", + "[FunctionCall(id='call_RjbFpLCehz1Nlk5kYmyMUenB', arguments='{\"start\":214,\"end\":398}', name='percentage_change_tool')]\n", + "[Prompt tokens: 806, Completion tokens: 20]\n", + "---------- DataAnalystAgent ----------\n", + "[FunctionExecutionResult(content='85.98130841121495', call_id='call_RjbFpLCehz1Nlk5kYmyMUenB')]\n", + "---------- DataAnalystAgent ----------\n", + "Tool calls:\n", + "percentage_change_tool({\"start\":214,\"end\":398}) = 85.98130841121495\n", + "---------- PlanningAgent ----------\n", + "Based on the data collected, Dwyane Wade was the Miami Heat player with the highest points in the 2006-2007 NBA season, scoring a total of 1,397 points. Between the 2007-2008 and 2008-2009 seasons, Dwyane Wade's total rebounds increased from 214 to 398. This represents an approximate 85.98% increase in his total rebounds.\n", + "\n", + "TERMINATE\n", + "[Prompt tokens: 834, Completion tokens: 90]\n", + "---------- Summary ----------\n", + "Number of messages: 18\n", + "Finish reason: Text 'TERMINATE' mentioned\n", + "Total prompt tokens: 4713\n", + "Total completion tokens: 619\n", + "Duration: 11.72 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=159, completion_tokens=132), content=\"To address this query, we'll need to break it down into a few specific tasks:\\n\\n1. Web search agent: Identify the Miami Heat player with the highest points in the 2006-2007 NBA season.\\n2. Web search agent: Find the total number of rebounds by this player in the 2007-2008 NBA season.\\n3. Web search agent: Find the total number of rebounds by this player in the 2008-2009 NBA season.\\n4. Data analyst: Calculate the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons.\\n\\nLet's get started with these tasks.\", type='TextMessage'), ToolCallRequestEvent(source='WebSearchAgent', models_usage=RequestUsage(prompt_tokens=281, completion_tokens=26), content=[FunctionCall(id='call_TSUHOBKhpHmTNoYeJzwSP5V4', arguments='{\"query\":\"Miami Heat highest points player 2006-2007 season\"}', name='search_web_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='WebSearchAgent', models_usage=None, content=[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', call_id='call_TSUHOBKhpHmTNoYeJzwSP5V4')], type='ToolCallExecutionEvent'), TextMessage(source='WebSearchAgent', models_usage=None, content='Tool calls:\\nsearch_web_tool({\"query\":\"Miami Heat highest points player 2006-2007 season\"}) = Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=382, completion_tokens=54), content='1. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2007-2008 NBA season.\\n2. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2008-2009 NBA season.', type='TextMessage'), ToolCallRequestEvent(source='DataAnalystAgent', models_usage=RequestUsage(prompt_tokens=416, completion_tokens=68), content=[FunctionCall(id='call_BkPBFkpuTG6c3eeoACrrRX7V', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_5LQquT7ZUAAQRf7gvckeTVdQ', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='DataAnalystAgent', models_usage=None, content=[FunctionExecutionResult(content=\"Error: The tool 'search_web_tool' is not available.\", call_id='call_BkPBFkpuTG6c3eeoACrrRX7V'), FunctionExecutionResult(content=\"Error: The tool 'search_web_tool' is not available.\", call_id='call_5LQquT7ZUAAQRf7gvckeTVdQ')], type='ToolCallExecutionEvent'), TextMessage(source='DataAnalystAgent', models_usage=None, content='Tool calls:\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = Error: The tool \\'search_web_tool\\' is not available.\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = Error: The tool \\'search_web_tool\\' is not available.', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=525, completion_tokens=76), content='It seems there was a miscommunication in task assignment. Let me reassess and reassign the tasks correctly.\\n\\n1. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2007-2008 NBA season.\\n2. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2008-2009 NBA season.', type='TextMessage'), ToolCallRequestEvent(source='WebSearchAgent', models_usage=RequestUsage(prompt_tokens=599, completion_tokens=70), content=[FunctionCall(id='call_buIWOtu1dJqPaxJmqMyuRkpj', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_qcnHKdoPsNAzMlPvoBvqmt8n', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='WebSearchAgent', models_usage=None, content=[FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.', call_id='call_buIWOtu1dJqPaxJmqMyuRkpj'), FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', call_id='call_qcnHKdoPsNAzMlPvoBvqmt8n')], type='ToolCallExecutionEvent'), TextMessage(source='WebSearchAgent', models_usage=None, content='Tool calls:\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=711, completion_tokens=83), content=\"With this information, we can proceed to calculate the percentage change in Dwyane Wade's total rebounds from the 2007-2008 season to the 2008-2009 season.\\n\\n1. Data analyst: Calculate the percentage change in Dwyane Wade's total rebounds between the 2007-2008 (214 rebounds) and the 2008-2009 (398 rebounds) NBA seasons.\", type='TextMessage'), ToolCallRequestEvent(source='DataAnalystAgent', models_usage=RequestUsage(prompt_tokens=806, completion_tokens=20), content=[FunctionCall(id='call_RjbFpLCehz1Nlk5kYmyMUenB', arguments='{\"start\":214,\"end\":398}', name='percentage_change_tool')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='DataAnalystAgent', models_usage=None, content=[FunctionExecutionResult(content='85.98130841121495', call_id='call_RjbFpLCehz1Nlk5kYmyMUenB')], type='ToolCallExecutionEvent'), TextMessage(source='DataAnalystAgent', models_usage=None, content='Tool calls:\\npercentage_change_tool({\"start\":214,\"end\":398}) = 85.98130841121495', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=834, completion_tokens=90), content=\"Based on the data collected, Dwyane Wade was the Miami Heat player with the highest points in the 2006-2007 NBA season, scoring a total of 1,397 points. Between the 2007-2008 and 2008-2009 seasons, Dwyane Wade's total rebounds increased from 214 to 398. This represents an approximate 85.98% increase in his total rebounds.\\n\\nTERMINATE\", type='TextMessage')], stop_reason=\"Text 'TERMINATE' mentioned\")" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def selector_func(messages: Sequence[AgentEvent | ChatMessage]) -> str | None:\n", + " if messages[-1].source != planning_agent.name:\n", + " return planning_agent.name\n", + " return None\n", + "\n", + "\n", + "# Reset the previous team and run the chat again with the selector function.\n", + "await team.reset()\n", + "team = SelectorGroupChat(\n", + " [planning_agent, web_search_agent, data_analyst_agent],\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", + " termination_condition=termination,\n", + " selector_func=selector_func,\n", + ")\n", + "\n", + "await Console(team.run_stream(task=task))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see from the conversation log that the Planning Agent always speaks immediately after the specialized agents." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } }, - { - "data": { - "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=159, completion_tokens=122), content='To address this request, we will divide the task into manageable subtasks. \\n\\n1. Web search agent: Identify the Miami Heat player with the highest points in the 2006-2007 season.\\n2. Web search agent: Gather the total rebounds for the identified player during the 2007-2008 season.\\n3. Web search agent: Gather the total rebounds for the identified player during the 2008-2009 season.\\n4. Data analyst: Calculate the percentage change in total rebounds for the identified player between the 2007-2008 and 2008-2009 seasons.', type='TextMessage'), ToolCallMessage(source='WebSearchAgent', models_usage=RequestUsage(prompt_tokens=271, completion_tokens=26), content=[FunctionCall(id='call_xdYlGP2lsqDeWdSiOlwOBNiO', arguments='{\"query\":\"Miami Heat highest points player 2006-2007 season\"}', name='search_web_tool')], type='ToolCallMessage'), ToolCallResultMessage(source='WebSearchAgent', models_usage=None, content=[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', call_id='call_xdYlGP2lsqDeWdSiOlwOBNiO')], type='ToolCallResultMessage'), TextMessage(source='WebSearchAgent', models_usage=None, content='Tool calls:\\nsearch_web_tool({\"query\":\"Miami Heat highest points player 2006-2007 season\"}) = Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', type='TextMessage'), ToolCallMessage(source='DataAnalystAgent', models_usage=RequestUsage(prompt_tokens=345, completion_tokens=68), content=[FunctionCall(id='call_asjxKtAVGfqrYl0jMpvwCrvV', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='WebSearchAgent'), FunctionCall(id='call_8L91Kizt0KU6RNwUgvNx7S0s', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='WebSearchAgent')], type='ToolCallMessage'), ToolCallResultMessage(source='DataAnalystAgent', models_usage=None, content=[FunctionExecutionResult(content=\"Error: The tool 'WebSearchAgent' is not available.\", call_id='call_asjxKtAVGfqrYl0jMpvwCrvV'), FunctionExecutionResult(content=\"Error: The tool 'WebSearchAgent' is not available.\", call_id='call_8L91Kizt0KU6RNwUgvNx7S0s')], type='ToolCallResultMessage'), TextMessage(source='DataAnalystAgent', models_usage=None, content='Tool calls:\\nWebSearchAgent({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = Error: The tool \\'WebSearchAgent\\' is not available.\\nWebSearchAgent({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = Error: The tool \\'WebSearchAgent\\' is not available.', type='TextMessage'), ToolCallMessage(source='WebSearchAgent', models_usage=RequestUsage(prompt_tokens=445, completion_tokens=70), content=[FunctionCall(id='call_imvRJ2jhpPdovBbx8MFjlFVS', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_U30KVmFG1aeXPbqGJjDmJ6iJ', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')], type='ToolCallMessage'), ToolCallResultMessage(source='WebSearchAgent', models_usage=None, content=[FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.', call_id='call_imvRJ2jhpPdovBbx8MFjlFVS'), FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', call_id='call_U30KVmFG1aeXPbqGJjDmJ6iJ')], type='ToolCallResultMessage'), TextMessage(source='WebSearchAgent', models_usage=None, content='Tool calls:\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', type='TextMessage'), ToolCallMessage(source='DataAnalystAgent', models_usage=RequestUsage(prompt_tokens=562, completion_tokens=20), content=[FunctionCall(id='call_CtAnvcbitN0JiwBfiLVzb5Do', arguments='{\"start\":214,\"end\":398}', name='percentage_change_tool')], type='ToolCallMessage'), ToolCallResultMessage(source='DataAnalystAgent', models_usage=None, content=[FunctionExecutionResult(content='85.98130841121495', call_id='call_CtAnvcbitN0JiwBfiLVzb5Do')], type='ToolCallResultMessage'), TextMessage(source='DataAnalystAgent', models_usage=None, content='Tool calls:\\npercentage_change_tool({\"start\":214,\"end\":398}) = 85.98130841121495', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=590, completion_tokens=122), content=\"Summary of Findings:\\n\\n1. Dwyane Wade was the Miami Heat player with the highest points in the 2006-2007 season, scoring a total of 1,397 points.\\n2. Dwyane Wade's total rebounds during the 2007-2008 season were 214.\\n3. Dwyane Wade's total rebounds during the 2008-2009 season were 398.\\n4. The percentage change in Dwyane Wade's total rebounds between the 2007-2008 and 2008-2009 seasons was approximately 85.98%.\\n\\nTERMINATE\", type='TextMessage')], stop_reason=\"Text 'TERMINATE' mentioned\")" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "task = \"Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?\"\n", - "\n", - "# Use asyncio.run(...) if you are running this in a script.\n", - "await Console(team.run_stream(task=task))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we can see, after the Web Search Agent conducts the necessary searches and the Data Analyst Agent completes the necessary calculations, we find that Dwayne Wade was the Miami Heat player with the highest points in the 2006-2007 season, and the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons is 85.98%!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Custom Selector Function" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Often times we want better control over the selection process. \n", - "To this end, we can set the `selector_func` argument with a custom selector function to override the default model-based selection.\n", - "For instance, we want the Planning Agent to speak immediately after any specialized agent to check the progress.\n", - "\n", - "```{note}\n", - "Returning `None` from the custom selector function will use the default model-based selection.\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?\n", - "---------- PlanningAgent ----------\n", - "To address this query, we'll need to break it down into a few specific tasks:\n", - "\n", - "1. Web search agent: Identify the Miami Heat player with the highest points in the 2006-2007 NBA season.\n", - "2. Web search agent: Find the total number of rebounds by this player in the 2007-2008 NBA season.\n", - "3. Web search agent: Find the total number of rebounds by this player in the 2008-2009 NBA season.\n", - "4. Data analyst: Calculate the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons.\n", - "\n", - "Let's get started with these tasks.\n", - "[Prompt tokens: 159, Completion tokens: 132]\n", - "---------- WebSearchAgent ----------\n", - "[FunctionCall(id='call_TSUHOBKhpHmTNoYeJzwSP5V4', arguments='{\"query\":\"Miami Heat highest points player 2006-2007 season\"}', name='search_web_tool')]\n", - "[Prompt tokens: 281, Completion tokens: 26]\n", - "---------- WebSearchAgent ----------\n", - "[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', call_id='call_TSUHOBKhpHmTNoYeJzwSP5V4')]\n", - "---------- WebSearchAgent ----------\n", - "Tool calls:\n", - "search_web_tool({\"query\":\"Miami Heat highest points player 2006-2007 season\"}) = Here are the total points scored by Miami Heat players in the 2006-2007 season:\n", - " Udonis Haslem: 844 points\n", - " Dwayne Wade: 1397 points\n", - " James Posey: 550 points\n", - " ...\n", - " \n", - "---------- PlanningAgent ----------\n", - "1. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2007-2008 NBA season.\n", - "2. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2008-2009 NBA season.\n", - "[Prompt tokens: 382, Completion tokens: 54]\n", - "---------- DataAnalystAgent ----------\n", - "[FunctionCall(id='call_BkPBFkpuTG6c3eeoACrrRX7V', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_5LQquT7ZUAAQRf7gvckeTVdQ', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')]\n", - "[Prompt tokens: 416, Completion tokens: 68]\n", - "---------- DataAnalystAgent ----------\n", - "[FunctionExecutionResult(content=\"Error: The tool 'search_web_tool' is not available.\", call_id='call_BkPBFkpuTG6c3eeoACrrRX7V'), FunctionExecutionResult(content=\"Error: The tool 'search_web_tool' is not available.\", call_id='call_5LQquT7ZUAAQRf7gvckeTVdQ')]\n", - "---------- DataAnalystAgent ----------\n", - "Tool calls:\n", - "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = Error: The tool 'search_web_tool' is not available.\n", - "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = Error: The tool 'search_web_tool' is not available.\n", - "---------- PlanningAgent ----------\n", - "It seems there was a miscommunication in task assignment. Let me reassess and reassign the tasks correctly.\n", - "\n", - "1. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2007-2008 NBA season.\n", - "2. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2008-2009 NBA season.\n", - "[Prompt tokens: 525, Completion tokens: 76]\n", - "---------- WebSearchAgent ----------\n", - "[FunctionCall(id='call_buIWOtu1dJqPaxJmqMyuRkpj', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_qcnHKdoPsNAzMlPvoBvqmt8n', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')]\n", - "[Prompt tokens: 599, Completion tokens: 70]\n", - "---------- WebSearchAgent ----------\n", - "[FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.', call_id='call_buIWOtu1dJqPaxJmqMyuRkpj'), FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', call_id='call_qcnHKdoPsNAzMlPvoBvqmt8n')]\n", - "---------- WebSearchAgent ----------\n", - "Tool calls:\n", - "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\n", - "search_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.\n", - "---------- PlanningAgent ----------\n", - "With this information, we can proceed to calculate the percentage change in Dwyane Wade's total rebounds from the 2007-2008 season to the 2008-2009 season.\n", - "\n", - "1. Data analyst: Calculate the percentage change in Dwyane Wade's total rebounds between the 2007-2008 (214 rebounds) and the 2008-2009 (398 rebounds) NBA seasons.\n", - "[Prompt tokens: 711, Completion tokens: 83]\n", - "---------- DataAnalystAgent ----------\n", - "[FunctionCall(id='call_RjbFpLCehz1Nlk5kYmyMUenB', arguments='{\"start\":214,\"end\":398}', name='percentage_change_tool')]\n", - "[Prompt tokens: 806, Completion tokens: 20]\n", - "---------- DataAnalystAgent ----------\n", - "[FunctionExecutionResult(content='85.98130841121495', call_id='call_RjbFpLCehz1Nlk5kYmyMUenB')]\n", - "---------- DataAnalystAgent ----------\n", - "Tool calls:\n", - "percentage_change_tool({\"start\":214,\"end\":398}) = 85.98130841121495\n", - "---------- PlanningAgent ----------\n", - "Based on the data collected, Dwyane Wade was the Miami Heat player with the highest points in the 2006-2007 NBA season, scoring a total of 1,397 points. Between the 2007-2008 and 2008-2009 seasons, Dwyane Wade's total rebounds increased from 214 to 398. This represents an approximate 85.98% increase in his total rebounds.\n", - "\n", - "TERMINATE\n", - "[Prompt tokens: 834, Completion tokens: 90]\n", - "---------- Summary ----------\n", - "Number of messages: 18\n", - "Finish reason: Text 'TERMINATE' mentioned\n", - "Total prompt tokens: 4713\n", - "Total completion tokens: 619\n", - "Duration: 11.72 seconds\n" - ] - }, - { - "data": { - "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Who was the Miami Heat player with the highest points in the 2006-2007 season, and what was the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons?', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=159, completion_tokens=132), content=\"To address this query, we'll need to break it down into a few specific tasks:\\n\\n1. Web search agent: Identify the Miami Heat player with the highest points in the 2006-2007 NBA season.\\n2. Web search agent: Find the total number of rebounds by this player in the 2007-2008 NBA season.\\n3. Web search agent: Find the total number of rebounds by this player in the 2008-2009 NBA season.\\n4. Data analyst: Calculate the percentage change in his total rebounds between the 2007-2008 and 2008-2009 seasons.\\n\\nLet's get started with these tasks.\", type='TextMessage'), ToolCallMessage(source='WebSearchAgent', models_usage=RequestUsage(prompt_tokens=281, completion_tokens=26), content=[FunctionCall(id='call_TSUHOBKhpHmTNoYeJzwSP5V4', arguments='{\"query\":\"Miami Heat highest points player 2006-2007 season\"}', name='search_web_tool')], type='ToolCallMessage'), ToolCallResultMessage(source='WebSearchAgent', models_usage=None, content=[FunctionExecutionResult(content='Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', call_id='call_TSUHOBKhpHmTNoYeJzwSP5V4')], type='ToolCallResultMessage'), TextMessage(source='WebSearchAgent', models_usage=None, content='Tool calls:\\nsearch_web_tool({\"query\":\"Miami Heat highest points player 2006-2007 season\"}) = Here are the total points scored by Miami Heat players in the 2006-2007 season:\\n Udonis Haslem: 844 points\\n Dwayne Wade: 1397 points\\n James Posey: 550 points\\n ...\\n ', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=382, completion_tokens=54), content='1. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2007-2008 NBA season.\\n2. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2008-2009 NBA season.', type='TextMessage'), ToolCallMessage(source='DataAnalystAgent', models_usage=RequestUsage(prompt_tokens=416, completion_tokens=68), content=[FunctionCall(id='call_BkPBFkpuTG6c3eeoACrrRX7V', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_5LQquT7ZUAAQRf7gvckeTVdQ', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')], type='ToolCallMessage'), ToolCallResultMessage(source='DataAnalystAgent', models_usage=None, content=[FunctionExecutionResult(content=\"Error: The tool 'search_web_tool' is not available.\", call_id='call_BkPBFkpuTG6c3eeoACrrRX7V'), FunctionExecutionResult(content=\"Error: The tool 'search_web_tool' is not available.\", call_id='call_5LQquT7ZUAAQRf7gvckeTVdQ')], type='ToolCallResultMessage'), TextMessage(source='DataAnalystAgent', models_usage=None, content='Tool calls:\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = Error: The tool \\'search_web_tool\\' is not available.\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = Error: The tool \\'search_web_tool\\' is not available.', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=525, completion_tokens=76), content='It seems there was a miscommunication in task assignment. Let me reassess and reassign the tasks correctly.\\n\\n1. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2007-2008 NBA season.\\n2. Web search agent: Find the total number of rebounds by Dwayne Wade in the 2008-2009 NBA season.', type='TextMessage'), ToolCallMessage(source='WebSearchAgent', models_usage=RequestUsage(prompt_tokens=599, completion_tokens=70), content=[FunctionCall(id='call_buIWOtu1dJqPaxJmqMyuRkpj', arguments='{\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}', name='search_web_tool'), FunctionCall(id='call_qcnHKdoPsNAzMlPvoBvqmt8n', arguments='{\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}', name='search_web_tool')], type='ToolCallMessage'), ToolCallResultMessage(source='WebSearchAgent', models_usage=None, content=[FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.', call_id='call_buIWOtu1dJqPaxJmqMyuRkpj'), FunctionExecutionResult(content='The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', call_id='call_qcnHKdoPsNAzMlPvoBvqmt8n')], type='ToolCallResultMessage'), TextMessage(source='WebSearchAgent', models_usage=None, content='Tool calls:\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2007-2008 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2007-2008 is 214.\\nsearch_web_tool({\"query\": \"Dwyane Wade total rebounds 2008-2009 season\"}) = The number of total rebounds for Dwayne Wade in the Miami Heat season 2008-2009 is 398.', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=711, completion_tokens=83), content=\"With this information, we can proceed to calculate the percentage change in Dwyane Wade's total rebounds from the 2007-2008 season to the 2008-2009 season.\\n\\n1. Data analyst: Calculate the percentage change in Dwyane Wade's total rebounds between the 2007-2008 (214 rebounds) and the 2008-2009 (398 rebounds) NBA seasons.\", type='TextMessage'), ToolCallMessage(source='DataAnalystAgent', models_usage=RequestUsage(prompt_tokens=806, completion_tokens=20), content=[FunctionCall(id='call_RjbFpLCehz1Nlk5kYmyMUenB', arguments='{\"start\":214,\"end\":398}', name='percentage_change_tool')], type='ToolCallMessage'), ToolCallResultMessage(source='DataAnalystAgent', models_usage=None, content=[FunctionExecutionResult(content='85.98130841121495', call_id='call_RjbFpLCehz1Nlk5kYmyMUenB')], type='ToolCallResultMessage'), TextMessage(source='DataAnalystAgent', models_usage=None, content='Tool calls:\\npercentage_change_tool({\"start\":214,\"end\":398}) = 85.98130841121495', type='TextMessage'), TextMessage(source='PlanningAgent', models_usage=RequestUsage(prompt_tokens=834, completion_tokens=90), content=\"Based on the data collected, Dwyane Wade was the Miami Heat player with the highest points in the 2006-2007 NBA season, scoring a total of 1,397 points. Between the 2007-2008 and 2008-2009 seasons, Dwyane Wade's total rebounds increased from 214 to 398. This represents an approximate 85.98% increase in his total rebounds.\\n\\nTERMINATE\", type='TextMessage')], stop_reason=\"Text 'TERMINATE' mentioned\")" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def selector_func(messages: Sequence[AgentMessage]) -> str | None:\n", - " if messages[-1].source != planning_agent.name:\n", - " return planning_agent.name\n", - " return None\n", - "\n", - "\n", - "# Reset the previous team and run the chat again with the selector function.\n", - "await team.reset()\n", - "team = SelectorGroupChat(\n", - " [planning_agent, web_search_agent, data_analyst_agent],\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", - " termination_condition=termination,\n", - " selector_func=selector_func,\n", - ")\n", - "\n", - "await Console(team.run_stream(task=task))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can see from the conversation log that the Planning Agent always speaks immediately after the specialized agents." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/state.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/state.ipynb index c7bff2cc6bb6..dad2dd559907 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/state.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/state.ipynb @@ -26,9 +26,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "In Tanganyika's depths so wide and deep, \n", - "Ancient secrets in still waters sleep, \n", - "Ripples tell tales that time longs to keep. \n" + "In Tanganyika's embrace so wide and deep, \n", + "Ancient waters cradle secrets they keep, \n", + "Echoes of time where horizons sleep. \n" ] } ], @@ -59,14 +59,14 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'type': 'AssistantAgentState', 'version': '1.0.0', 'llm_messages': [{'content': 'Write a 3 line poem on lake tangayika', 'source': 'user', 'type': 'UserMessage'}, {'content': \"In Tanganyika's depths so wide and deep, \\nAncient secrets in still waters sleep, \\nRipples tell tales that time longs to keep. \", 'source': 'assistant_agent', 'type': 'AssistantMessage'}]}\n" + "{'type': 'AssistantAgentState', 'version': '1.0.0', 'llm_messages': [{'content': 'Write a 3 line poem on lake tangayika', 'source': 'user', 'type': 'UserMessage'}, {'content': \"In Tanganyika's embrace so wide and deep, \\nAncient waters cradle secrets they keep, \\nEchoes of time where horizons sleep. \", 'source': 'assistant_agent', 'type': 'AssistantMessage'}]}\n" ] } ], @@ -77,15 +77,14 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "The last line of the poem I wrote was: \n", - "\"Ripples tell tales that time longs to keep.\"\n" + "The last line of the poem was: \"Echoes of time where horizons sleep.\"\n" ] } ], @@ -131,7 +130,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -141,16 +140,16 @@ "---------- user ----------\n", "Write a beautiful poem 3-line about lake tangayika\n", "---------- assistant_agent ----------\n", - "In Tanganyika's depths, where light gently weaves, \n", - "Silver reflections dance on ancient water's face, \n", - "Whispered stories of time in the rippling leaves. \n", - "[Prompt tokens: 29, Completion tokens: 36]\n", + "In Tanganyika's gleam, beneath the azure skies, \n", + "Whispers of ancient waters, in tranquil guise, \n", + "Nature's mirror, where dreams and serenity lie.\n", + "[Prompt tokens: 29, Completion tokens: 34]\n", "---------- Summary ----------\n", "Number of messages: 2\n", "Finish reason: Maximum number of messages 2 reached, current message count: 2\n", "Total prompt tokens: 29\n", - "Total completion tokens: 36\n", - "Duration: 1.16 seconds\n" + "Total completion tokens: 34\n", + "Duration: 0.71 seconds\n" ] } ], @@ -184,7 +183,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -194,23 +193,23 @@ "---------- user ----------\n", "What was the last line of the poem you wrote?\n", "---------- assistant_agent ----------\n", - "I don't write poems on my own, but I can help create one with you or try to recall a specific poem if you have one in mind. Let me know what you'd like to do!\n", - "[Prompt tokens: 28, Completion tokens: 39]\n", + "I'm sorry, but I am unable to recall or access previous interactions, including any specific poem I may have composed in our past conversations. If you like, I can write a new poem for you.\n", + "[Prompt tokens: 28, Completion tokens: 40]\n", "---------- Summary ----------\n", "Number of messages: 2\n", "Finish reason: Maximum number of messages 2 reached, current message count: 2\n", "Total prompt tokens: 28\n", - "Total completion tokens: 39\n", - "Duration: 0.95 seconds\n" + "Total completion tokens: 40\n", + "Duration: 0.70 seconds\n" ] }, { "data": { "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, type='TextMessage', content='What was the last line of the poem you wrote?'), TextMessage(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=28, completion_tokens=39), type='TextMessage', content=\"I don't write poems on my own, but I can help create one with you or try to recall a specific poem if you have one in mind. Let me know what you'd like to do!\")], stop_reason='Maximum number of messages 2 reached, current message count: 2')" + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='What was the last line of the poem you wrote?', type='TextMessage'), TextMessage(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=28, completion_tokens=40), content=\"I'm sorry, but I am unable to recall or access previous interactions, including any specific poem I may have composed in our past conversations. If you like, I can write a new poem for you.\", type='TextMessage')], stop_reason='Maximum number of messages 2 reached, current message count: 2')" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -225,42 +224,40 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Next, we load the state of the team and ask the same question. We see that the team is able to accurately return the last line of the poem it wrote.\n", - "\n", - "Note: You can serialize the state of the team to a file and load it back later." + "Next, we load the state of the team and ask the same question. We see that the team is able to accurately return the last line of the poem it wrote." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'type': 'TeamState', 'version': '1.0.0', 'agent_states': {'group_chat_manager/c80054be-efb2-4bc7-ba0d-900962092c44': {'type': 'RoundRobinManagerState', 'version': '1.0.0', 'message_thread': [{'source': 'user', 'models_usage': None, 'type': 'TextMessage', 'content': 'Write a beautiful poem 3-line about lake tangayika'}, {'source': 'assistant_agent', 'models_usage': {'prompt_tokens': 29, 'completion_tokens': 36}, 'type': 'TextMessage', 'content': \"In Tanganyika's depths, where light gently weaves, \\nSilver reflections dance on ancient water's face, \\nWhispered stories of time in the rippling leaves. \"}], 'current_turn': 0, 'next_speaker_index': 0}, 'collect_output_messages/c80054be-efb2-4bc7-ba0d-900962092c44': {}, 'assistant_agent/c80054be-efb2-4bc7-ba0d-900962092c44': {'type': 'ChatAgentContainerState', 'version': '1.0.0', 'agent_state': {'type': 'AssistantAgentState', 'version': '1.0.0', 'llm_messages': [{'content': 'Write a beautiful poem 3-line about lake tangayika', 'source': 'user', 'type': 'UserMessage'}, {'content': \"In Tanganyika's depths, where light gently weaves, \\nSilver reflections dance on ancient water's face, \\nWhispered stories of time in the rippling leaves. \", 'source': 'assistant_agent', 'type': 'AssistantMessage'}]}, 'message_buffer': []}}, 'team_id': 'c80054be-efb2-4bc7-ba0d-900962092c44'}\n", + "{'type': 'TeamState', 'version': '1.0.0', 'agent_states': {'group_chat_manager/a55364ad-86fd-46ab-9449-dcb5260b1e06': {'type': 'RoundRobinManagerState', 'version': '1.0.0', 'message_thread': [{'source': 'user', 'models_usage': None, 'content': 'Write a beautiful poem 3-line about lake tangayika', 'type': 'TextMessage'}, {'source': 'assistant_agent', 'models_usage': {'prompt_tokens': 29, 'completion_tokens': 34}, 'content': \"In Tanganyika's gleam, beneath the azure skies, \\nWhispers of ancient waters, in tranquil guise, \\nNature's mirror, where dreams and serenity lie.\", 'type': 'TextMessage'}], 'current_turn': 0, 'next_speaker_index': 0}, 'collect_output_messages/a55364ad-86fd-46ab-9449-dcb5260b1e06': {}, 'assistant_agent/a55364ad-86fd-46ab-9449-dcb5260b1e06': {'type': 'ChatAgentContainerState', 'version': '1.0.0', 'agent_state': {'type': 'AssistantAgentState', 'version': '1.0.0', 'llm_messages': [{'content': 'Write a beautiful poem 3-line about lake tangayika', 'source': 'user', 'type': 'UserMessage'}, {'content': \"In Tanganyika's gleam, beneath the azure skies, \\nWhispers of ancient waters, in tranquil guise, \\nNature's mirror, where dreams and serenity lie.\", 'source': 'assistant_agent', 'type': 'AssistantMessage'}]}, 'message_buffer': []}}, 'team_id': 'a55364ad-86fd-46ab-9449-dcb5260b1e06'}\n", "---------- user ----------\n", "What was the last line of the poem you wrote?\n", "---------- assistant_agent ----------\n", - "The last line of the poem I wrote was: \n", - "\"Whispered stories of time in the rippling leaves.\"\n", - "[Prompt tokens: 88, Completion tokens: 24]\n", + "The last line of the poem I wrote is: \n", + "\"Nature's mirror, where dreams and serenity lie.\"\n", + "[Prompt tokens: 86, Completion tokens: 22]\n", "---------- Summary ----------\n", "Number of messages: 2\n", "Finish reason: Maximum number of messages 2 reached, current message count: 2\n", - "Total prompt tokens: 88\n", - "Total completion tokens: 24\n", - "Duration: 0.79 seconds\n" + "Total prompt tokens: 86\n", + "Total completion tokens: 22\n", + "Duration: 0.96 seconds\n" ] }, { "data": { "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, type='TextMessage', content='What was the last line of the poem you wrote?'), TextMessage(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=88, completion_tokens=24), type='TextMessage', content='The last line of the poem I wrote was: \\n\"Whispered stories of time in the rippling leaves.\"')], stop_reason='Maximum number of messages 2 reached, current message count: 2')" + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='What was the last line of the poem you wrote?', type='TextMessage'), TextMessage(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=86, completion_tokens=22), content='The last line of the poem I wrote is: \\n\"Nature\\'s mirror, where dreams and serenity lie.\"', type='TextMessage')], stop_reason='Maximum number of messages 2 reached, current message count: 2')" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -273,11 +270,72 @@ "stream = agent_team.run_stream(task=\"What was the last line of the poem you wrote?\")\n", "await Console(stream)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Persisting State (File or Database)\n", + "\n", + "In many cases, we may want to persist the state of the team to disk (or a database) and load it back later. State is a dictionary that can be serialized to a file or written to a database." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "What was the last line of the poem you wrote?\n", + "---------- assistant_agent ----------\n", + "The last line of the poem I wrote is: \n", + "\"Nature's mirror, where dreams and serenity lie.\"\n", + "[Prompt tokens: 86, Completion tokens: 22]\n", + "---------- Summary ----------\n", + "Number of messages: 2\n", + "Finish reason: Maximum number of messages 2 reached, current message count: 2\n", + "Total prompt tokens: 86\n", + "Total completion tokens: 22\n", + "Duration: 0.72 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='What was the last line of the poem you wrote?', type='TextMessage'), TextMessage(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=86, completion_tokens=22), content='The last line of the poem I wrote is: \\n\"Nature\\'s mirror, where dreams and serenity lie.\"', type='TextMessage')], stop_reason='Maximum number of messages 2 reached, current message count: 2')" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import json\n", + "\n", + "## save state to disk\n", + "\n", + "with open(\"coding/team_state.json\", \"w\") as f:\n", + " json.dump(team_state, f)\n", + "\n", + "## load state from disk\n", + "with open(\"coding/team_state.json\", \"r\") as f:\n", + " team_state = json.load(f)\n", + "\n", + "new_agent_team = RoundRobinGroupChat([assistant_agent], termination_condition=MaxMessageTermination(max_messages=2))\n", + "await new_agent_team.load_state(team_state)\n", + "stream = new_agent_team.run_stream(task=\"What was the last line of the poem you wrote?\")\n", + "await Console(stream)" + ] } ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "agnext", "language": "python", "name": "python3" }, @@ -291,7 +349,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.5" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/swarm.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/swarm.ipynb index b0feb00d003b..6dd0511089cc 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/swarm.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/swarm.ipynb @@ -1,532 +1,532 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Swarm\n", - "\n", - "{py:class}`~autogen_agentchat.teams.Swarm` implements a team in which agents can hand off \n", - "task to other agents based on their capabilities. \n", - "It is a multi-agent design pattern first introduced by OpenAI in \n", - "[an experimental project](https://github.com/openai/swarm).\n", - "The key idea is to let agent delegate tasks to other agents using a special tool call, while\n", - "all agents share the same message context.\n", - "This enables agents to make local decisions about task planning, rather than\n", - "relying on a central orchestrator such as in {py:class}`~autogen_agentchat.teams.SelectorGroupChat`.\n", - "\n", - "```{note}\n", - "{py:class}`~autogen_agentchat.teams.Swarm` is a high-level API. If you need more\n", - "control and customization that is not supported by this API, you can take a look\n", - "at the [Handoff Pattern](../../core-user-guide/design-patterns/handoffs.ipynb)\n", - "in the Core API documentation and implement your own version of the Swarm pattern.\n", - "```\n", - "\n", - "## How Does It Work?\n", - "\n", - "At its core, the {py:class}`~autogen_agentchat.teams.Swarm` team is a group chat\n", - "where agents take turn to generate a response. \n", - "Similar to {py:class}`~autogen_agentchat.teams.SelectorGroupChat`\n", - "and {py:class}`~autogen_agentchat.teams.RoundRobinGroupChat`, participant agents\n", - "broadcast their responses so all agents share the same mesasge context.\n", - "\n", - "Different from the other two group chat teams, at each turn,\n", - "**the speaker agent is selected based on the most recent\n", - "{py:class}`~autogen_agentchat.messages.HandoffMessage` message in the context.**\n", - "This naturally requires each agent in the team to be able to generate\n", - "{py:class}`~autogen_agentchat.messages.HandoffMessage` to signal\n", - "which other agents that it hands off to.\n", - "\n", - "For {py:class}`~autogen_agentchat.agents.AssistantAgent`, you can set the\n", - "`handoffs` argument to specify which agents it can hand off to. You can\n", - "use {py:class}`~autogen_agentchat.base.Handoff` to customize the message\n", - "content and handoff behavior.\n", - "\n", - "The overall process can be summarized as follows:\n", - "\n", - "1. Each agent has the ability to generate {py:class}`~autogen_agentchat.messages.HandoffMessage`\n", - " to signal which other agents it can hand off to. For {py:class}`~autogen_agentchat.agents.AssistantAgent`, this means setting the `handoffs` argument.\n", - "2. When the team starts on a task, the first speaker agents operate on the task and make locallized decision about whether to hand off and to whom.\n", - "3. When an agent generates a {py:class}`~autogen_agentchat.messages.HandoffMessage`, the receiving agent takes over the task with the same message context.\n", - "4. The process continues until a termination condition is met.\n", - "\n", - "In this section, we will show you two examples of how to use the {py:class}`~autogen_agentchat.teams.Swarm` team:\n", - "\n", - "1. A customer support team with human-in-the-loop handoff.\n", - "2. An automonous team for content generation." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Customer Support Example" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![Customer Support](swarm_customer_support.svg)\n", - "\n", - "This system implements a flights refund scenario with two agents:\n", - "\n", - "- **Travel Agent**: Handles general travel and refund coordination.\n", - "- **Flights Refunder**: Specializes in processing flight refunds with the `refund_flight` tool.\n", - "\n", - "Additionally, we let the user interact with the agents, when agents handoff to `\"user\"`.\n", - "\n", - "#### Workflow\n", - "1. The **Travel Agent** initiates the conversation and evaluates the user's request.\n", - "2. Based on the request:\n", - " - For refund-related tasks, the Travel Agent hands off to the **Flights Refunder**.\n", - " - For information needed from the customer, either agent can hand off to the `\"user\"`.\n", - "3. The **Flights Refunder** processes refunds using the `refund_flight` tool when appropriate.\n", - "4. If an agent hands off to the `\"user\"`, the team execution will stop and wait for the user to input a response.\n", - "5. When the user provides input, it's sent back to the team as a {py:class}`~autogen_agentchat.messages.HandaffMessage`. This message is directed to the agent that originally requested user input.\n", - "6. The process continues until the Travel Agent determines the task is complete and terminates the workflow." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Any, Dict, List\n", - "\n", - "from autogen_agentchat.agents import AssistantAgent\n", - "from autogen_agentchat.conditions import HandoffTermination, TextMentionTermination\n", - "from autogen_agentchat.messages import HandoffMessage\n", - "from autogen_agentchat.teams import Swarm\n", - "from autogen_agentchat.ui import Console\n", - "from autogen_ext.models.openai import OpenAIChatCompletionClient" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tools" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "def refund_flight(flight_id: str) -> str:\n", - " \"\"\"Refund a flight\"\"\"\n", - " return f\"Flight {flight_id} refunded\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Agents" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "model_client = OpenAIChatCompletionClient(\n", - " model=\"gpt-4o\",\n", - " # api_key=\"YOUR_API_KEY\",\n", - ")\n", - "\n", - "travel_agent = AssistantAgent(\n", - " \"travel_agent\",\n", - " model_client=model_client,\n", - " handoffs=[\"flights_refunder\", \"user\"],\n", - " system_message=\"\"\"You are a travel agent.\n", - " The flights_refunder is in charge of refunding flights.\n", - " If you need information from the user, you must first send your message, then you can handoff to the user.\n", - " Use TERMINATE when the travel planning is complete.\"\"\",\n", - ")\n", - "\n", - "flights_refunder = AssistantAgent(\n", - " \"flights_refunder\",\n", - " model_client=model_client,\n", - " handoffs=[\"travel_agent\", \"user\"],\n", - " tools=[refund_flight],\n", - " system_message=\"\"\"You are an agent specialized in refunding flights.\n", - " You only need flight reference numbers to refund a flight.\n", - " You have the ability to refund a flight using the refund_flight tool.\n", - " If you need information from the user, you must first send your message, then you can handoff to the user.\n", - " When the transaction is complete, handoff to the travel agent to finalize.\"\"\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "termination = HandoffTermination(target=\"user\") | TextMentionTermination(\"TERMINATE\")\n", - "team = Swarm([travel_agent, flights_refunder], termination_condition=termination)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "I need to refund my flight.\n", - "---------- travel_agent ----------\n", - "[FunctionCall(id='call_ZQ2rGjq4Z29pd0yP2sNcuyd2', arguments='{}', name='transfer_to_flights_refunder')]\n", - "[Prompt tokens: 119, Completion tokens: 14]\n", - "---------- travel_agent ----------\n", - "[FunctionExecutionResult(content='Transferred to flights_refunder, adopting the role of flights_refunder immediately.', call_id='call_ZQ2rGjq4Z29pd0yP2sNcuyd2')]\n", - "---------- travel_agent ----------\n", - "Transferred to flights_refunder, adopting the role of flights_refunder immediately.\n", - "---------- flights_refunder ----------\n", - "Could you please provide me with the flight reference number so I can process the refund for you?\n", - "[Prompt tokens: 191, Completion tokens: 20]\n", - "---------- flights_refunder ----------\n", - "[FunctionCall(id='call_1iRfzNpxTJhRTW2ww9aQJ8sK', arguments='{}', name='transfer_to_user')]\n", - "[Prompt tokens: 219, Completion tokens: 11]\n", - "---------- flights_refunder ----------\n", - "[FunctionExecutionResult(content='Transferred to user, adopting the role of user immediately.', call_id='call_1iRfzNpxTJhRTW2ww9aQJ8sK')]\n", - "---------- flights_refunder ----------\n", - "Transferred to user, adopting the role of user immediately.\n", - "---------- Summary ----------\n", - "Number of messages: 8\n", - "Finish reason: Handoff to user from flights_refunder detected.\n", - "Total prompt tokens: 529\n", - "Total completion tokens: 45\n", - "Duration: 2.05 seconds\n", - "---------- user ----------\n", - "Sure, it's 507811\n", - "---------- flights_refunder ----------\n", - "[FunctionCall(id='call_UKCsoEBdflkvpuT9Bi2xlvTd', arguments='{\"flight_id\":\"507811\"}', name='refund_flight')]\n", - "[Prompt tokens: 266, Completion tokens: 18]\n", - "---------- flights_refunder ----------\n", - "[FunctionExecutionResult(content='Flight 507811 refunded', call_id='call_UKCsoEBdflkvpuT9Bi2xlvTd')]\n", - "---------- flights_refunder ----------\n", - "Tool calls:\n", - "refund_flight({\"flight_id\":\"507811\"}) = Flight 507811 refunded\n", - "---------- flights_refunder ----------\n", - "[FunctionCall(id='call_MQ2CXR8UhVtjNc6jG3wSQp2W', arguments='{}', name='transfer_to_travel_agent')]\n", - "[Prompt tokens: 303, Completion tokens: 13]\n", - "---------- flights_refunder ----------\n", - "[FunctionExecutionResult(content='Transferred to travel_agent, adopting the role of travel_agent immediately.', call_id='call_MQ2CXR8UhVtjNc6jG3wSQp2W')]\n", - "---------- flights_refunder ----------\n", - "Transferred to travel_agent, adopting the role of travel_agent immediately.\n", - "---------- travel_agent ----------\n", - "Your flight with reference number 507811 has been successfully refunded. If you need anything else, feel free to let me know. Safe travels! TERMINATE\n", - "[Prompt tokens: 272, Completion tokens: 32]\n", - "---------- Summary ----------\n", - "Number of messages: 8\n", - "Finish reason: Text 'TERMINATE' mentioned\n", - "Total prompt tokens: 841\n", - "Total completion tokens: 63\n", - "Duration: 1.64 seconds\n" - ] - } - ], - "source": [ - "task = \"I need to refund my flight.\"\n", - "\n", - "\n", - "async def run_team_stream() -> None:\n", - " task_result = await Console(team.run_stream(task=task))\n", - " last_message = task_result.messages[-1]\n", - "\n", - " while isinstance(last_message, HandoffMessage) and last_message.target == \"user\":\n", - " user_message = input(\"User: \")\n", - "\n", - " task_result = await Console(\n", - " team.run_stream(task=HandoffMessage(source=\"user\", target=last_message.source, content=user_message))\n", - " )\n", - " last_message = task_result.messages[-1]\n", - "\n", - "\n", - "await run_team_stream()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Stock Research Example" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![Stock Research](swarm_stock_research.svg)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This system is designed to perform stock research tasks by leveraging four agents:\n", - "\n", - "- **Planner**: The central coordinator that delegates specific tasks to specialized agents based on their expertise. The planner ensures that each agent is utilized efficiently and oversees the overall workflow.\n", - "- **Financial Analyst**: A specialized agent responsible for analyzing financial metrics and stock data using tools such as `get_stock_data`.\n", - "- **News Analyst**: An agent focused on gathering and summarizing recent news articles relevant to the stock, using tools such as `get_news`.\n", - "- **Writer**: An agent tasked with compiling the findings from the stock and news analysis into a cohesive final report.\n", - "\n", - "#### Workflow\n", - "1. The **Planner** initiates the research process by delegating tasks to the appropriate agents in a step-by-step manner.\n", - "2. Each agent performs its task independently and appends their work to the shared **message thread/history**. Rather than directly returning results to the planner, all agents contribute to and read from this shared message history. When agents generate their work using the LLM, they have access to this shared message history, which provides context and helps track the overall progress of the task.\n", - "3. Once an agent completes its task, it hands off control back to the planner.\n", - "4. The process continues until the planner determines that all necessary tasks have been completed and decides to terminate the workflow." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tools" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "async def get_stock_data(symbol: str) -> Dict[str, Any]:\n", - " \"\"\"Get stock market data for a given symbol\"\"\"\n", - " return {\"price\": 180.25, \"volume\": 1000000, \"pe_ratio\": 65.4, \"market_cap\": \"700B\"}\n", - "\n", - "\n", - "async def get_news(query: str) -> List[Dict[str, str]]:\n", - " \"\"\"Get recent news articles about a company\"\"\"\n", - " return [\n", - " {\n", - " \"title\": \"Tesla Expands Cybertruck Production\",\n", - " \"date\": \"2024-03-20\",\n", - " \"summary\": \"Tesla ramps up Cybertruck manufacturing capacity at Gigafactory Texas, aiming to meet strong demand.\",\n", - " },\n", - " {\n", - " \"title\": \"Tesla FSD Beta Shows Promise\",\n", - " \"date\": \"2024-03-19\",\n", - " \"summary\": \"Latest Full Self-Driving beta demonstrates significant improvements in urban navigation and safety features.\",\n", - " },\n", - " {\n", - " \"title\": \"Model Y Dominates Global EV Sales\",\n", - " \"date\": \"2024-03-18\",\n", - " \"summary\": \"Tesla's Model Y becomes best-selling electric vehicle worldwide, capturing significant market share.\",\n", - " },\n", - " ]" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "model_client = OpenAIChatCompletionClient(\n", - " model=\"gpt-4o\",\n", - " # api_key=\"YOUR_API_KEY\",\n", - ")\n", - "\n", - "planner = AssistantAgent(\n", - " \"planner\",\n", - " model_client=model_client,\n", - " handoffs=[\"financial_analyst\", \"news_analyst\", \"writer\"],\n", - " system_message=\"\"\"You are a research planning coordinator.\n", - " Coordinate market research by delegating to specialized agents:\n", - " - Financial Analyst: For stock data analysis\n", - " - News Analyst: For news gathering and analysis\n", - " - Writer: For compiling final report\n", - " Always send your plan first, then handoff to appropriate agent.\n", - " Always handoff to a single agent at a time.\n", - " Use TERMINATE when research is complete.\"\"\",\n", - ")\n", - "\n", - "financial_analyst = AssistantAgent(\n", - " \"financial_analyst\",\n", - " model_client=model_client,\n", - " handoffs=[\"planner\"],\n", - " tools=[get_stock_data],\n", - " system_message=\"\"\"You are a financial analyst.\n", - " Analyze stock market data using the get_stock_data tool.\n", - " Provide insights on financial metrics.\n", - " Always handoff back to planner when analysis is complete.\"\"\",\n", - ")\n", - "\n", - "news_analyst = AssistantAgent(\n", - " \"news_analyst\",\n", - " model_client=model_client,\n", - " handoffs=[\"planner\"],\n", - " tools=[get_news],\n", - " system_message=\"\"\"You are a news analyst.\n", - " Gather and analyze relevant news using the get_news tool.\n", - " Summarize key market insights from news.\n", - " Always handoff back to planner when analysis is complete.\"\"\",\n", - ")\n", - "\n", - "writer = AssistantAgent(\n", - " \"writer\",\n", - " model_client=model_client,\n", - " handoffs=[\"planner\"],\n", - " system_message=\"\"\"You are a financial report writer.\n", - " Compile research findings into clear, concise reports.\n", - " Always handoff back to planner when writing is complete.\"\"\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "Conduct market research for TSLA stock\n", - "---------- planner ----------\n", - "[FunctionCall(id='call_BX5QaRuhmB8CxTsBlqCUIXPb', arguments='{}', name='transfer_to_financial_analyst')]\n", - "[Prompt tokens: 169, Completion tokens: 166]\n", - "---------- planner ----------\n", - "[FunctionExecutionResult(content='Transferred to financial_analyst, adopting the role of financial_analyst immediately.', call_id='call_BX5QaRuhmB8CxTsBlqCUIXPb')]\n", - "---------- planner ----------\n", - "Transferred to financial_analyst, adopting the role of financial_analyst immediately.\n", - "---------- financial_analyst ----------\n", - "[FunctionCall(id='call_SAXy1ebtA9mnaZo4ztpD2xHA', arguments='{\"symbol\":\"TSLA\"}', name='get_stock_data')]\n", - "[Prompt tokens: 136, Completion tokens: 16]\n", - "---------- financial_analyst ----------\n", - "[FunctionExecutionResult(content=\"{'price': 180.25, 'volume': 1000000, 'pe_ratio': 65.4, 'market_cap': '700B'}\", call_id='call_SAXy1ebtA9mnaZo4ztpD2xHA')]\n", - "---------- financial_analyst ----------\n", - "Tool calls:\n", - "get_stock_data({\"symbol\":\"TSLA\"}) = {'price': 180.25, 'volume': 1000000, 'pe_ratio': 65.4, 'market_cap': '700B'}\n", - "---------- financial_analyst ----------\n", - "[FunctionCall(id='call_IsdcFUfBVmtcVzfSuwQpeAwl', arguments='{}', name='transfer_to_planner')]\n", - "[Prompt tokens: 199, Completion tokens: 337]\n", - "---------- financial_analyst ----------\n", - "[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_IsdcFUfBVmtcVzfSuwQpeAwl')]\n", - "---------- financial_analyst ----------\n", - "Transferred to planner, adopting the role of planner immediately.\n", - "---------- planner ----------\n", - "[FunctionCall(id='call_tN5goNFahrdcSfKnQqT0RONN', arguments='{}', name='transfer_to_news_analyst')]\n", - "[Prompt tokens: 291, Completion tokens: 14]\n", - "---------- planner ----------\n", - "[FunctionExecutionResult(content='Transferred to news_analyst, adopting the role of news_analyst immediately.', call_id='call_tN5goNFahrdcSfKnQqT0RONN')]\n", - "---------- planner ----------\n", - "Transferred to news_analyst, adopting the role of news_analyst immediately.\n", - "---------- news_analyst ----------\n", - "[FunctionCall(id='call_Owjw6ZbiPdJgNWMHWxhCKgsp', arguments='{\"query\":\"Tesla market news\"}', name='get_news')]\n", - "[Prompt tokens: 235, Completion tokens: 16]\n", - "---------- news_analyst ----------\n", - "[FunctionExecutionResult(content='[{\\'title\\': \\'Tesla Expands Cybertruck Production\\', \\'date\\': \\'2024-03-20\\', \\'summary\\': \\'Tesla ramps up Cybertruck manufacturing capacity at Gigafactory Texas, aiming to meet strong demand.\\'}, {\\'title\\': \\'Tesla FSD Beta Shows Promise\\', \\'date\\': \\'2024-03-19\\', \\'summary\\': \\'Latest Full Self-Driving beta demonstrates significant improvements in urban navigation and safety features.\\'}, {\\'title\\': \\'Model Y Dominates Global EV Sales\\', \\'date\\': \\'2024-03-18\\', \\'summary\\': \"Tesla\\'s Model Y becomes best-selling electric vehicle worldwide, capturing significant market share.\"}]', call_id='call_Owjw6ZbiPdJgNWMHWxhCKgsp')]\n", - "---------- news_analyst ----------\n", - "Tool calls:\n", - "get_news({\"query\":\"Tesla market news\"}) = [{'title': 'Tesla Expands Cybertruck Production', 'date': '2024-03-20', 'summary': 'Tesla ramps up Cybertruck manufacturing capacity at Gigafactory Texas, aiming to meet strong demand.'}, {'title': 'Tesla FSD Beta Shows Promise', 'date': '2024-03-19', 'summary': 'Latest Full Self-Driving beta demonstrates significant improvements in urban navigation and safety features.'}, {'title': 'Model Y Dominates Global EV Sales', 'date': '2024-03-18', 'summary': \"Tesla's Model Y becomes best-selling electric vehicle worldwide, capturing significant market share.\"}]\n", - "---------- news_analyst ----------\n", - "Here are some of the key market insights regarding Tesla (TSLA):\n", - "\n", - "1. **Expansion in Cybertruck Production**: Tesla has increased its Cybertruck production capacity at the Gigafactory in Texas to meet the high demand. This move might positively impact Tesla's revenues if the demand for the Cybertruck continues to grow.\n", - "\n", - "2. **Advancements in Full Self-Driving (FSD) Technology**: The recent beta release of Tesla's Full Self-Driving software shows significant advancements, particularly in urban navigation and safety. Progress in this area could enhance Tesla's competitive edge in the autonomous driving sector.\n", - "\n", - "3. **Dominance of Model Y in EV Sales**: Tesla's Model Y has become the best-selling electric vehicle globally, capturing a substantial market share. Such strong sales performance reinforces Tesla's leadership in the electric vehicle market.\n", - "\n", - "These developments reflect Tesla's ongoing innovation and ability to capture market demand, which could positively influence its stock performance and market position. \n", - "\n", - "I will now hand off back to the planner.\n", - "[Prompt tokens: 398, Completion tokens: 203]\n", - "---------- news_analyst ----------\n", - "[FunctionCall(id='call_pn7y6PKsBspWA17uOh3AKNMT', arguments='{}', name='transfer_to_planner')]\n", - "[Prompt tokens: 609, Completion tokens: 12]\n", - "---------- news_analyst ----------\n", - "[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_pn7y6PKsBspWA17uOh3AKNMT')]\n", - "---------- news_analyst ----------\n", - "Transferred to planner, adopting the role of planner immediately.\n", - "---------- planner ----------\n", - "[FunctionCall(id='call_MmXyWuD2uJT64ZdVI5NfhYdX', arguments='{}', name='transfer_to_writer')]\n", - "[Prompt tokens: 722, Completion tokens: 11]\n", - "---------- planner ----------\n", - "[FunctionExecutionResult(content='Transferred to writer, adopting the role of writer immediately.', call_id='call_MmXyWuD2uJT64ZdVI5NfhYdX')]\n", - "---------- planner ----------\n", - "Transferred to writer, adopting the role of writer immediately.\n", - "---------- writer ----------\n", - "[FunctionCall(id='call_Pdgu39O6GMYplBiB8jp3uyN3', arguments='{}', name='transfer_to_planner')]\n", - "[Prompt tokens: 599, Completion tokens: 323]\n", - "---------- writer ----------\n", - "[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_Pdgu39O6GMYplBiB8jp3uyN3')]\n", - "---------- writer ----------\n", - "Transferred to planner, adopting the role of planner immediately.\n", - "---------- planner ----------\n", - "TERMINATE\n", - "[Prompt tokens: 772, Completion tokens: 4]\n", - "---------- Summary ----------\n", - "Number of messages: 27\n", - "Finish reason: Text 'TERMINATE' mentioned\n", - "Total prompt tokens: 4130\n", - "Total completion tokens: 1102\n", - "Duration: 17.74 seconds\n" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Swarm\n", + "\n", + "{py:class}`~autogen_agentchat.teams.Swarm` implements a team in which agents can hand off \n", + "task to other agents based on their capabilities. \n", + "It is a multi-agent design pattern first introduced by OpenAI in \n", + "[an experimental project](https://github.com/openai/swarm).\n", + "The key idea is to let agent delegate tasks to other agents using a special tool call, while\n", + "all agents share the same message context.\n", + "This enables agents to make local decisions about task planning, rather than\n", + "relying on a central orchestrator such as in {py:class}`~autogen_agentchat.teams.SelectorGroupChat`.\n", + "\n", + "```{note}\n", + "{py:class}`~autogen_agentchat.teams.Swarm` is a high-level API. If you need more\n", + "control and customization that is not supported by this API, you can take a look\n", + "at the [Handoff Pattern](../../core-user-guide/design-patterns/handoffs.ipynb)\n", + "in the Core API documentation and implement your own version of the Swarm pattern.\n", + "```\n", + "\n", + "## How Does It Work?\n", + "\n", + "At its core, the {py:class}`~autogen_agentchat.teams.Swarm` team is a group chat\n", + "where agents take turn to generate a response. \n", + "Similar to {py:class}`~autogen_agentchat.teams.SelectorGroupChat`\n", + "and {py:class}`~autogen_agentchat.teams.RoundRobinGroupChat`, participant agents\n", + "broadcast their responses so all agents share the same mesasge context.\n", + "\n", + "Different from the other two group chat teams, at each turn,\n", + "**the speaker agent is selected based on the most recent\n", + "{py:class}`~autogen_agentchat.messages.HandoffMessage` message in the context.**\n", + "This naturally requires each agent in the team to be able to generate\n", + "{py:class}`~autogen_agentchat.messages.HandoffMessage` to signal\n", + "which other agents that it hands off to.\n", + "\n", + "For {py:class}`~autogen_agentchat.agents.AssistantAgent`, you can set the\n", + "`handoffs` argument to specify which agents it can hand off to. You can\n", + "use {py:class}`~autogen_agentchat.base.Handoff` to customize the message\n", + "content and handoff behavior.\n", + "\n", + "The overall process can be summarized as follows:\n", + "\n", + "1. Each agent has the ability to generate {py:class}`~autogen_agentchat.messages.HandoffMessage`\n", + " to signal which other agents it can hand off to. For {py:class}`~autogen_agentchat.agents.AssistantAgent`, this means setting the `handoffs` argument.\n", + "2. When the team starts on a task, the first speaker agents operate on the task and make locallized decision about whether to hand off and to whom.\n", + "3. When an agent generates a {py:class}`~autogen_agentchat.messages.HandoffMessage`, the receiving agent takes over the task with the same message context.\n", + "4. The process continues until a termination condition is met.\n", + "\n", + "In this section, we will show you two examples of how to use the {py:class}`~autogen_agentchat.teams.Swarm` team:\n", + "\n", + "1. A customer support team with human-in-the-loop handoff.\n", + "2. An automonous team for content generation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Customer Support Example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Customer Support](swarm_customer_support.svg)\n", + "\n", + "This system implements a flights refund scenario with two agents:\n", + "\n", + "- **Travel Agent**: Handles general travel and refund coordination.\n", + "- **Flights Refunder**: Specializes in processing flight refunds with the `refund_flight` tool.\n", + "\n", + "Additionally, we let the user interact with the agents, when agents handoff to `\"user\"`.\n", + "\n", + "#### Workflow\n", + "1. The **Travel Agent** initiates the conversation and evaluates the user's request.\n", + "2. Based on the request:\n", + " - For refund-related tasks, the Travel Agent hands off to the **Flights Refunder**.\n", + " - For information needed from the customer, either agent can hand off to the `\"user\"`.\n", + "3. The **Flights Refunder** processes refunds using the `refund_flight` tool when appropriate.\n", + "4. If an agent hands off to the `\"user\"`, the team execution will stop and wait for the user to input a response.\n", + "5. When the user provides input, it's sent back to the team as a {py:class}`~autogen_agentchat.messages.HandaffMessage`. This message is directed to the agent that originally requested user input.\n", + "6. The process continues until the Travel Agent determines the task is complete and terminates the workflow." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Any, Dict, List\n", + "\n", + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.conditions import HandoffTermination, TextMentionTermination\n", + "from autogen_agentchat.messages import HandoffMessage\n", + "from autogen_agentchat.teams import Swarm\n", + "from autogen_agentchat.ui import Console\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tools" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def refund_flight(flight_id: str) -> str:\n", + " \"\"\"Refund a flight\"\"\"\n", + " return f\"Flight {flight_id} refunded\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Agents" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "model_client = OpenAIChatCompletionClient(\n", + " model=\"gpt-4o\",\n", + " # api_key=\"YOUR_API_KEY\",\n", + ")\n", + "\n", + "travel_agent = AssistantAgent(\n", + " \"travel_agent\",\n", + " model_client=model_client,\n", + " handoffs=[\"flights_refunder\", \"user\"],\n", + " system_message=\"\"\"You are a travel agent.\n", + " The flights_refunder is in charge of refunding flights.\n", + " If you need information from the user, you must first send your message, then you can handoff to the user.\n", + " Use TERMINATE when the travel planning is complete.\"\"\",\n", + ")\n", + "\n", + "flights_refunder = AssistantAgent(\n", + " \"flights_refunder\",\n", + " model_client=model_client,\n", + " handoffs=[\"travel_agent\", \"user\"],\n", + " tools=[refund_flight],\n", + " system_message=\"\"\"You are an agent specialized in refunding flights.\n", + " You only need flight reference numbers to refund a flight.\n", + " You have the ability to refund a flight using the refund_flight tool.\n", + " If you need information from the user, you must first send your message, then you can handoff to the user.\n", + " When the transaction is complete, handoff to the travel agent to finalize.\"\"\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "termination = HandoffTermination(target=\"user\") | TextMentionTermination(\"TERMINATE\")\n", + "team = Swarm([travel_agent, flights_refunder], termination_condition=termination)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "I need to refund my flight.\n", + "---------- travel_agent ----------\n", + "[FunctionCall(id='call_ZQ2rGjq4Z29pd0yP2sNcuyd2', arguments='{}', name='transfer_to_flights_refunder')]\n", + "[Prompt tokens: 119, Completion tokens: 14]\n", + "---------- travel_agent ----------\n", + "[FunctionExecutionResult(content='Transferred to flights_refunder, adopting the role of flights_refunder immediately.', call_id='call_ZQ2rGjq4Z29pd0yP2sNcuyd2')]\n", + "---------- travel_agent ----------\n", + "Transferred to flights_refunder, adopting the role of flights_refunder immediately.\n", + "---------- flights_refunder ----------\n", + "Could you please provide me with the flight reference number so I can process the refund for you?\n", + "[Prompt tokens: 191, Completion tokens: 20]\n", + "---------- flights_refunder ----------\n", + "[FunctionCall(id='call_1iRfzNpxTJhRTW2ww9aQJ8sK', arguments='{}', name='transfer_to_user')]\n", + "[Prompt tokens: 219, Completion tokens: 11]\n", + "---------- flights_refunder ----------\n", + "[FunctionExecutionResult(content='Transferred to user, adopting the role of user immediately.', call_id='call_1iRfzNpxTJhRTW2ww9aQJ8sK')]\n", + "---------- flights_refunder ----------\n", + "Transferred to user, adopting the role of user immediately.\n", + "---------- Summary ----------\n", + "Number of messages: 8\n", + "Finish reason: Handoff to user from flights_refunder detected.\n", + "Total prompt tokens: 529\n", + "Total completion tokens: 45\n", + "Duration: 2.05 seconds\n", + "---------- user ----------\n", + "Sure, it's 507811\n", + "---------- flights_refunder ----------\n", + "[FunctionCall(id='call_UKCsoEBdflkvpuT9Bi2xlvTd', arguments='{\"flight_id\":\"507811\"}', name='refund_flight')]\n", + "[Prompt tokens: 266, Completion tokens: 18]\n", + "---------- flights_refunder ----------\n", + "[FunctionExecutionResult(content='Flight 507811 refunded', call_id='call_UKCsoEBdflkvpuT9Bi2xlvTd')]\n", + "---------- flights_refunder ----------\n", + "Tool calls:\n", + "refund_flight({\"flight_id\":\"507811\"}) = Flight 507811 refunded\n", + "---------- flights_refunder ----------\n", + "[FunctionCall(id='call_MQ2CXR8UhVtjNc6jG3wSQp2W', arguments='{}', name='transfer_to_travel_agent')]\n", + "[Prompt tokens: 303, Completion tokens: 13]\n", + "---------- flights_refunder ----------\n", + "[FunctionExecutionResult(content='Transferred to travel_agent, adopting the role of travel_agent immediately.', call_id='call_MQ2CXR8UhVtjNc6jG3wSQp2W')]\n", + "---------- flights_refunder ----------\n", + "Transferred to travel_agent, adopting the role of travel_agent immediately.\n", + "---------- travel_agent ----------\n", + "Your flight with reference number 507811 has been successfully refunded. If you need anything else, feel free to let me know. Safe travels! TERMINATE\n", + "[Prompt tokens: 272, Completion tokens: 32]\n", + "---------- Summary ----------\n", + "Number of messages: 8\n", + "Finish reason: Text 'TERMINATE' mentioned\n", + "Total prompt tokens: 841\n", + "Total completion tokens: 63\n", + "Duration: 1.64 seconds\n" + ] + } + ], + "source": [ + "task = \"I need to refund my flight.\"\n", + "\n", + "\n", + "async def run_team_stream() -> None:\n", + " task_result = await Console(team.run_stream(task=task))\n", + " last_message = task_result.messages[-1]\n", + "\n", + " while isinstance(last_message, HandoffMessage) and last_message.target == \"user\":\n", + " user_message = input(\"User: \")\n", + "\n", + " task_result = await Console(\n", + " team.run_stream(task=HandoffMessage(source=\"user\", target=last_message.source, content=user_message))\n", + " )\n", + " last_message = task_result.messages[-1]\n", + "\n", + "\n", + "await run_team_stream()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Stock Research Example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Stock Research](swarm_stock_research.svg)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This system is designed to perform stock research tasks by leveraging four agents:\n", + "\n", + "- **Planner**: The central coordinator that delegates specific tasks to specialized agents based on their expertise. The planner ensures that each agent is utilized efficiently and oversees the overall workflow.\n", + "- **Financial Analyst**: A specialized agent responsible for analyzing financial metrics and stock data using tools such as `get_stock_data`.\n", + "- **News Analyst**: An agent focused on gathering and summarizing recent news articles relevant to the stock, using tools such as `get_news`.\n", + "- **Writer**: An agent tasked with compiling the findings from the stock and news analysis into a cohesive final report.\n", + "\n", + "#### Workflow\n", + "1. The **Planner** initiates the research process by delegating tasks to the appropriate agents in a step-by-step manner.\n", + "2. Each agent performs its task independently and appends their work to the shared **message thread/history**. Rather than directly returning results to the planner, all agents contribute to and read from this shared message history. When agents generate their work using the LLM, they have access to this shared message history, which provides context and helps track the overall progress of the task.\n", + "3. Once an agent completes its task, it hands off control back to the planner.\n", + "4. The process continues until the planner determines that all necessary tasks have been completed and decides to terminate the workflow." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tools" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "async def get_stock_data(symbol: str) -> Dict[str, Any]:\n", + " \"\"\"Get stock market data for a given symbol\"\"\"\n", + " return {\"price\": 180.25, \"volume\": 1000000, \"pe_ratio\": 65.4, \"market_cap\": \"700B\"}\n", + "\n", + "\n", + "async def get_news(query: str) -> List[Dict[str, str]]:\n", + " \"\"\"Get recent news articles about a company\"\"\"\n", + " return [\n", + " {\n", + " \"title\": \"Tesla Expands Cybertruck Production\",\n", + " \"date\": \"2024-03-20\",\n", + " \"summary\": \"Tesla ramps up Cybertruck manufacturing capacity at Gigafactory Texas, aiming to meet strong demand.\",\n", + " },\n", + " {\n", + " \"title\": \"Tesla FSD Beta Shows Promise\",\n", + " \"date\": \"2024-03-19\",\n", + " \"summary\": \"Latest Full Self-Driving beta demonstrates significant improvements in urban navigation and safety features.\",\n", + " },\n", + " {\n", + " \"title\": \"Model Y Dominates Global EV Sales\",\n", + " \"date\": \"2024-03-18\",\n", + " \"summary\": \"Tesla's Model Y becomes best-selling electric vehicle worldwide, capturing significant market share.\",\n", + " },\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "model_client = OpenAIChatCompletionClient(\n", + " model=\"gpt-4o\",\n", + " # api_key=\"YOUR_API_KEY\",\n", + ")\n", + "\n", + "planner = AssistantAgent(\n", + " \"planner\",\n", + " model_client=model_client,\n", + " handoffs=[\"financial_analyst\", \"news_analyst\", \"writer\"],\n", + " system_message=\"\"\"You are a research planning coordinator.\n", + " Coordinate market research by delegating to specialized agents:\n", + " - Financial Analyst: For stock data analysis\n", + " - News Analyst: For news gathering and analysis\n", + " - Writer: For compiling final report\n", + " Always send your plan first, then handoff to appropriate agent.\n", + " Always handoff to a single agent at a time.\n", + " Use TERMINATE when research is complete.\"\"\",\n", + ")\n", + "\n", + "financial_analyst = AssistantAgent(\n", + " \"financial_analyst\",\n", + " model_client=model_client,\n", + " handoffs=[\"planner\"],\n", + " tools=[get_stock_data],\n", + " system_message=\"\"\"You are a financial analyst.\n", + " Analyze stock market data using the get_stock_data tool.\n", + " Provide insights on financial metrics.\n", + " Always handoff back to planner when analysis is complete.\"\"\",\n", + ")\n", + "\n", + "news_analyst = AssistantAgent(\n", + " \"news_analyst\",\n", + " model_client=model_client,\n", + " handoffs=[\"planner\"],\n", + " tools=[get_news],\n", + " system_message=\"\"\"You are a news analyst.\n", + " Gather and analyze relevant news using the get_news tool.\n", + " Summarize key market insights from news.\n", + " Always handoff back to planner when analysis is complete.\"\"\",\n", + ")\n", + "\n", + "writer = AssistantAgent(\n", + " \"writer\",\n", + " model_client=model_client,\n", + " handoffs=[\"planner\"],\n", + " system_message=\"\"\"You are a financial report writer.\n", + " Compile research findings into clear, concise reports.\n", + " Always handoff back to planner when writing is complete.\"\"\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Conduct market research for TSLA stock\n", + "---------- planner ----------\n", + "[FunctionCall(id='call_BX5QaRuhmB8CxTsBlqCUIXPb', arguments='{}', name='transfer_to_financial_analyst')]\n", + "[Prompt tokens: 169, Completion tokens: 166]\n", + "---------- planner ----------\n", + "[FunctionExecutionResult(content='Transferred to financial_analyst, adopting the role of financial_analyst immediately.', call_id='call_BX5QaRuhmB8CxTsBlqCUIXPb')]\n", + "---------- planner ----------\n", + "Transferred to financial_analyst, adopting the role of financial_analyst immediately.\n", + "---------- financial_analyst ----------\n", + "[FunctionCall(id='call_SAXy1ebtA9mnaZo4ztpD2xHA', arguments='{\"symbol\":\"TSLA\"}', name='get_stock_data')]\n", + "[Prompt tokens: 136, Completion tokens: 16]\n", + "---------- financial_analyst ----------\n", + "[FunctionExecutionResult(content=\"{'price': 180.25, 'volume': 1000000, 'pe_ratio': 65.4, 'market_cap': '700B'}\", call_id='call_SAXy1ebtA9mnaZo4ztpD2xHA')]\n", + "---------- financial_analyst ----------\n", + "Tool calls:\n", + "get_stock_data({\"symbol\":\"TSLA\"}) = {'price': 180.25, 'volume': 1000000, 'pe_ratio': 65.4, 'market_cap': '700B'}\n", + "---------- financial_analyst ----------\n", + "[FunctionCall(id='call_IsdcFUfBVmtcVzfSuwQpeAwl', arguments='{}', name='transfer_to_planner')]\n", + "[Prompt tokens: 199, Completion tokens: 337]\n", + "---------- financial_analyst ----------\n", + "[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_IsdcFUfBVmtcVzfSuwQpeAwl')]\n", + "---------- financial_analyst ----------\n", + "Transferred to planner, adopting the role of planner immediately.\n", + "---------- planner ----------\n", + "[FunctionCall(id='call_tN5goNFahrdcSfKnQqT0RONN', arguments='{}', name='transfer_to_news_analyst')]\n", + "[Prompt tokens: 291, Completion tokens: 14]\n", + "---------- planner ----------\n", + "[FunctionExecutionResult(content='Transferred to news_analyst, adopting the role of news_analyst immediately.', call_id='call_tN5goNFahrdcSfKnQqT0RONN')]\n", + "---------- planner ----------\n", + "Transferred to news_analyst, adopting the role of news_analyst immediately.\n", + "---------- news_analyst ----------\n", + "[FunctionCall(id='call_Owjw6ZbiPdJgNWMHWxhCKgsp', arguments='{\"query\":\"Tesla market news\"}', name='get_news')]\n", + "[Prompt tokens: 235, Completion tokens: 16]\n", + "---------- news_analyst ----------\n", + "[FunctionExecutionResult(content='[{\\'title\\': \\'Tesla Expands Cybertruck Production\\', \\'date\\': \\'2024-03-20\\', \\'summary\\': \\'Tesla ramps up Cybertruck manufacturing capacity at Gigafactory Texas, aiming to meet strong demand.\\'}, {\\'title\\': \\'Tesla FSD Beta Shows Promise\\', \\'date\\': \\'2024-03-19\\', \\'summary\\': \\'Latest Full Self-Driving beta demonstrates significant improvements in urban navigation and safety features.\\'}, {\\'title\\': \\'Model Y Dominates Global EV Sales\\', \\'date\\': \\'2024-03-18\\', \\'summary\\': \"Tesla\\'s Model Y becomes best-selling electric vehicle worldwide, capturing significant market share.\"}]', call_id='call_Owjw6ZbiPdJgNWMHWxhCKgsp')]\n", + "---------- news_analyst ----------\n", + "Tool calls:\n", + "get_news({\"query\":\"Tesla market news\"}) = [{'title': 'Tesla Expands Cybertruck Production', 'date': '2024-03-20', 'summary': 'Tesla ramps up Cybertruck manufacturing capacity at Gigafactory Texas, aiming to meet strong demand.'}, {'title': 'Tesla FSD Beta Shows Promise', 'date': '2024-03-19', 'summary': 'Latest Full Self-Driving beta demonstrates significant improvements in urban navigation and safety features.'}, {'title': 'Model Y Dominates Global EV Sales', 'date': '2024-03-18', 'summary': \"Tesla's Model Y becomes best-selling electric vehicle worldwide, capturing significant market share.\"}]\n", + "---------- news_analyst ----------\n", + "Here are some of the key market insights regarding Tesla (TSLA):\n", + "\n", + "1. **Expansion in Cybertruck Production**: Tesla has increased its Cybertruck production capacity at the Gigafactory in Texas to meet the high demand. This move might positively impact Tesla's revenues if the demand for the Cybertruck continues to grow.\n", + "\n", + "2. **Advancements in Full Self-Driving (FSD) Technology**: The recent beta release of Tesla's Full Self-Driving software shows significant advancements, particularly in urban navigation and safety. Progress in this area could enhance Tesla's competitive edge in the autonomous driving sector.\n", + "\n", + "3. **Dominance of Model Y in EV Sales**: Tesla's Model Y has become the best-selling electric vehicle globally, capturing a substantial market share. Such strong sales performance reinforces Tesla's leadership in the electric vehicle market.\n", + "\n", + "These developments reflect Tesla's ongoing innovation and ability to capture market demand, which could positively influence its stock performance and market position. \n", + "\n", + "I will now hand off back to the planner.\n", + "[Prompt tokens: 398, Completion tokens: 203]\n", + "---------- news_analyst ----------\n", + "[FunctionCall(id='call_pn7y6PKsBspWA17uOh3AKNMT', arguments='{}', name='transfer_to_planner')]\n", + "[Prompt tokens: 609, Completion tokens: 12]\n", + "---------- news_analyst ----------\n", + "[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_pn7y6PKsBspWA17uOh3AKNMT')]\n", + "---------- news_analyst ----------\n", + "Transferred to planner, adopting the role of planner immediately.\n", + "---------- planner ----------\n", + "[FunctionCall(id='call_MmXyWuD2uJT64ZdVI5NfhYdX', arguments='{}', name='transfer_to_writer')]\n", + "[Prompt tokens: 722, Completion tokens: 11]\n", + "---------- planner ----------\n", + "[FunctionExecutionResult(content='Transferred to writer, adopting the role of writer immediately.', call_id='call_MmXyWuD2uJT64ZdVI5NfhYdX')]\n", + "---------- planner ----------\n", + "Transferred to writer, adopting the role of writer immediately.\n", + "---------- writer ----------\n", + "[FunctionCall(id='call_Pdgu39O6GMYplBiB8jp3uyN3', arguments='{}', name='transfer_to_planner')]\n", + "[Prompt tokens: 599, Completion tokens: 323]\n", + "---------- writer ----------\n", + "[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_Pdgu39O6GMYplBiB8jp3uyN3')]\n", + "---------- writer ----------\n", + "Transferred to planner, adopting the role of planner immediately.\n", + "---------- planner ----------\n", + "TERMINATE\n", + "[Prompt tokens: 772, Completion tokens: 4]\n", + "---------- Summary ----------\n", + "Number of messages: 27\n", + "Finish reason: Text 'TERMINATE' mentioned\n", + "Total prompt tokens: 4130\n", + "Total completion tokens: 1102\n", + "Duration: 17.74 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Conduct market research for TSLA stock', type='TextMessage'), ToolCallRequestEvent(source='planner', models_usage=RequestUsage(prompt_tokens=169, completion_tokens=166), content=[FunctionCall(id='call_BX5QaRuhmB8CxTsBlqCUIXPb', arguments='{}', name='transfer_to_financial_analyst')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='planner', models_usage=None, content=[FunctionExecutionResult(content='Transferred to financial_analyst, adopting the role of financial_analyst immediately.', call_id='call_BX5QaRuhmB8CxTsBlqCUIXPb')], type='ToolCallExecutionEvent'), HandoffMessage(source='planner', models_usage=None, target='financial_analyst', content='Transferred to financial_analyst, adopting the role of financial_analyst immediately.', type='HandoffMessage'), ToolCallRequestEvent(source='financial_analyst', models_usage=RequestUsage(prompt_tokens=136, completion_tokens=16), content=[FunctionCall(id='call_SAXy1ebtA9mnaZo4ztpD2xHA', arguments='{\"symbol\":\"TSLA\"}', name='get_stock_data')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='financial_analyst', models_usage=None, content=[FunctionExecutionResult(content=\"{'price': 180.25, 'volume': 1000000, 'pe_ratio': 65.4, 'market_cap': '700B'}\", call_id='call_SAXy1ebtA9mnaZo4ztpD2xHA')], type='ToolCallExecutionEvent'), TextMessage(source='financial_analyst', models_usage=None, content='Tool calls:\\nget_stock_data({\"symbol\":\"TSLA\"}) = {\\'price\\': 180.25, \\'volume\\': 1000000, \\'pe_ratio\\': 65.4, \\'market_cap\\': \\'700B\\'}', type='TextMessage'), ToolCallRequestEvent(source='financial_analyst', models_usage=RequestUsage(prompt_tokens=199, completion_tokens=337), content=[FunctionCall(id='call_IsdcFUfBVmtcVzfSuwQpeAwl', arguments='{}', name='transfer_to_planner')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='financial_analyst', models_usage=None, content=[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_IsdcFUfBVmtcVzfSuwQpeAwl')], type='ToolCallExecutionEvent'), HandoffMessage(source='financial_analyst', models_usage=None, target='planner', content='Transferred to planner, adopting the role of planner immediately.', type='HandoffMessage'), ToolCallRequestEvent(source='planner', models_usage=RequestUsage(prompt_tokens=291, completion_tokens=14), content=[FunctionCall(id='call_tN5goNFahrdcSfKnQqT0RONN', arguments='{}', name='transfer_to_news_analyst')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='planner', models_usage=None, content=[FunctionExecutionResult(content='Transferred to news_analyst, adopting the role of news_analyst immediately.', call_id='call_tN5goNFahrdcSfKnQqT0RONN')], type='ToolCallExecutionEvent'), HandoffMessage(source='planner', models_usage=None, target='news_analyst', content='Transferred to news_analyst, adopting the role of news_analyst immediately.', type='HandoffMessage'), ToolCallRequestEvent(source='news_analyst', models_usage=RequestUsage(prompt_tokens=235, completion_tokens=16), content=[FunctionCall(id='call_Owjw6ZbiPdJgNWMHWxhCKgsp', arguments='{\"query\":\"Tesla market news\"}', name='get_news')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='news_analyst', models_usage=None, content=[FunctionExecutionResult(content='[{\\'title\\': \\'Tesla Expands Cybertruck Production\\', \\'date\\': \\'2024-03-20\\', \\'summary\\': \\'Tesla ramps up Cybertruck manufacturing capacity at Gigafactory Texas, aiming to meet strong demand.\\'}, {\\'title\\': \\'Tesla FSD Beta Shows Promise\\', \\'date\\': \\'2024-03-19\\', \\'summary\\': \\'Latest Full Self-Driving beta demonstrates significant improvements in urban navigation and safety features.\\'}, {\\'title\\': \\'Model Y Dominates Global EV Sales\\', \\'date\\': \\'2024-03-18\\', \\'summary\\': \"Tesla\\'s Model Y becomes best-selling electric vehicle worldwide, capturing significant market share.\"}]', call_id='call_Owjw6ZbiPdJgNWMHWxhCKgsp')], type='ToolCallExecutionEvent'), TextMessage(source='news_analyst', models_usage=None, content='Tool calls:\\nget_news({\"query\":\"Tesla market news\"}) = [{\\'title\\': \\'Tesla Expands Cybertruck Production\\', \\'date\\': \\'2024-03-20\\', \\'summary\\': \\'Tesla ramps up Cybertruck manufacturing capacity at Gigafactory Texas, aiming to meet strong demand.\\'}, {\\'title\\': \\'Tesla FSD Beta Shows Promise\\', \\'date\\': \\'2024-03-19\\', \\'summary\\': \\'Latest Full Self-Driving beta demonstrates significant improvements in urban navigation and safety features.\\'}, {\\'title\\': \\'Model Y Dominates Global EV Sales\\', \\'date\\': \\'2024-03-18\\', \\'summary\\': \"Tesla\\'s Model Y becomes best-selling electric vehicle worldwide, capturing significant market share.\"}]', type='TextMessage'), TextMessage(source='news_analyst', models_usage=RequestUsage(prompt_tokens=398, completion_tokens=203), content=\"Here are some of the key market insights regarding Tesla (TSLA):\\n\\n1. **Expansion in Cybertruck Production**: Tesla has increased its Cybertruck production capacity at the Gigafactory in Texas to meet the high demand. This move might positively impact Tesla's revenues if the demand for the Cybertruck continues to grow.\\n\\n2. **Advancements in Full Self-Driving (FSD) Technology**: The recent beta release of Tesla's Full Self-Driving software shows significant advancements, particularly in urban navigation and safety. Progress in this area could enhance Tesla's competitive edge in the autonomous driving sector.\\n\\n3. **Dominance of Model Y in EV Sales**: Tesla's Model Y has become the best-selling electric vehicle globally, capturing a substantial market share. Such strong sales performance reinforces Tesla's leadership in the electric vehicle market.\\n\\nThese developments reflect Tesla's ongoing innovation and ability to capture market demand, which could positively influence its stock performance and market position. \\n\\nI will now hand off back to the planner.\", type='TextMessage'), ToolCallRequestEvent(source='news_analyst', models_usage=RequestUsage(prompt_tokens=609, completion_tokens=12), content=[FunctionCall(id='call_pn7y6PKsBspWA17uOh3AKNMT', arguments='{}', name='transfer_to_planner')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='news_analyst', models_usage=None, content=[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_pn7y6PKsBspWA17uOh3AKNMT')], type='ToolCallExecutionEvent'), HandoffMessage(source='news_analyst', models_usage=None, target='planner', content='Transferred to planner, adopting the role of planner immediately.', type='HandoffMessage'), ToolCallRequestEvent(source='planner', models_usage=RequestUsage(prompt_tokens=722, completion_tokens=11), content=[FunctionCall(id='call_MmXyWuD2uJT64ZdVI5NfhYdX', arguments='{}', name='transfer_to_writer')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='planner', models_usage=None, content=[FunctionExecutionResult(content='Transferred to writer, adopting the role of writer immediately.', call_id='call_MmXyWuD2uJT64ZdVI5NfhYdX')], type='ToolCallExecutionEvent'), HandoffMessage(source='planner', models_usage=None, target='writer', content='Transferred to writer, adopting the role of writer immediately.', type='HandoffMessage'), ToolCallRequestEvent(source='writer', models_usage=RequestUsage(prompt_tokens=599, completion_tokens=323), content=[FunctionCall(id='call_Pdgu39O6GMYplBiB8jp3uyN3', arguments='{}', name='transfer_to_planner')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='writer', models_usage=None, content=[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_Pdgu39O6GMYplBiB8jp3uyN3')], type='ToolCallExecutionEvent'), HandoffMessage(source='writer', models_usage=None, target='planner', content='Transferred to planner, adopting the role of planner immediately.', type='HandoffMessage'), TextMessage(source='planner', models_usage=RequestUsage(prompt_tokens=772, completion_tokens=4), content='TERMINATE', type='TextMessage')], stop_reason=\"Text 'TERMINATE' mentioned\")" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Define termination condition\n", + "text_termination = TextMentionTermination(\"TERMINATE\")\n", + "termination = text_termination\n", + "\n", + "research_team = Swarm(\n", + " participants=[planner, financial_analyst, news_analyst, writer], termination_condition=termination\n", + ")\n", + "\n", + "task = \"Conduct market research for TSLA stock\"\n", + "await Console(research_team.run_stream(task=task))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } }, - { - "data": { - "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Conduct market research for TSLA stock', type='TextMessage'), ToolCallMessage(source='planner', models_usage=RequestUsage(prompt_tokens=169, completion_tokens=166), content=[FunctionCall(id='call_BX5QaRuhmB8CxTsBlqCUIXPb', arguments='{}', name='transfer_to_financial_analyst')], type='ToolCallMessage'), ToolCallResultMessage(source='planner', models_usage=None, content=[FunctionExecutionResult(content='Transferred to financial_analyst, adopting the role of financial_analyst immediately.', call_id='call_BX5QaRuhmB8CxTsBlqCUIXPb')], type='ToolCallResultMessage'), HandoffMessage(source='planner', models_usage=None, target='financial_analyst', content='Transferred to financial_analyst, adopting the role of financial_analyst immediately.', type='HandoffMessage'), ToolCallMessage(source='financial_analyst', models_usage=RequestUsage(prompt_tokens=136, completion_tokens=16), content=[FunctionCall(id='call_SAXy1ebtA9mnaZo4ztpD2xHA', arguments='{\"symbol\":\"TSLA\"}', name='get_stock_data')], type='ToolCallMessage'), ToolCallResultMessage(source='financial_analyst', models_usage=None, content=[FunctionExecutionResult(content=\"{'price': 180.25, 'volume': 1000000, 'pe_ratio': 65.4, 'market_cap': '700B'}\", call_id='call_SAXy1ebtA9mnaZo4ztpD2xHA')], type='ToolCallResultMessage'), TextMessage(source='financial_analyst', models_usage=None, content='Tool calls:\\nget_stock_data({\"symbol\":\"TSLA\"}) = {\\'price\\': 180.25, \\'volume\\': 1000000, \\'pe_ratio\\': 65.4, \\'market_cap\\': \\'700B\\'}', type='TextMessage'), ToolCallMessage(source='financial_analyst', models_usage=RequestUsage(prompt_tokens=199, completion_tokens=337), content=[FunctionCall(id='call_IsdcFUfBVmtcVzfSuwQpeAwl', arguments='{}', name='transfer_to_planner')], type='ToolCallMessage'), ToolCallResultMessage(source='financial_analyst', models_usage=None, content=[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_IsdcFUfBVmtcVzfSuwQpeAwl')], type='ToolCallResultMessage'), HandoffMessage(source='financial_analyst', models_usage=None, target='planner', content='Transferred to planner, adopting the role of planner immediately.', type='HandoffMessage'), ToolCallMessage(source='planner', models_usage=RequestUsage(prompt_tokens=291, completion_tokens=14), content=[FunctionCall(id='call_tN5goNFahrdcSfKnQqT0RONN', arguments='{}', name='transfer_to_news_analyst')], type='ToolCallMessage'), ToolCallResultMessage(source='planner', models_usage=None, content=[FunctionExecutionResult(content='Transferred to news_analyst, adopting the role of news_analyst immediately.', call_id='call_tN5goNFahrdcSfKnQqT0RONN')], type='ToolCallResultMessage'), HandoffMessage(source='planner', models_usage=None, target='news_analyst', content='Transferred to news_analyst, adopting the role of news_analyst immediately.', type='HandoffMessage'), ToolCallMessage(source='news_analyst', models_usage=RequestUsage(prompt_tokens=235, completion_tokens=16), content=[FunctionCall(id='call_Owjw6ZbiPdJgNWMHWxhCKgsp', arguments='{\"query\":\"Tesla market news\"}', name='get_news')], type='ToolCallMessage'), ToolCallResultMessage(source='news_analyst', models_usage=None, content=[FunctionExecutionResult(content='[{\\'title\\': \\'Tesla Expands Cybertruck Production\\', \\'date\\': \\'2024-03-20\\', \\'summary\\': \\'Tesla ramps up Cybertruck manufacturing capacity at Gigafactory Texas, aiming to meet strong demand.\\'}, {\\'title\\': \\'Tesla FSD Beta Shows Promise\\', \\'date\\': \\'2024-03-19\\', \\'summary\\': \\'Latest Full Self-Driving beta demonstrates significant improvements in urban navigation and safety features.\\'}, {\\'title\\': \\'Model Y Dominates Global EV Sales\\', \\'date\\': \\'2024-03-18\\', \\'summary\\': \"Tesla\\'s Model Y becomes best-selling electric vehicle worldwide, capturing significant market share.\"}]', call_id='call_Owjw6ZbiPdJgNWMHWxhCKgsp')], type='ToolCallResultMessage'), TextMessage(source='news_analyst', models_usage=None, content='Tool calls:\\nget_news({\"query\":\"Tesla market news\"}) = [{\\'title\\': \\'Tesla Expands Cybertruck Production\\', \\'date\\': \\'2024-03-20\\', \\'summary\\': \\'Tesla ramps up Cybertruck manufacturing capacity at Gigafactory Texas, aiming to meet strong demand.\\'}, {\\'title\\': \\'Tesla FSD Beta Shows Promise\\', \\'date\\': \\'2024-03-19\\', \\'summary\\': \\'Latest Full Self-Driving beta demonstrates significant improvements in urban navigation and safety features.\\'}, {\\'title\\': \\'Model Y Dominates Global EV Sales\\', \\'date\\': \\'2024-03-18\\', \\'summary\\': \"Tesla\\'s Model Y becomes best-selling electric vehicle worldwide, capturing significant market share.\"}]', type='TextMessage'), TextMessage(source='news_analyst', models_usage=RequestUsage(prompt_tokens=398, completion_tokens=203), content=\"Here are some of the key market insights regarding Tesla (TSLA):\\n\\n1. **Expansion in Cybertruck Production**: Tesla has increased its Cybertruck production capacity at the Gigafactory in Texas to meet the high demand. This move might positively impact Tesla's revenues if the demand for the Cybertruck continues to grow.\\n\\n2. **Advancements in Full Self-Driving (FSD) Technology**: The recent beta release of Tesla's Full Self-Driving software shows significant advancements, particularly in urban navigation and safety. Progress in this area could enhance Tesla's competitive edge in the autonomous driving sector.\\n\\n3. **Dominance of Model Y in EV Sales**: Tesla's Model Y has become the best-selling electric vehicle globally, capturing a substantial market share. Such strong sales performance reinforces Tesla's leadership in the electric vehicle market.\\n\\nThese developments reflect Tesla's ongoing innovation and ability to capture market demand, which could positively influence its stock performance and market position. \\n\\nI will now hand off back to the planner.\", type='TextMessage'), ToolCallMessage(source='news_analyst', models_usage=RequestUsage(prompt_tokens=609, completion_tokens=12), content=[FunctionCall(id='call_pn7y6PKsBspWA17uOh3AKNMT', arguments='{}', name='transfer_to_planner')], type='ToolCallMessage'), ToolCallResultMessage(source='news_analyst', models_usage=None, content=[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_pn7y6PKsBspWA17uOh3AKNMT')], type='ToolCallResultMessage'), HandoffMessage(source='news_analyst', models_usage=None, target='planner', content='Transferred to planner, adopting the role of planner immediately.', type='HandoffMessage'), ToolCallMessage(source='planner', models_usage=RequestUsage(prompt_tokens=722, completion_tokens=11), content=[FunctionCall(id='call_MmXyWuD2uJT64ZdVI5NfhYdX', arguments='{}', name='transfer_to_writer')], type='ToolCallMessage'), ToolCallResultMessage(source='planner', models_usage=None, content=[FunctionExecutionResult(content='Transferred to writer, adopting the role of writer immediately.', call_id='call_MmXyWuD2uJT64ZdVI5NfhYdX')], type='ToolCallResultMessage'), HandoffMessage(source='planner', models_usage=None, target='writer', content='Transferred to writer, adopting the role of writer immediately.', type='HandoffMessage'), ToolCallMessage(source='writer', models_usage=RequestUsage(prompt_tokens=599, completion_tokens=323), content=[FunctionCall(id='call_Pdgu39O6GMYplBiB8jp3uyN3', arguments='{}', name='transfer_to_planner')], type='ToolCallMessage'), ToolCallResultMessage(source='writer', models_usage=None, content=[FunctionExecutionResult(content='Transferred to planner, adopting the role of planner immediately.', call_id='call_Pdgu39O6GMYplBiB8jp3uyN3')], type='ToolCallResultMessage'), HandoffMessage(source='writer', models_usage=None, target='planner', content='Transferred to planner, adopting the role of planner immediately.', type='HandoffMessage'), TextMessage(source='planner', models_usage=RequestUsage(prompt_tokens=772, completion_tokens=4), content='TERMINATE', type='TextMessage')], stop_reason=\"Text 'TERMINATE' mentioned\")" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Define termination condition\n", - "text_termination = TextMentionTermination(\"TERMINATE\")\n", - "termination = text_termination\n", - "\n", - "research_team = Swarm(\n", - " participants=[planner, financial_analyst, news_analyst, writer], termination_condition=termination\n", - ")\n", - "\n", - "task = \"Conduct market research for TSLA stock\"\n", - "await Console(research_team.run_stream(task=task))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/teams.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/teams.ipynb index a97e521f8255..b62fbbe6ed2e 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/teams.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/teams.ipynb @@ -1,764 +1,764 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Teams\n", - "\n", - "In this section you'll learn how to create a _multi-agent team_ (or simply team) using AutoGen. A team is a group of agents that work together to achieve a common goal.\n", - "\n", - "We'll first show you how to create and run a team. We'll then explain how to observe the team's behavior, which is crucial for debugging and understanding the team's performance, and common operations to control the team's behavior.\n", - "\n", - "We'll start by focusing on a simple team with consisting of a single agent (the baseline case) and use a round robin strategy to select the agent to act. We'll then show how to create a team with multiple agents and how to implement a more sophisticated strategy to select the agent to act." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Creating a Team\n", - "\n", - "{py:class}`~autogen_agentchat.teams.RoundRobinGroupChat` is a simple yet effective team configuration where all agents share the same context and take turns responding in a round-robin fashion. Each agent, during its turn, broadcasts its response to all other agents, ensuring that the entire team maintains a consistent context.\n", - "\n", - "We will begin by creating a team with a single {py:class}`~autogen_agentchat.agents.AssistantAgent` and a {py:class}`~autogen_agentchat.conditions.TextMentionTermination` condition that stops the team when a specific word is detected in the agent's response.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_agentchat.agents import AssistantAgent\n", - "from autogen_agentchat.conditions import TextMentionTermination\n", - "from autogen_agentchat.teams import RoundRobinGroupChat\n", - "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", - "\n", - "# Create an OpenAI model client.\n", - "model_client = OpenAIChatCompletionClient(\n", - " model=\"gpt-4o-2024-08-06\",\n", - " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY env variable set.\n", - ")\n", - "\n", - "\n", - "# Define a tool that gets the weather for a city.\n", - "async def get_weather(city: str) -> str:\n", - " \"\"\"Get the weather for a city.\"\"\"\n", - " return f\"The weather in {city} is 72 degrees and Sunny.\"\n", - "\n", - "\n", - "# Create an assistant agent.\n", - "weather_agent = AssistantAgent(\n", - " \"assistant\",\n", - " model_client=model_client,\n", - " tools=[get_weather],\n", - " system_message=\"Respond 'TERMINATE' when task is complete.\",\n", - ")\n", - "\n", - "# Define a termination condition.\n", - "text_termination = TextMentionTermination(\"TERMINATE\")\n", - "\n", - "# Create a single-agent team.\n", - "single_agent_team = RoundRobinGroupChat([weather_agent], termination_condition=text_termination)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Running a Team\n", - "\n", - "Let's calls the {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run` method\n", - "to start the team with a task." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='What is the weather in New York?'), ToolCallMessage(source='assistant', models_usage=RequestUsage(prompt_tokens=70, completion_tokens=15), content=[FunctionCall(id='call_6qWxrK1VdEVSryXKyIVpwE0h', arguments='{\"city\":\"New York\"}', name='get_weather')]), ToolCallResultMessage(source='assistant', models_usage=None, content=[FunctionExecutionResult(content='The weather in New York is 72 degrees and Sunny.', call_id='call_6qWxrK1VdEVSryXKyIVpwE0h')]), TextMessage(source='assistant', models_usage=RequestUsage(prompt_tokens=96, completion_tokens=13), content='The weather in New York is 72 degrees and sunny.'), TextMessage(source='assistant', models_usage=RequestUsage(prompt_tokens=125, completion_tokens=4), content='TERMINATE')], stop_reason=\"Text 'TERMINATE' mentioned\")\n" - ] - } - ], - "source": [ - "async def run_team() -> None:\n", - " result = await single_agent_team.run(task=\"What is the weather in New York?\")\n", - " print(result)\n", - "\n", - "\n", - "# Use `asyncio.run(run_team())` when running in a script.\n", - "await run_team()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The team ran the same agent until the termination condition was met.\n", - "In this case, the termination condition was met when the word \"TERMINATE\" is detected in the\n", - "agent's response.\n", - "When the team stops, it returns a {py:class}`~autogen_agentchat.base.TaskResult` object with all the messages produced by the agents in the team." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Observability\n", - "\n", - "Similar to the agent's {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages_stream` method, you can stream the team's messages by calling the {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run_stream` method. This method returns a generator that yields messages produced by the agents in the team as they are generated, with the final item being the task result.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "source='user' models_usage=None content='What is the weather in New York?'\n", - "source='assistant' models_usage=RequestUsage(prompt_tokens=70, completion_tokens=15) content=[FunctionCall(id='call_FcJlzgDmPUgGSKLjeGajFZ7V', arguments='{\"city\":\"New York\"}', name='get_weather')]\n", - "source='assistant' models_usage=None content=[FunctionExecutionResult(content='The weather in New York is 72 degrees and Sunny.', call_id='call_FcJlzgDmPUgGSKLjeGajFZ7V')]\n", - "source='assistant' models_usage=RequestUsage(prompt_tokens=96, completion_tokens=14) content='The weather in New York is currently 72 degrees and sunny.'\n", - "source='assistant' models_usage=RequestUsage(prompt_tokens=126, completion_tokens=4) content='TERMINATE'\n", - "Stop Reason: Text 'TERMINATE' mentioned\n" - ] - } - ], - "source": [ - "from autogen_agentchat.base import TaskResult\n", - "\n", - "\n", - "async def run_team_stream() -> None:\n", - " async for message in single_agent_team.run_stream(task=\"What is the weather in New York?\"):\n", - " if isinstance(message, TaskResult):\n", - " print(\"Stop Reason:\", message.stop_reason)\n", - " else:\n", - " print(message)\n", - "\n", - "\n", - "# Use `asyncio.run(run_team_stream())` when running in a script.\n", - "await run_team_stream()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As demonstrated in the example above, you can determine the reason why the team stopped by checking the {py:attr}`~autogen_agentchat.base.TaskResult.stop_reason` attribute.\n", - "\n", - "The {py:meth}`~autogen_agentchat.ui.Console` method provides a convenient way to print messages to the console with proper formatting.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "What is the weather in Seattle?\n", - "---------- assistant ----------\n", - "[FunctionCall(id='call_QBqpeKQlczRYIlCIzKh43Kha', arguments='{\"city\":\"Seattle\"}', name='get_weather')]\n", - "[Prompt tokens: 69, Completion tokens: 14]\n", - "---------- assistant ----------\n", - "[FunctionExecutionResult(content='The weather in Seattle is 72 degrees and Sunny.', call_id='call_QBqpeKQlczRYIlCIzKh43Kha')]\n", - "---------- assistant ----------\n", - "The weather in Seattle is currently 72 degrees and sunny.\n", - "[Prompt tokens: 93, Completion tokens: 13]\n", - "---------- assistant ----------\n", - "TERMINATE\n", - "[Prompt tokens: 122, Completion tokens: 4]\n", - "---------- Summary ----------\n", - "Number of messages: 5\n", - "Finish reason: Text 'TERMINATE' mentioned\n", - "Total prompt tokens: 284\n", - "Total completion tokens: 31\n", - "Duration: 1.82 seconds\n" - ] - } - ], - "source": [ - "from autogen_agentchat.ui import Console\n", - "\n", - "# Use `asyncio.run(single_agent_team.reset())` when running in a script.\n", - "await single_agent_team.reset() # Reset the team for the next run.\n", - "# Use `asyncio.run(single_agent_team.run_stream(task=\"What is the weather in Seattle?\"))` when running in a script.\n", - "await Console(\n", - " single_agent_team.run_stream(task=\"What is the weather in Seattle?\")\n", - ") # Stream the messages to the console." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Resetting a Team\n", - "\n", - "You can reset the team by calling the {py:meth}`~autogen_agentchat.teams.BaseGroupChat.reset` method. This method will clear the team's state, including all agents." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "await single_agent_team.reset() # Reset the team for the next run." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It is usually a good idea to reset the team if the next task is not related to the previous task.\n", - "However, if the next task is related to the previous task, you don't need to reset and you can instead resume. We will cover this in the next section." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## Team Usage Guide\n", - "\n", - "We will now implement a slightly more complex team with multiple agents and learn how to resume the team after stopping it and even involve the user in the conversation.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Reflection Pattern\n", - "\n", - "We will now create a team with two agents that implement the _reflection_ pattern, a multi-agent design pattern where a critic agent evaluates the responses of a primary agent.\n", - "\n", - "Learn more about the reflection pattern using the [Core API](../../core-user-guide/design-patterns/reflection.ipynb).\n", - "\n", - "In this example, we will use the {py:class}`~autogen_agentchat.agents.AssistantAgent` class for both the primary and critic agents. We will combine the {py:class}`~autogen_agentchat.conditions.TextMentionTermination` and {py:class}`~autogen_agentchat.conditions.MaxMessageTermination` conditions to stop the team.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_agentchat.agents import AssistantAgent\n", - "from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination\n", - "from autogen_agentchat.teams import RoundRobinGroupChat\n", - "from autogen_agentchat.ui import Console\n", - "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", - "\n", - "# Create an OpenAI model client.\n", - "model_client = OpenAIChatCompletionClient(\n", - " model=\"gpt-4o-2024-08-06\",\n", - " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY env variable set.\n", - ")\n", - "\n", - "# Create the primary agent.\n", - "primary_agent = AssistantAgent(\n", - " \"primary\",\n", - " model_client=model_client,\n", - " system_message=\"You are a helpful AI assistant.\",\n", - ")\n", - "\n", - "# Create the critic agent.\n", - "critic_agent = AssistantAgent(\n", - " \"critic\",\n", - " model_client=model_client,\n", - " system_message=\"Provide constructive feedback. Respond with 'APPROVE' to when your feedbacks are addressed.\",\n", - ")\n", - "\n", - "# Define a termination condition that stops the task if the critic approves.\n", - "text_termination = TextMentionTermination(\"APPROVE\")\n", - "# Define a termination condition that stops the task after 5 messages.\n", - "max_message_termination = MaxMessageTermination(5)\n", - "# Combine the termination conditions using the `|`` operator so that the\n", - "# task stops when either condition is met.\n", - "termination = text_termination | max_message_termination\n", - "\n", - "# Create a team with the primary and critic agents.\n", - "reflection_team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=termination)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's give a poem-writing task to the team and see how the agents interact with each other." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "Write a short poem about fall season.\n", - "---------- primary ----------\n", - "Golden leaves dance on the breeze, \n", - "Whispering secrets through the trees. \n", - "Crisp air nips at cheeks so bright, \n", - "As daylight fades to early night. \n", - "\n", - "Pumpkins sit on porches grand, \n", - "In a painted, harvest land. \n", - "Sweaters hug us, warm and snug, \n", - "While cider fills each steamy mug. \n", - "\n", - "In this season's gentle sway, \n", - "Nature tells of time's ballet. \n", - "With each leaf's descent and flight, \n", - "Autumn sings its soft goodnight. \n", - "[Prompt tokens: 27, Completion tokens: 107]\n", - "---------- critic ----------\n", - "Your poem is beautiful and elegantly captures the essence of the fall season. The imagery you used creates vivid pictures of autumn landscapes and activities, making it easy for the reader to visualize the scene. The rhyming and rhythm contribute to the poem's musicality, enhancing its appeal. Each stanza highlights different aspects of fall, creating a well-rounded depiction of the season.\n", - "\n", - "To make the poem even more evocative, consider including a few additional sensory details or emotions tied to the season. For instance, you might evoke the sounds of rustling leaves or the feeling of warmth from a fireplace. Overall, it's a delightful and charming poem that effectively conveys the spirit of fall.\n", - "\n", - "If these suggestions are considered, please share the revised poem for additional feedback!\n", - "[Prompt tokens: 152, Completion tokens: 148]\n", - "---------- primary ----------\n", - "Thank you for the thoughtful feedback! Here's a revised version of the poem, incorporating more sensory details and emotions:\n", - "\n", - "---\n", - "\n", - "Golden leaves dance on the breeze, \n", - "Whispering secrets through the trees. \n", - "Crisp air kisses cheeks aglow, \n", - "As twilight casts a gentle show. \n", - "\n", - "Pumpkins guard each porch with pride, \n", - "In this painted, harvest tide. \n", - "Sweaters hug us, warm and snug, \n", - "While cider steams in every mug. \n", - "\n", - "Children laugh in rustling leaves, \n", - "As branches weave autumnal eaves. \n", - "Fireplaces crackle, whisper warmth, \n", - "Embracing hearts in homey charms. \n", - "\n", - "In this season's tender sway, \n", - "Nature turns in grand ballet. \n", - "With each leaf's descent and flight, \n", - "Autumn sings its soft goodnight. \n", - "\n", - "---\n", - "\n", - "I hope this version resonates even more deeply with the spirit of fall.\n", - "[Prompt tokens: 294, Completion tokens: 178]\n", - "---------- critic ----------\n", - "Your revised poem beautifully captures the essence of the fall season with delightful sensory details and emotions. The inclusion of words like \"twilight casts a gentle show,\" \"children laugh in rustling leaves,\" and \"fireplaces crackle\" adds depth and paints a vivid picture of autumn scenes. The addition of emotions, particularly the \"embracing hearts in homey charms,\" evokes a sense of warmth and comfort associated with this season.\n", - "\n", - "The poem flows smoothly with its rhythmic quality and maintains a harmonious balance in its description of autumn. Overall, it now provides an even richer and more immersive experience for the reader. Excellent work on enhancing the sensory experience—this version resonates wonderfully with the spirit of fall. \n", - "\n", - "APPROVE\n", - "[Prompt tokens: 490, Completion tokens: 142]\n", - "---------- Summary ----------\n", - "Number of messages: 5\n", - "Finish reason: Text 'APPROVE' mentioned, Maximum number of messages 5 reached, current message count: 5\n", - "Total prompt tokens: 963\n", - "Total completion tokens: 575\n", - "Duration: 8.10 seconds\n" - ] - } - ], - "source": [ - "# Use `asyncio.run(Console(reflection_team.run_stream(task=\"Write a short poem about fall season.\")))` when running in a script.\n", - "await Console(\n", - " reflection_team.run_stream(task=\"Write a short poem about fall season.\")\n", - ") # Stream the messages to the console." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Resuming a Team\n", - "\n", - "Let's run the team again with a new task while keeping the context about the previous task." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "将这首诗用中文唐诗风格写一遍。\n", - "---------- primary ----------\n", - "金叶飘舞随风起, \n", - "林间低语秋声细。 \n", - "凉风轻拂面颊红, \n", - "夕光渐隐天色丽。 \n", - "\n", - "南瓜门前静自笑, \n", - "丰收景色绘秋貌。 \n", - "暖衣贴身享温柔, \n", - "热饮氤氲杯中绕。 \n", - "\n", - "童声笑逐落叶中, \n", - "枝叶缀出秋帷浓。 \n", - "炉火轻鸣诉温情, \n", - "温馨满怀思乡容。 \n", - "\n", - "此时佳季自徘徊, \n", - "秋之舞步若梦来。 \n", - "片片叶落随风去, \n", - "秋声袅袅道安睡。 \n", - "[Prompt tokens: 664, Completion tokens: 155]\n", - "---------- critic ----------\n", - "这首诗成功地以唐诗的风格捕捉了秋天的精髓,以古雅的语言和流畅的节奏展示了秋日的美丽。你运用了优美的意象,如“金叶飘舞”和“夕光渐隐”,将秋季的景色描绘得栩栩如生。词句简炼而意境深远,充满了诗意。\n", - "\n", - "同时,诗中融入的情感,如“温馨满怀思乡容”以及“炉火轻鸣诉温情”,有效传达了秋天的暖意与思乡之情,令人感到温暖和亲切。\n", - "\n", - "整体而言,这是一首极具唐诗魅力的作品,成功地展现了秋天的许多层面,并引发读者的共鸣。恭喜你完成了这次优雅的改编!\n", - "\n", - "APPROVE\n", - "[Prompt tokens: 837, Completion tokens: 199]\n", - "---------- Summary ----------\n", - "Number of messages: 3\n", - "Finish reason: Text 'APPROVE' mentioned\n", - "Total prompt tokens: 1501\n", - "Total completion tokens: 354\n", - "Duration: 4.44 seconds\n" - ] - } - ], - "source": [ - "# Write the poem in Chinese Tang poetry style.\n", - "# Use `asyncio.run(Console(reflection_team.run_stream(task=\"将这首诗用中文唐诗风格写一遍。\")))` when running in a script.\n", - "await Console(reflection_team.run_stream(task=\"将这首诗用中文唐诗风格写一遍。\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Resume with another task." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "Write the poem in Spanish.\n", - "---------- primary ----------\n", - "Certainly! Here's a translation of the poem into Spanish:\n", - "\n", - "---\n", - "\n", - "Hojas doradas bailan en la brisa, \n", - "Susurran secretos en la arboleda. \n", - "El aire fresco besa las mejillas, \n", - "Mientras el crepúsculo se despide a su manera. \n", - "\n", - "Calabazas vigilan cada entrada, \n", - "En esta tierra de cosecha pintada. \n", - "Los suéteres nos abrazan calurosos, \n", - "Mientras el vapor sube de cada taza amorosa. \n", - "\n", - "Risas de niños suenan en hojas caídas, \n", - "Mientras ramas tejen toldos de caricias. \n", - "Las chimeneas crepitan, susurran calor, \n", - "Abrazando los corazones con su amor hogareño. \n", - "\n", - "En la danza de esta estación serena, \n", - "La naturaleza gira con su escena. \n", - "Con cada hoja que desciende en el viento, \n", - "El otoño canta su suave cuento. \n", - "\n", - "---\n", - "\n", - "Espero que esta traducción refleje el mismo espíritu y encantamiento del poema original.\n", - "[Prompt tokens: 1719, Completion tokens: 209]\n", - "---------- critic ----------\n", - "Your translation of the poem into Spanish beautifully captures the essence and lyrical quality of the original. The imagery and emotions are conveyed effectively, maintaining the warmth and serene atmosphere of fall that the poem embodies. Each stanza mirrors the themes presented in the English version, like the golden leaves, harvest, and cozy reflections of autumn.\n", - "\n", - "Overall, your translation is both poetic and faithful to the original content. If you have further adjustments or specific stylistic preferences, feel free to share. Great job on this translation!\n", - "\n", - "APPROVE\n", - "[Prompt tokens: 1946, Completion tokens: 102]\n", - "---------- Summary ----------\n", - "Number of messages: 3\n", - "Finish reason: Text 'APPROVE' mentioned\n", - "Total prompt tokens: 3665\n", - "Total completion tokens: 311\n", - "Duration: 4.22 seconds\n" - ] - } - ], - "source": [ - "# Write the poem in Spanish.\n", - "# Use `asyncio.run(Console(reflection_team.run_stream(task=\"Write the poem in Spanish.\")))` when running in a script.\n", - "await Console(reflection_team.run_stream(task=\"Write the poem in Spanish.\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Resuming a Previous Task\n", - "\n", - "We can call {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run` or {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run_stream` methods\n", - "without setting the `task` again to resume the previous task. The team will continue from where it left off." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- primary ----------\n", - "Thank you for your kind words! I'm glad the translation captures the essence and lyrical quality of the original poem. If you have any more requests or need further assistance, please feel free to let me know. I'm here to help!\n", - "[Prompt tokens: 2042, Completion tokens: 46]\n", - "---------- critic ----------\n", - "You're very welcome! I'm glad to hear that you're satisfied with the translation. If you have any more requests, whether it's poetry, translation, or any other kind of assistance, don't hesitate to reach out. I'm here to help with whatever you need. Enjoy the rest of your creative journey!\n", - "[Prompt tokens: 2106, Completion tokens: 58]\n", - "---------- primary ----------\n", - "Thank you for your encouraging words! I'm here to assist you with any other requests you might have, whether related to creativity, translation, or any other topic. Feel free to reach out whenever you need help or inspiration. Enjoy your journey in creativity!\n", - "[Prompt tokens: 2158, Completion tokens: 50]\n", - "---------- critic ----------\n", - "You're welcome! It's always a pleasure to assist you. If you ever have more questions or need inspiration, don't hesitate to reach out. Happy creating, and enjoy every moment of your creative journey!\n", - "[Prompt tokens: 2226, Completion tokens: 39]\n", - "---------- primary ----------\n", - "Thank you so much! Your encouragement is greatly appreciated. Feel free to reach out at any time for assistance or inspiration. I wish you the best on your creative journey and hope you enjoy every step of the way!\n", - "[Prompt tokens: 2259, Completion tokens: 43]\n", - "---------- Summary ----------\n", - "Number of messages: 5\n", - "Finish reason: Maximum number of messages 5 reached, current message count: 5\n", - "Total prompt tokens: 10791\n", - "Total completion tokens: 236\n", - "Duration: 5.00 seconds\n" - ] - } - ], - "source": [ - "# Use the `asyncio.run(Console(reflection_team.run_stream()))` when running in a script.\n", - "await Console(reflection_team.run_stream())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Pausing for User Input\n", - "\n", - "Sometimes, teams may require additional input from the application (e.g., the user) to continue making meaningful progress on a task. Here are two ways to achieve this:\n", - "\n", - "- Set the maximum number of turns so that the team stops after the specified number of turns.\n", - "- Use the {py:class}`~autogen_agentchat.conditions.HandoffTermination` termination condition.\n", - "\n", - "You can also create custom termination conditions. For more details, see [Termination Conditions](./termination.ipynb).\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Via Max Turns\n", - "\n", - "This method allows you to pause the team for user input by setting a maximum number of turns. For instance, you can configure the team to stop after the first agent responds by setting `max_turns` to 1. This is particularly useful in scenarios where continuous user engagement is required, such as in a chatbot.\n", - "\n", - "To implement this, set the `max_turns` parameter in the {py:meth}`~autogen_agentchat.teams.RoundRobinGroupChat` constructor.\n", - "\n", - "```python\n", - "team = RoundRobinGroupChat([...], max_turns=1)\n", - "```\n", - "\n", - "Once the team stops, the turn count will be reset. When you resume the team,\n", - "it will start from 0 again.\n", - "\n", - "Note that `max_turn` is specific to the team class and is currently only supported by\n", - "{py:class}`~autogen_agentchat.teams.RoundRobinGroupChat`, {py:class}`~autogen_agentchat.teams.SelectorGroupChat`, and {py:class}`~autogen_agentchat.teams.Swarm`.\n", - "When used with termination conditions, the team will stop when either condition is met." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Via Handoff\n", - "\n", - "You can use the {py:class}`~autogen_agentchat.conditions.HandoffTermination` termination condition\n", - "to stop the team when an agent sends a {py:class}`~autogen_agentchat.messages.HandoffMessage` message.\n", - "\n", - "Let's create a team with a single {py:class}`~autogen_agentchat.agents.AssistantAgent` agent\n", - "with a handoff setting.\n", - "\n", - "```{note}\n", - "The model used with {py:class}`~autogen_agentchat.agents.AssistantAgent` must support tool call\n", - "to use the handoff feature.\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_agentchat.agents import AssistantAgent\n", - "from autogen_agentchat.base import Handoff\n", - "from autogen_agentchat.conditions import HandoffTermination, TextMentionTermination\n", - "from autogen_agentchat.teams import RoundRobinGroupChat\n", - "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", - "\n", - "# Create an OpenAI model client.\n", - "model_client = OpenAIChatCompletionClient(\n", - " model=\"gpt-4o-2024-08-06\",\n", - " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY env variable set.\n", - ")\n", - "\n", - "# Create a lazy assistant agent that always hands off to the user.\n", - "lazy_agent = AssistantAgent(\n", - " \"lazy_assistant\",\n", - " model_client=model_client,\n", - " handoffs=[Handoff(target=\"user\", message=\"Transfer to user.\")],\n", - " system_message=\"Always transfer to user when you don't know the answer. Respond 'TERMINATE' when task is complete.\",\n", - ")\n", - "\n", - "# Define a termination condition that checks for handoff message targetting helper and text \"TERMINATE\".\n", - "handoff_termination = HandoffTermination(target=\"user\")\n", - "text_termination = TextMentionTermination(\"TERMINATE\")\n", - "termination = handoff_termination | text_termination\n", - "\n", - "# Create a single-agent team.\n", - "lazy_agent_team = RoundRobinGroupChat([lazy_agent], termination_condition=termination)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's run the team with a task that requires additional input from the user\n", - "because the agent doesn't have relevant tools to continue processing the task." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "What is the weather in New York?\n", - "---------- lazy_assistant ----------\n", - "[FunctionCall(id='call_YHm4KPjFIWZE95YrJWlJwcv4', arguments='{}', name='transfer_to_user')]\n", - "[Prompt tokens: 68, Completion tokens: 11]\n", - "---------- lazy_assistant ----------\n", - "[FunctionExecutionResult(content='Transfer to user.', call_id='call_YHm4KPjFIWZE95YrJWlJwcv4')]\n", - "---------- lazy_assistant ----------\n", - "Transfer to user.\n", - "---------- Summary ----------\n", - "Number of messages: 4\n", - "Finish reason: Handoff to user from lazy_assistant detected.\n", - "Total prompt tokens: 68\n", - "Total completion tokens: 11\n", - "Duration: 0.73 seconds\n" - ] - } - ], - "source": [ - "from autogen_agentchat.ui import Console\n", - "\n", - "# Use `asyncio.run(Console(lazy_agent_team.run_stream(task=\"What is the weather in New York?\")))` when running in a script.\n", - "await Console(lazy_agent_team.run_stream(task=\"What is the weather in New York?\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can see the team stopped due to the handoff message was detected.\n", - "Let's continue the team by providing the information the agent needs." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "It is raining in New York.\n", - "---------- lazy_assistant ----------\n", - "I hope you stay dry! Is there anything else you would like to know or do?\n", - "[Prompt tokens: 108, Completion tokens: 19]\n", - "---------- lazy_assistant ----------\n", - "TERMINATE\n", - "[Prompt tokens: 134, Completion tokens: 4]\n", - "---------- Summary ----------\n", - "Number of messages: 3\n", - "Finish reason: Text 'TERMINATE' mentioned\n", - "Total prompt tokens: 242\n", - "Total completion tokens: 23\n", - "Duration: 6.77 seconds\n" - ] - } - ], - "source": [ - "# Use `asyncio.run(Console(lazy_agent_team.run_stream(task=\"It is raining in New York.\")))` when running in a script.\n", - "await Console(lazy_agent_team.run_stream(task=\"It is raining in New York.\"))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Teams\n", + "\n", + "In this section you'll learn how to create a _multi-agent team_ (or simply team) using AutoGen. A team is a group of agents that work together to achieve a common goal.\n", + "\n", + "We'll first show you how to create and run a team. We'll then explain how to observe the team's behavior, which is crucial for debugging and understanding the team's performance, and common operations to control the team's behavior.\n", + "\n", + "We'll start by focusing on a simple team with consisting of a single agent (the baseline case) and use a round robin strategy to select the agent to act. We'll then show how to create a team with multiple agents and how to implement a more sophisticated strategy to select the agent to act." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating a Team\n", + "\n", + "{py:class}`~autogen_agentchat.teams.RoundRobinGroupChat` is a simple yet effective team configuration where all agents share the same context and take turns responding in a round-robin fashion. Each agent, during its turn, broadcasts its response to all other agents, ensuring that the entire team maintains a consistent context.\n", + "\n", + "We will begin by creating a team with a single {py:class}`~autogen_agentchat.agents.AssistantAgent` and a {py:class}`~autogen_agentchat.conditions.TextMentionTermination` condition that stops the team when a specific word is detected in the agent's response.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.conditions import TextMentionTermination\n", + "from autogen_agentchat.teams import RoundRobinGroupChat\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", + "\n", + "# Create an OpenAI model client.\n", + "model_client = OpenAIChatCompletionClient(\n", + " model=\"gpt-4o-2024-08-06\",\n", + " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY env variable set.\n", + ")\n", + "\n", + "\n", + "# Define a tool that gets the weather for a city.\n", + "async def get_weather(city: str) -> str:\n", + " \"\"\"Get the weather for a city.\"\"\"\n", + " return f\"The weather in {city} is 72 degrees and Sunny.\"\n", + "\n", + "\n", + "# Create an assistant agent.\n", + "weather_agent = AssistantAgent(\n", + " \"assistant\",\n", + " model_client=model_client,\n", + " tools=[get_weather],\n", + " system_message=\"Respond 'TERMINATE' when task is complete.\",\n", + ")\n", + "\n", + "# Define a termination condition.\n", + "text_termination = TextMentionTermination(\"TERMINATE\")\n", + "\n", + "# Create a single-agent team.\n", + "single_agent_team = RoundRobinGroupChat([weather_agent], termination_condition=text_termination)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Running a Team\n", + "\n", + "Let's calls the {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run` method\n", + "to start the team with a task." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='What is the weather in New York?'), ToolCallRequestEvent(source='assistant', models_usage=RequestUsage(prompt_tokens=70, completion_tokens=15), content=[FunctionCall(id='call_6qWxrK1VdEVSryXKyIVpwE0h', arguments='{\"city\":\"New York\"}', name='get_weather')]), ToolCallExecutionEvent(source='assistant', models_usage=None, content=[FunctionExecutionResult(content='The weather in New York is 72 degrees and Sunny.', call_id='call_6qWxrK1VdEVSryXKyIVpwE0h')]), TextMessage(source='assistant', models_usage=RequestUsage(prompt_tokens=96, completion_tokens=13), content='The weather in New York is 72 degrees and sunny.'), TextMessage(source='assistant', models_usage=RequestUsage(prompt_tokens=125, completion_tokens=4), content='TERMINATE')], stop_reason=\"Text 'TERMINATE' mentioned\")\n" + ] + } + ], + "source": [ + "async def run_team() -> None:\n", + " result = await single_agent_team.run(task=\"What is the weather in New York?\")\n", + " print(result)\n", + "\n", + "\n", + "# Use `asyncio.run(run_team())` when running in a script.\n", + "await run_team()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The team ran the same agent until the termination condition was met.\n", + "In this case, the termination condition was met when the word \"TERMINATE\" is detected in the\n", + "agent's response.\n", + "When the team stops, it returns a {py:class}`~autogen_agentchat.base.TaskResult` object with all the messages produced by the agents in the team." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Observability\n", + "\n", + "Similar to the agent's {py:meth}`~autogen_agentchat.agents.BaseChatAgent.on_messages_stream` method, you can stream the team's messages by calling the {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run_stream` method. This method returns a generator that yields messages produced by the agents in the team as they are generated, with the final item being the task result.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "source='user' models_usage=None content='What is the weather in New York?'\n", + "source='assistant' models_usage=RequestUsage(prompt_tokens=70, completion_tokens=15) content=[FunctionCall(id='call_FcJlzgDmPUgGSKLjeGajFZ7V', arguments='{\"city\":\"New York\"}', name='get_weather')]\n", + "source='assistant' models_usage=None content=[FunctionExecutionResult(content='The weather in New York is 72 degrees and Sunny.', call_id='call_FcJlzgDmPUgGSKLjeGajFZ7V')]\n", + "source='assistant' models_usage=RequestUsage(prompt_tokens=96, completion_tokens=14) content='The weather in New York is currently 72 degrees and sunny.'\n", + "source='assistant' models_usage=RequestUsage(prompt_tokens=126, completion_tokens=4) content='TERMINATE'\n", + "Stop Reason: Text 'TERMINATE' mentioned\n" + ] + } + ], + "source": [ + "from autogen_agentchat.base import TaskResult\n", + "\n", + "\n", + "async def run_team_stream() -> None:\n", + " async for message in single_agent_team.run_stream(task=\"What is the weather in New York?\"):\n", + " if isinstance(message, TaskResult):\n", + " print(\"Stop Reason:\", message.stop_reason)\n", + " else:\n", + " print(message)\n", + "\n", + "\n", + "# Use `asyncio.run(run_team_stream())` when running in a script.\n", + "await run_team_stream()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As demonstrated in the example above, you can determine the reason why the team stopped by checking the {py:attr}`~autogen_agentchat.base.TaskResult.stop_reason` attribute.\n", + "\n", + "The {py:meth}`~autogen_agentchat.ui.Console` method provides a convenient way to print messages to the console with proper formatting.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "What is the weather in Seattle?\n", + "---------- assistant ----------\n", + "[FunctionCall(id='call_QBqpeKQlczRYIlCIzKh43Kha', arguments='{\"city\":\"Seattle\"}', name='get_weather')]\n", + "[Prompt tokens: 69, Completion tokens: 14]\n", + "---------- assistant ----------\n", + "[FunctionExecutionResult(content='The weather in Seattle is 72 degrees and Sunny.', call_id='call_QBqpeKQlczRYIlCIzKh43Kha')]\n", + "---------- assistant ----------\n", + "The weather in Seattle is currently 72 degrees and sunny.\n", + "[Prompt tokens: 93, Completion tokens: 13]\n", + "---------- assistant ----------\n", + "TERMINATE\n", + "[Prompt tokens: 122, Completion tokens: 4]\n", + "---------- Summary ----------\n", + "Number of messages: 5\n", + "Finish reason: Text 'TERMINATE' mentioned\n", + "Total prompt tokens: 284\n", + "Total completion tokens: 31\n", + "Duration: 1.82 seconds\n" + ] + } + ], + "source": [ + "from autogen_agentchat.ui import Console\n", + "\n", + "# Use `asyncio.run(single_agent_team.reset())` when running in a script.\n", + "await single_agent_team.reset() # Reset the team for the next run.\n", + "# Use `asyncio.run(single_agent_team.run_stream(task=\"What is the weather in Seattle?\"))` when running in a script.\n", + "await Console(\n", + " single_agent_team.run_stream(task=\"What is the weather in Seattle?\")\n", + ") # Stream the messages to the console." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Resetting a Team\n", + "\n", + "You can reset the team by calling the {py:meth}`~autogen_agentchat.teams.BaseGroupChat.reset` method. This method will clear the team's state, including all agents." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "await single_agent_team.reset() # Reset the team for the next run." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is usually a good idea to reset the team if the next task is not related to the previous task.\n", + "However, if the next task is related to the previous task, you don't need to reset and you can instead resume. We will cover this in the next section." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## Team Usage Guide\n", + "\n", + "We will now implement a slightly more complex team with multiple agents and learn how to resume the team after stopping it and even involve the user in the conversation.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Reflection Pattern\n", + "\n", + "We will now create a team with two agents that implement the _reflection_ pattern, a multi-agent design pattern where a critic agent evaluates the responses of a primary agent.\n", + "\n", + "Learn more about the reflection pattern using the [Core API](../../core-user-guide/design-patterns/reflection.ipynb).\n", + "\n", + "In this example, we will use the {py:class}`~autogen_agentchat.agents.AssistantAgent` class for both the primary and critic agents. We will combine the {py:class}`~autogen_agentchat.conditions.TextMentionTermination` and {py:class}`~autogen_agentchat.conditions.MaxMessageTermination` conditions to stop the team.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination\n", + "from autogen_agentchat.teams import RoundRobinGroupChat\n", + "from autogen_agentchat.ui import Console\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", + "\n", + "# Create an OpenAI model client.\n", + "model_client = OpenAIChatCompletionClient(\n", + " model=\"gpt-4o-2024-08-06\",\n", + " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY env variable set.\n", + ")\n", + "\n", + "# Create the primary agent.\n", + "primary_agent = AssistantAgent(\n", + " \"primary\",\n", + " model_client=model_client,\n", + " system_message=\"You are a helpful AI assistant.\",\n", + ")\n", + "\n", + "# Create the critic agent.\n", + "critic_agent = AssistantAgent(\n", + " \"critic\",\n", + " model_client=model_client,\n", + " system_message=\"Provide constructive feedback. Respond with 'APPROVE' to when your feedbacks are addressed.\",\n", + ")\n", + "\n", + "# Define a termination condition that stops the task if the critic approves.\n", + "text_termination = TextMentionTermination(\"APPROVE\")\n", + "# Define a termination condition that stops the task after 5 messages.\n", + "max_message_termination = MaxMessageTermination(5)\n", + "# Combine the termination conditions using the `|`` operator so that the\n", + "# task stops when either condition is met.\n", + "termination = text_termination | max_message_termination\n", + "\n", + "# Create a team with the primary and critic agents.\n", + "reflection_team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=termination)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's give a poem-writing task to the team and see how the agents interact with each other." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Write a short poem about fall season.\n", + "---------- primary ----------\n", + "Golden leaves dance on the breeze, \n", + "Whispering secrets through the trees. \n", + "Crisp air nips at cheeks so bright, \n", + "As daylight fades to early night. \n", + "\n", + "Pumpkins sit on porches grand, \n", + "In a painted, harvest land. \n", + "Sweaters hug us, warm and snug, \n", + "While cider fills each steamy mug. \n", + "\n", + "In this season's gentle sway, \n", + "Nature tells of time's ballet. \n", + "With each leaf's descent and flight, \n", + "Autumn sings its soft goodnight. \n", + "[Prompt tokens: 27, Completion tokens: 107]\n", + "---------- critic ----------\n", + "Your poem is beautiful and elegantly captures the essence of the fall season. The imagery you used creates vivid pictures of autumn landscapes and activities, making it easy for the reader to visualize the scene. The rhyming and rhythm contribute to the poem's musicality, enhancing its appeal. Each stanza highlights different aspects of fall, creating a well-rounded depiction of the season.\n", + "\n", + "To make the poem even more evocative, consider including a few additional sensory details or emotions tied to the season. For instance, you might evoke the sounds of rustling leaves or the feeling of warmth from a fireplace. Overall, it's a delightful and charming poem that effectively conveys the spirit of fall.\n", + "\n", + "If these suggestions are considered, please share the revised poem for additional feedback!\n", + "[Prompt tokens: 152, Completion tokens: 148]\n", + "---------- primary ----------\n", + "Thank you for the thoughtful feedback! Here's a revised version of the poem, incorporating more sensory details and emotions:\n", + "\n", + "---\n", + "\n", + "Golden leaves dance on the breeze, \n", + "Whispering secrets through the trees. \n", + "Crisp air kisses cheeks aglow, \n", + "As twilight casts a gentle show. \n", + "\n", + "Pumpkins guard each porch with pride, \n", + "In this painted, harvest tide. \n", + "Sweaters hug us, warm and snug, \n", + "While cider steams in every mug. \n", + "\n", + "Children laugh in rustling leaves, \n", + "As branches weave autumnal eaves. \n", + "Fireplaces crackle, whisper warmth, \n", + "Embracing hearts in homey charms. \n", + "\n", + "In this season's tender sway, \n", + "Nature turns in grand ballet. \n", + "With each leaf's descent and flight, \n", + "Autumn sings its soft goodnight. \n", + "\n", + "---\n", + "\n", + "I hope this version resonates even more deeply with the spirit of fall.\n", + "[Prompt tokens: 294, Completion tokens: 178]\n", + "---------- critic ----------\n", + "Your revised poem beautifully captures the essence of the fall season with delightful sensory details and emotions. The inclusion of words like \"twilight casts a gentle show,\" \"children laugh in rustling leaves,\" and \"fireplaces crackle\" adds depth and paints a vivid picture of autumn scenes. The addition of emotions, particularly the \"embracing hearts in homey charms,\" evokes a sense of warmth and comfort associated with this season.\n", + "\n", + "The poem flows smoothly with its rhythmic quality and maintains a harmonious balance in its description of autumn. Overall, it now provides an even richer and more immersive experience for the reader. Excellent work on enhancing the sensory experience—this version resonates wonderfully with the spirit of fall. \n", + "\n", + "APPROVE\n", + "[Prompt tokens: 490, Completion tokens: 142]\n", + "---------- Summary ----------\n", + "Number of messages: 5\n", + "Finish reason: Text 'APPROVE' mentioned, Maximum number of messages 5 reached, current message count: 5\n", + "Total prompt tokens: 963\n", + "Total completion tokens: 575\n", + "Duration: 8.10 seconds\n" + ] + } + ], + "source": [ + "# Use `asyncio.run(Console(reflection_team.run_stream(task=\"Write a short poem about fall season.\")))` when running in a script.\n", + "await Console(\n", + " reflection_team.run_stream(task=\"Write a short poem about fall season.\")\n", + ") # Stream the messages to the console." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Resuming a Team\n", + "\n", + "Let's run the team again with a new task while keeping the context about the previous task." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "将这首诗用中文唐诗风格写一遍。\n", + "---------- primary ----------\n", + "金叶飘舞随风起, \n", + "林间低语秋声细。 \n", + "凉风轻拂面颊红, \n", + "夕光渐隐天色丽。 \n", + "\n", + "南瓜门前静自笑, \n", + "丰收景色绘秋貌。 \n", + "暖衣贴身享温柔, \n", + "热饮氤氲杯中绕。 \n", + "\n", + "童声笑逐落叶中, \n", + "枝叶缀出秋帷浓。 \n", + "炉火轻鸣诉温情, \n", + "温馨满怀思乡容。 \n", + "\n", + "此时佳季自徘徊, \n", + "秋之舞步若梦来。 \n", + "片片叶落随风去, \n", + "秋声袅袅道安睡。 \n", + "[Prompt tokens: 664, Completion tokens: 155]\n", + "---------- critic ----------\n", + "这首诗成功地以唐诗的风格捕捉了秋天的精髓,以古雅的语言和流畅的节奏展示了秋日的美丽。你运用了优美的意象,如“金叶飘舞”和“夕光渐隐”,将秋季的景色描绘得栩栩如生。词句简炼而意境深远,充满了诗意。\n", + "\n", + "同时,诗中融入的情感,如“温馨满怀思乡容”以及“炉火轻鸣诉温情”,有效传达了秋天的暖意与思乡之情,令人感到温暖和亲切。\n", + "\n", + "整体而言,这是一首极具唐诗魅力的作品,成功地展现了秋天的许多层面,并引发读者的共鸣。恭喜你完成了这次优雅的改编!\n", + "\n", + "APPROVE\n", + "[Prompt tokens: 837, Completion tokens: 199]\n", + "---------- Summary ----------\n", + "Number of messages: 3\n", + "Finish reason: Text 'APPROVE' mentioned\n", + "Total prompt tokens: 1501\n", + "Total completion tokens: 354\n", + "Duration: 4.44 seconds\n" + ] + } + ], + "source": [ + "# Write the poem in Chinese Tang poetry style.\n", + "# Use `asyncio.run(Console(reflection_team.run_stream(task=\"将这首诗用中文唐诗风格写一遍。\")))` when running in a script.\n", + "await Console(reflection_team.run_stream(task=\"将这首诗用中文唐诗风格写一遍。\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Resume with another task." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Write the poem in Spanish.\n", + "---------- primary ----------\n", + "Certainly! Here's a translation of the poem into Spanish:\n", + "\n", + "---\n", + "\n", + "Hojas doradas bailan en la brisa, \n", + "Susurran secretos en la arboleda. \n", + "El aire fresco besa las mejillas, \n", + "Mientras el crepúsculo se despide a su manera. \n", + "\n", + "Calabazas vigilan cada entrada, \n", + "En esta tierra de cosecha pintada. \n", + "Los suéteres nos abrazan calurosos, \n", + "Mientras el vapor sube de cada taza amorosa. \n", + "\n", + "Risas de niños suenan en hojas caídas, \n", + "Mientras ramas tejen toldos de caricias. \n", + "Las chimeneas crepitan, susurran calor, \n", + "Abrazando los corazones con su amor hogareño. \n", + "\n", + "En la danza de esta estación serena, \n", + "La naturaleza gira con su escena. \n", + "Con cada hoja que desciende en el viento, \n", + "El otoño canta su suave cuento. \n", + "\n", + "---\n", + "\n", + "Espero que esta traducción refleje el mismo espíritu y encantamiento del poema original.\n", + "[Prompt tokens: 1719, Completion tokens: 209]\n", + "---------- critic ----------\n", + "Your translation of the poem into Spanish beautifully captures the essence and lyrical quality of the original. The imagery and emotions are conveyed effectively, maintaining the warmth and serene atmosphere of fall that the poem embodies. Each stanza mirrors the themes presented in the English version, like the golden leaves, harvest, and cozy reflections of autumn.\n", + "\n", + "Overall, your translation is both poetic and faithful to the original content. If you have further adjustments or specific stylistic preferences, feel free to share. Great job on this translation!\n", + "\n", + "APPROVE\n", + "[Prompt tokens: 1946, Completion tokens: 102]\n", + "---------- Summary ----------\n", + "Number of messages: 3\n", + "Finish reason: Text 'APPROVE' mentioned\n", + "Total prompt tokens: 3665\n", + "Total completion tokens: 311\n", + "Duration: 4.22 seconds\n" + ] + } + ], + "source": [ + "# Write the poem in Spanish.\n", + "# Use `asyncio.run(Console(reflection_team.run_stream(task=\"Write the poem in Spanish.\")))` when running in a script.\n", + "await Console(reflection_team.run_stream(task=\"Write the poem in Spanish.\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Resuming a Previous Task\n", + "\n", + "We can call {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run` or {py:meth}`~autogen_agentchat.teams.BaseGroupChat.run_stream` methods\n", + "without setting the `task` again to resume the previous task. The team will continue from where it left off." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- primary ----------\n", + "Thank you for your kind words! I'm glad the translation captures the essence and lyrical quality of the original poem. If you have any more requests or need further assistance, please feel free to let me know. I'm here to help!\n", + "[Prompt tokens: 2042, Completion tokens: 46]\n", + "---------- critic ----------\n", + "You're very welcome! I'm glad to hear that you're satisfied with the translation. If you have any more requests, whether it's poetry, translation, or any other kind of assistance, don't hesitate to reach out. I'm here to help with whatever you need. Enjoy the rest of your creative journey!\n", + "[Prompt tokens: 2106, Completion tokens: 58]\n", + "---------- primary ----------\n", + "Thank you for your encouraging words! I'm here to assist you with any other requests you might have, whether related to creativity, translation, or any other topic. Feel free to reach out whenever you need help or inspiration. Enjoy your journey in creativity!\n", + "[Prompt tokens: 2158, Completion tokens: 50]\n", + "---------- critic ----------\n", + "You're welcome! It's always a pleasure to assist you. If you ever have more questions or need inspiration, don't hesitate to reach out. Happy creating, and enjoy every moment of your creative journey!\n", + "[Prompt tokens: 2226, Completion tokens: 39]\n", + "---------- primary ----------\n", + "Thank you so much! Your encouragement is greatly appreciated. Feel free to reach out at any time for assistance or inspiration. I wish you the best on your creative journey and hope you enjoy every step of the way!\n", + "[Prompt tokens: 2259, Completion tokens: 43]\n", + "---------- Summary ----------\n", + "Number of messages: 5\n", + "Finish reason: Maximum number of messages 5 reached, current message count: 5\n", + "Total prompt tokens: 10791\n", + "Total completion tokens: 236\n", + "Duration: 5.00 seconds\n" + ] + } + ], + "source": [ + "# Use the `asyncio.run(Console(reflection_team.run_stream()))` when running in a script.\n", + "await Console(reflection_team.run_stream())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pausing for User Input\n", + "\n", + "Sometimes, teams may require additional input from the application (e.g., the user) to continue making meaningful progress on a task. Here are two ways to achieve this:\n", + "\n", + "- Set the maximum number of turns so that the team stops after the specified number of turns.\n", + "- Use the {py:class}`~autogen_agentchat.conditions.HandoffTermination` termination condition.\n", + "\n", + "You can also create custom termination conditions. For more details, see [Termination Conditions](./termination.ipynb).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Via Max Turns\n", + "\n", + "This method allows you to pause the team for user input by setting a maximum number of turns. For instance, you can configure the team to stop after the first agent responds by setting `max_turns` to 1. This is particularly useful in scenarios where continuous user engagement is required, such as in a chatbot.\n", + "\n", + "To implement this, set the `max_turns` parameter in the {py:meth}`~autogen_agentchat.teams.RoundRobinGroupChat` constructor.\n", + "\n", + "```python\n", + "team = RoundRobinGroupChat([...], max_turns=1)\n", + "```\n", + "\n", + "Once the team stops, the turn count will be reset. When you resume the team,\n", + "it will start from 0 again.\n", + "\n", + "Note that `max_turn` is specific to the team class and is currently only supported by\n", + "{py:class}`~autogen_agentchat.teams.RoundRobinGroupChat`, {py:class}`~autogen_agentchat.teams.SelectorGroupChat`, and {py:class}`~autogen_agentchat.teams.Swarm`.\n", + "When used with termination conditions, the team will stop when either condition is met." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Via Handoff\n", + "\n", + "You can use the {py:class}`~autogen_agentchat.conditions.HandoffTermination` termination condition\n", + "to stop the team when an agent sends a {py:class}`~autogen_agentchat.messages.HandoffMessage` message.\n", + "\n", + "Let's create a team with a single {py:class}`~autogen_agentchat.agents.AssistantAgent` agent\n", + "with a handoff setting.\n", + "\n", + "```{note}\n", + "The model used with {py:class}`~autogen_agentchat.agents.AssistantAgent` must support tool call\n", + "to use the handoff feature.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.base import Handoff\n", + "from autogen_agentchat.conditions import HandoffTermination, TextMentionTermination\n", + "from autogen_agentchat.teams import RoundRobinGroupChat\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", + "\n", + "# Create an OpenAI model client.\n", + "model_client = OpenAIChatCompletionClient(\n", + " model=\"gpt-4o-2024-08-06\",\n", + " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY env variable set.\n", + ")\n", + "\n", + "# Create a lazy assistant agent that always hands off to the user.\n", + "lazy_agent = AssistantAgent(\n", + " \"lazy_assistant\",\n", + " model_client=model_client,\n", + " handoffs=[Handoff(target=\"user\", message=\"Transfer to user.\")],\n", + " system_message=\"Always transfer to user when you don't know the answer. Respond 'TERMINATE' when task is complete.\",\n", + ")\n", + "\n", + "# Define a termination condition that checks for handoff message targetting helper and text \"TERMINATE\".\n", + "handoff_termination = HandoffTermination(target=\"user\")\n", + "text_termination = TextMentionTermination(\"TERMINATE\")\n", + "termination = handoff_termination | text_termination\n", + "\n", + "# Create a single-agent team.\n", + "lazy_agent_team = RoundRobinGroupChat([lazy_agent], termination_condition=termination)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's run the team with a task that requires additional input from the user\n", + "because the agent doesn't have relevant tools to continue processing the task." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "What is the weather in New York?\n", + "---------- lazy_assistant ----------\n", + "[FunctionCall(id='call_YHm4KPjFIWZE95YrJWlJwcv4', arguments='{}', name='transfer_to_user')]\n", + "[Prompt tokens: 68, Completion tokens: 11]\n", + "---------- lazy_assistant ----------\n", + "[FunctionExecutionResult(content='Transfer to user.', call_id='call_YHm4KPjFIWZE95YrJWlJwcv4')]\n", + "---------- lazy_assistant ----------\n", + "Transfer to user.\n", + "---------- Summary ----------\n", + "Number of messages: 4\n", + "Finish reason: Handoff to user from lazy_assistant detected.\n", + "Total prompt tokens: 68\n", + "Total completion tokens: 11\n", + "Duration: 0.73 seconds\n" + ] + } + ], + "source": [ + "from autogen_agentchat.ui import Console\n", + "\n", + "# Use `asyncio.run(Console(lazy_agent_team.run_stream(task=\"What is the weather in New York?\")))` when running in a script.\n", + "await Console(lazy_agent_team.run_stream(task=\"What is the weather in New York?\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see the team stopped due to the handoff message was detected.\n", + "Let's continue the team by providing the information the agent needs." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "It is raining in New York.\n", + "---------- lazy_assistant ----------\n", + "I hope you stay dry! Is there anything else you would like to know or do?\n", + "[Prompt tokens: 108, Completion tokens: 19]\n", + "---------- lazy_assistant ----------\n", + "TERMINATE\n", + "[Prompt tokens: 134, Completion tokens: 4]\n", + "---------- Summary ----------\n", + "Number of messages: 3\n", + "Finish reason: Text 'TERMINATE' mentioned\n", + "Total prompt tokens: 242\n", + "Total completion tokens: 23\n", + "Duration: 6.77 seconds\n" + ] + } + ], + "source": [ + "# Use `asyncio.run(Console(lazy_agent_team.run_stream(task=\"It is raining in New York.\")))` when running in a script.\n", + "await Console(lazy_agent_team.run_stream(task=\"It is raining in New York.\"))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/termination.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/termination.ipynb index a115df68b18d..998e9f3529a8 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/termination.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/termination.ipynb @@ -1,304 +1,304 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Termination \n", - "\n", - "In the previous section, we explored how to define agents, and organize them into teams that can solve tasks. However, a run can go on forever, and in many cases, we need to know _when_ to stop them. This is the role of the termination condition.\n", - "\n", - "AgentChat supports several termination condition by providing a base {py:class}`~autogen_agentchat.base.TerminationCondition` class and several implementations that inherit from it.\n", - "\n", - "A termination condition is a callable that takes a sequece of {py:class}`~autogen_agentchat.messages.AgentMessage` objects **since the last time the condition was called**, and returns a {py:class}`~autogen_agentchat.messages.StopMessage` if the conversation should be terminated, or `None` otherwise.\n", - "Once a termination condition has been reached, it must be reset by calling {py:meth}`~autogen_agentchat.base.TerminationCondition.reset` before it can be used again.\n", - "\n", - "Some important things to note about termination conditions: \n", - "- They are stateful but reset automatically after each run ({py:meth}`~autogen_agentchat.base.TaskRunner.run` or {py:meth}`~autogen_agentchat.base.TaskRunner.run_stream`) is finished.\n", - "- They can be combined using the AND and OR operators.\n", - "\n", - "```{note}\n", - "For group chat teams (i.e., {py:class}`~autogen_agentchat.teams.RoundRobinGroupChat`,\n", - "{py:class}`~autogen_agentchat.teams.SelectorGroupChat`, and {py:class}`~autogen_agentchat.teams.Swarm`),\n", - "the termination condition is called after each agent responds.\n", - "While a response may contain multiple inner messages, the team calls its termination condition just once for all the messages from a single response.\n", - "So the condition is called with the \"delta sequence\" of messages since the last time it was called.\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Built-In Termination Conditions: \n", - "1. {py:class}`~autogen_agentchat.conditions.MaxMessageTermination`: Stops after a specified number of messages have been produced, including both agent and task messages.\n", - "2. {py:class}`~autogen_agentchat.conditions.TextMentionTermination`: Stops when specific text or string is mentioned in a message (e.g., \"TERMINATE\").\n", - "3. {py:class}`~autogen_agentchat.conditions.TokenUsageTermination`: Stops when a certain number of prompt or completion tokens are used. This requires the agents to report token usage in their messages.\n", - "4. {py:class}`~autogen_agentchat.conditions.TimeoutTermination`: Stops after a specified duration in seconds.\n", - "5. {py:class}`~autogen_agentchat.conditions.HandoffTermination`: Stops when a handoff to a specific target is requested. Handoff messages can be used to build patterns such as {py:class}`~autogen_agentchat.teams.Swarm`. This is useful when you want to pause the run and allow application or user to provide input when an agent hands off to them.\n", - "6. {py:class}`~autogen_agentchat.conditions.SourceMatchTermination`: Stops after a specific agent responds.\n", - "7. {py:class}`~autogen_agentchat.conditions.ExternalTermination`: Enables programmatic control of termination from outside the run. This is useful for UI integration (e.g., \"Stop\" buttons in chat interfaces).\n", - "8. {py:class}`~autogen_agentchat.conditions.StopMessageTermination`: Stops when a {py:class}`~autogen_agentchat.messages.StopMessage` is produced by an agent." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To demonstrate the characteristics of termination conditions, we'll create a team consisting of two agents: a primary agent responsible for text generation and a critic agent that reviews and provides feedback on the generated text." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_agentchat.agents import AssistantAgent\n", - "from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination\n", - "from autogen_agentchat.teams import RoundRobinGroupChat\n", - "from autogen_agentchat.ui import Console\n", - "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", - "\n", - "model_client = OpenAIChatCompletionClient(\n", - " model=\"gpt-4o\",\n", - " temperature=1,\n", - " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY env variable set.\n", - ")\n", - "\n", - "# Create the primary agent.\n", - "primary_agent = AssistantAgent(\n", - " \"primary\",\n", - " model_client=model_client,\n", - " system_message=\"You are a helpful AI assistant.\",\n", - ")\n", - "\n", - "# Create the critic agent.\n", - "critic_agent = AssistantAgent(\n", - " \"critic\",\n", - " model_client=model_client,\n", - " system_message=\"Provide constructive feedback for every message. Respond with 'APPROVE' to when your feedbacks are addressed.\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's explore how termination conditions automatically reset after each `run` or `run_stream` call, allowing the team to resume its conversation from where it left off." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "Write a unique, Haiku about the weather in Paris\n", - "---------- primary ----------\n", - "Gentle rain whispers, \n", - "Cobblestones glisten softly— \n", - "Paris dreams in gray.\n", - "[Prompt tokens: 30, Completion tokens: 19]\n", - "---------- critic ----------\n", - "The Haiku captures the essence of a rainy day in Paris beautifully, and the imagery is vivid. However, it's important to ensure the use of the traditional 5-7-5 syllable structure for Haikus. Your current Haiku lines are composed of 4-7-5 syllables, which slightly deviates from the form. Consider revising the first line to fit the structure.\n", - "\n", - "For example:\n", - "Soft rain whispers down, \n", - "Cobblestones glisten softly — \n", - "Paris dreams in gray.\n", - "\n", - "This revision maintains the essence of your original lines while adhering to the traditional Haiku structure.\n", - "[Prompt tokens: 70, Completion tokens: 120]\n", - "---------- Summary ----------\n", - "Number of messages: 3\n", - "Finish reason: Maximum number of messages 3 reached, current message count: 3\n", - "Total prompt tokens: 100\n", - "Total completion tokens: 139\n", - "Duration: 3.34 seconds\n" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Termination \n", + "\n", + "In the previous section, we explored how to define agents, and organize them into teams that can solve tasks. However, a run can go on forever, and in many cases, we need to know _when_ to stop them. This is the role of the termination condition.\n", + "\n", + "AgentChat supports several termination condition by providing a base {py:class}`~autogen_agentchat.base.TerminationCondition` class and several implementations that inherit from it.\n", + "\n", + "A termination condition is a callable that takes a sequece of {py:class}`~autogen_agentchat.messages.AgentEvent` or {py:class}`~autogen_agentchat.messages.ChatMessage` objects **since the last time the condition was called**, and returns a {py:class}`~autogen_agentchat.messages.StopMessage` if the conversation should be terminated, or `None` otherwise.\n", + "Once a termination condition has been reached, it must be reset by calling {py:meth}`~autogen_agentchat.base.TerminationCondition.reset` before it can be used again.\n", + "\n", + "Some important things to note about termination conditions: \n", + "- They are stateful but reset automatically after each run ({py:meth}`~autogen_agentchat.base.TaskRunner.run` or {py:meth}`~autogen_agentchat.base.TaskRunner.run_stream`) is finished.\n", + "- They can be combined using the AND and OR operators.\n", + "\n", + "```{note}\n", + "For group chat teams (i.e., {py:class}`~autogen_agentchat.teams.RoundRobinGroupChat`,\n", + "{py:class}`~autogen_agentchat.teams.SelectorGroupChat`, and {py:class}`~autogen_agentchat.teams.Swarm`),\n", + "the termination condition is called after each agent responds.\n", + "While a response may contain multiple inner messages, the team calls its termination condition just once for all the messages from a single response.\n", + "So the condition is called with the \"delta sequence\" of messages since the last time it was called.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Built-In Termination Conditions: \n", + "1. {py:class}`~autogen_agentchat.conditions.MaxMessageTermination`: Stops after a specified number of messages have been produced, including both agent and task messages.\n", + "2. {py:class}`~autogen_agentchat.conditions.TextMentionTermination`: Stops when specific text or string is mentioned in a message (e.g., \"TERMINATE\").\n", + "3. {py:class}`~autogen_agentchat.conditions.TokenUsageTermination`: Stops when a certain number of prompt or completion tokens are used. This requires the agents to report token usage in their messages.\n", + "4. {py:class}`~autogen_agentchat.conditions.TimeoutTermination`: Stops after a specified duration in seconds.\n", + "5. {py:class}`~autogen_agentchat.conditions.HandoffTermination`: Stops when a handoff to a specific target is requested. Handoff messages can be used to build patterns such as {py:class}`~autogen_agentchat.teams.Swarm`. This is useful when you want to pause the run and allow application or user to provide input when an agent hands off to them.\n", + "6. {py:class}`~autogen_agentchat.conditions.SourceMatchTermination`: Stops after a specific agent responds.\n", + "7. {py:class}`~autogen_agentchat.conditions.ExternalTermination`: Enables programmatic control of termination from outside the run. This is useful for UI integration (e.g., \"Stop\" buttons in chat interfaces).\n", + "8. {py:class}`~autogen_agentchat.conditions.StopMessageTermination`: Stops when a {py:class}`~autogen_agentchat.messages.StopMessage` is produced by an agent." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To demonstrate the characteristics of termination conditions, we'll create a team consisting of two agents: a primary agent responsible for text generation and a critic agent that reviews and provides feedback on the generated text." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination\n", + "from autogen_agentchat.teams import RoundRobinGroupChat\n", + "from autogen_agentchat.ui import Console\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", + "\n", + "model_client = OpenAIChatCompletionClient(\n", + " model=\"gpt-4o\",\n", + " temperature=1,\n", + " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY env variable set.\n", + ")\n", + "\n", + "# Create the primary agent.\n", + "primary_agent = AssistantAgent(\n", + " \"primary\",\n", + " model_client=model_client,\n", + " system_message=\"You are a helpful AI assistant.\",\n", + ")\n", + "\n", + "# Create the critic agent.\n", + "critic_agent = AssistantAgent(\n", + " \"critic\",\n", + " model_client=model_client,\n", + " system_message=\"Provide constructive feedback for every message. Respond with 'APPROVE' to when your feedbacks are addressed.\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's explore how termination conditions automatically reset after each `run` or `run_stream` call, allowing the team to resume its conversation from where it left off." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Write a unique, Haiku about the weather in Paris\n", + "---------- primary ----------\n", + "Gentle rain whispers, \n", + "Cobblestones glisten softly— \n", + "Paris dreams in gray.\n", + "[Prompt tokens: 30, Completion tokens: 19]\n", + "---------- critic ----------\n", + "The Haiku captures the essence of a rainy day in Paris beautifully, and the imagery is vivid. However, it's important to ensure the use of the traditional 5-7-5 syllable structure for Haikus. Your current Haiku lines are composed of 4-7-5 syllables, which slightly deviates from the form. Consider revising the first line to fit the structure.\n", + "\n", + "For example:\n", + "Soft rain whispers down, \n", + "Cobblestones glisten softly — \n", + "Paris dreams in gray.\n", + "\n", + "This revision maintains the essence of your original lines while adhering to the traditional Haiku structure.\n", + "[Prompt tokens: 70, Completion tokens: 120]\n", + "---------- Summary ----------\n", + "Number of messages: 3\n", + "Finish reason: Maximum number of messages 3 reached, current message count: 3\n", + "Total prompt tokens: 100\n", + "Total completion tokens: 139\n", + "Duration: 3.34 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a unique, Haiku about the weather in Paris'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=30, completion_tokens=19), content='Gentle rain whispers, \\nCobblestones glisten softly— \\nParis dreams in gray.'), TextMessage(source='critic', models_usage=RequestUsage(prompt_tokens=70, completion_tokens=120), content=\"The Haiku captures the essence of a rainy day in Paris beautifully, and the imagery is vivid. However, it's important to ensure the use of the traditional 5-7-5 syllable structure for Haikus. Your current Haiku lines are composed of 4-7-5 syllables, which slightly deviates from the form. Consider revising the first line to fit the structure.\\n\\nFor example:\\nSoft rain whispers down, \\nCobblestones glisten softly — \\nParis dreams in gray.\\n\\nThis revision maintains the essence of your original lines while adhering to the traditional Haiku structure.\")], stop_reason='Maximum number of messages 3 reached, current message count: 3')" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "max_msg_termination = MaxMessageTermination(max_messages=3)\n", + "round_robin_team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=max_msg_termination)\n", + "\n", + "# Use asyncio.run(...) if you are running this script as a standalone script.\n", + "await Console(round_robin_team.run_stream(task=\"Write a unique, Haiku about the weather in Paris\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The conversation stopped after reaching the maximum message limit. Since the primary agent didn't get to respond to the feedback, let's continue the conversation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- primary ----------\n", + "Thank you for your feedback. Here is the revised Haiku:\n", + "\n", + "Soft rain whispers down, \n", + "Cobblestones glisten softly — \n", + "Paris dreams in gray.\n", + "[Prompt tokens: 181, Completion tokens: 32]\n", + "---------- critic ----------\n", + "The revised Haiku now follows the traditional 5-7-5 syllable pattern, and it still beautifully captures the atmospheric mood of Paris in the rain. The imagery and flow are both clear and evocative. Well done on making the adjustment! \n", + "\n", + "APPROVE\n", + "[Prompt tokens: 234, Completion tokens: 54]\n", + "---------- primary ----------\n", + "Thank you for your kind words and approval. I'm glad the revision meets your expectations and captures the essence of Paris. If you have any more requests or need further assistance, feel free to ask!\n", + "[Prompt tokens: 279, Completion tokens: 39]\n", + "---------- Summary ----------\n", + "Number of messages: 3\n", + "Finish reason: Maximum number of messages 3 reached, current message count: 3\n", + "Total prompt tokens: 694\n", + "Total completion tokens: 125\n", + "Duration: 6.43 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=181, completion_tokens=32), content='Thank you for your feedback. Here is the revised Haiku:\\n\\nSoft rain whispers down, \\nCobblestones glisten softly — \\nParis dreams in gray.'), TextMessage(source='critic', models_usage=RequestUsage(prompt_tokens=234, completion_tokens=54), content='The revised Haiku now follows the traditional 5-7-5 syllable pattern, and it still beautifully captures the atmospheric mood of Paris in the rain. The imagery and flow are both clear and evocative. Well done on making the adjustment! \\n\\nAPPROVE'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=279, completion_tokens=39), content=\"Thank you for your kind words and approval. I'm glad the revision meets your expectations and captures the essence of Paris. If you have any more requests or need further assistance, feel free to ask!\")], stop_reason='Maximum number of messages 3 reached, current message count: 3')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Use asyncio.run(...) if you are running this script as a standalone script.\n", + "await Console(round_robin_team.run_stream())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The team continued from where it left off, allowing the primary agent to respond to the feedback." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, let's show how termination conditions can be combined using the AND (`&`) and OR (`|`) operators to create more complex termination logic. For example, we'll create a team that stops either after 10 messages are generated or when the critic agent approves a message.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Write a unique, Haiku about the weather in Paris\n", + "---------- primary ----------\n", + "Spring breeze gently hums, \n", + "Cherry blossoms in full bloom— \n", + "Paris wakes to life.\n", + "[Prompt tokens: 467, Completion tokens: 19]\n", + "---------- critic ----------\n", + "The Haiku beautifully captures the awakening of Paris in the spring. The imagery of a gentle spring breeze and cherry blossoms in full bloom effectively conveys the rejuvenating feel of the season. The final line, \"Paris wakes to life,\" encapsulates the renewed energy and vibrancy of the city. The Haiku adheres to the 5-7-5 syllable structure and portrays a vivid seasonal transformation in a concise and poetic manner. Excellent work!\n", + "\n", + "APPROVE\n", + "[Prompt tokens: 746, Completion tokens: 93]\n", + "---------- Summary ----------\n", + "Number of messages: 3\n", + "Finish reason: Text 'APPROVE' mentioned\n", + "Total prompt tokens: 1213\n", + "Total completion tokens: 112\n", + "Duration: 2.75 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a unique, Haiku about the weather in Paris'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=467, completion_tokens=19), content='Spring breeze gently hums, \\nCherry blossoms in full bloom— \\nParis wakes to life.'), TextMessage(source='critic', models_usage=RequestUsage(prompt_tokens=746, completion_tokens=93), content='The Haiku beautifully captures the awakening of Paris in the spring. The imagery of a gentle spring breeze and cherry blossoms in full bloom effectively conveys the rejuvenating feel of the season. The final line, \"Paris wakes to life,\" encapsulates the renewed energy and vibrancy of the city. The Haiku adheres to the 5-7-5 syllable structure and portrays a vivid seasonal transformation in a concise and poetic manner. Excellent work!\\n\\nAPPROVE')], stop_reason=\"Text 'APPROVE' mentioned\")" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "max_msg_termination = MaxMessageTermination(max_messages=10)\n", + "text_termination = TextMentionTermination(\"APPROVE\")\n", + "combined_termination = max_msg_termination | text_termination\n", + "\n", + "round_robin_team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=combined_termination)\n", + "\n", + "# Use asyncio.run(...) if you are running this script as a standalone script.\n", + "await Console(round_robin_team.run_stream(task=\"Write a unique, Haiku about the weather in Paris\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The conversation stopped after the critic agent approved the message, although it could have also stopped if 10 messages were generated.\n", + "\n", + "Alternatively, if we want to stop the run only when both conditions are met, we can use the AND (`&`) operator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "combined_termination = max_msg_termination & text_termination" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } }, - { - "data": { - "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a unique, Haiku about the weather in Paris'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=30, completion_tokens=19), content='Gentle rain whispers, \\nCobblestones glisten softly— \\nParis dreams in gray.'), TextMessage(source='critic', models_usage=RequestUsage(prompt_tokens=70, completion_tokens=120), content=\"The Haiku captures the essence of a rainy day in Paris beautifully, and the imagery is vivid. However, it's important to ensure the use of the traditional 5-7-5 syllable structure for Haikus. Your current Haiku lines are composed of 4-7-5 syllables, which slightly deviates from the form. Consider revising the first line to fit the structure.\\n\\nFor example:\\nSoft rain whispers down, \\nCobblestones glisten softly — \\nParis dreams in gray.\\n\\nThis revision maintains the essence of your original lines while adhering to the traditional Haiku structure.\")], stop_reason='Maximum number of messages 3 reached, current message count: 3')" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "max_msg_termination = MaxMessageTermination(max_messages=3)\n", - "round_robin_team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=max_msg_termination)\n", - "\n", - "# Use asyncio.run(...) if you are running this script as a standalone script.\n", - "await Console(round_robin_team.run_stream(task=\"Write a unique, Haiku about the weather in Paris\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The conversation stopped after reaching the maximum message limit. Since the primary agent didn't get to respond to the feedback, let's continue the conversation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- primary ----------\n", - "Thank you for your feedback. Here is the revised Haiku:\n", - "\n", - "Soft rain whispers down, \n", - "Cobblestones glisten softly — \n", - "Paris dreams in gray.\n", - "[Prompt tokens: 181, Completion tokens: 32]\n", - "---------- critic ----------\n", - "The revised Haiku now follows the traditional 5-7-5 syllable pattern, and it still beautifully captures the atmospheric mood of Paris in the rain. The imagery and flow are both clear and evocative. Well done on making the adjustment! \n", - "\n", - "APPROVE\n", - "[Prompt tokens: 234, Completion tokens: 54]\n", - "---------- primary ----------\n", - "Thank you for your kind words and approval. I'm glad the revision meets your expectations and captures the essence of Paris. If you have any more requests or need further assistance, feel free to ask!\n", - "[Prompt tokens: 279, Completion tokens: 39]\n", - "---------- Summary ----------\n", - "Number of messages: 3\n", - "Finish reason: Maximum number of messages 3 reached, current message count: 3\n", - "Total prompt tokens: 694\n", - "Total completion tokens: 125\n", - "Duration: 6.43 seconds\n" - ] - }, - { - "data": { - "text/plain": [ - "TaskResult(messages=[TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=181, completion_tokens=32), content='Thank you for your feedback. Here is the revised Haiku:\\n\\nSoft rain whispers down, \\nCobblestones glisten softly — \\nParis dreams in gray.'), TextMessage(source='critic', models_usage=RequestUsage(prompt_tokens=234, completion_tokens=54), content='The revised Haiku now follows the traditional 5-7-5 syllable pattern, and it still beautifully captures the atmospheric mood of Paris in the rain. The imagery and flow are both clear and evocative. Well done on making the adjustment! \\n\\nAPPROVE'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=279, completion_tokens=39), content=\"Thank you for your kind words and approval. I'm glad the revision meets your expectations and captures the essence of Paris. If you have any more requests or need further assistance, feel free to ask!\")], stop_reason='Maximum number of messages 3 reached, current message count: 3')" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Use asyncio.run(...) if you are running this script as a standalone script.\n", - "await Console(round_robin_team.run_stream())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The team continued from where it left off, allowing the primary agent to respond to the feedback." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, let's show how termination conditions can be combined using the AND (`&`) and OR (`|`) operators to create more complex termination logic. For example, we'll create a team that stops either after 10 messages are generated or when the critic agent approves a message.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "---------- user ----------\n", - "Write a unique, Haiku about the weather in Paris\n", - "---------- primary ----------\n", - "Spring breeze gently hums, \n", - "Cherry blossoms in full bloom— \n", - "Paris wakes to life.\n", - "[Prompt tokens: 467, Completion tokens: 19]\n", - "---------- critic ----------\n", - "The Haiku beautifully captures the awakening of Paris in the spring. The imagery of a gentle spring breeze and cherry blossoms in full bloom effectively conveys the rejuvenating feel of the season. The final line, \"Paris wakes to life,\" encapsulates the renewed energy and vibrancy of the city. The Haiku adheres to the 5-7-5 syllable structure and portrays a vivid seasonal transformation in a concise and poetic manner. Excellent work!\n", - "\n", - "APPROVE\n", - "[Prompt tokens: 746, Completion tokens: 93]\n", - "---------- Summary ----------\n", - "Number of messages: 3\n", - "Finish reason: Text 'APPROVE' mentioned\n", - "Total prompt tokens: 1213\n", - "Total completion tokens: 112\n", - "Duration: 2.75 seconds\n" - ] - }, - { - "data": { - "text/plain": [ - "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a unique, Haiku about the weather in Paris'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=467, completion_tokens=19), content='Spring breeze gently hums, \\nCherry blossoms in full bloom— \\nParis wakes to life.'), TextMessage(source='critic', models_usage=RequestUsage(prompt_tokens=746, completion_tokens=93), content='The Haiku beautifully captures the awakening of Paris in the spring. The imagery of a gentle spring breeze and cherry blossoms in full bloom effectively conveys the rejuvenating feel of the season. The final line, \"Paris wakes to life,\" encapsulates the renewed energy and vibrancy of the city. The Haiku adheres to the 5-7-5 syllable structure and portrays a vivid seasonal transformation in a concise and poetic manner. Excellent work!\\n\\nAPPROVE')], stop_reason=\"Text 'APPROVE' mentioned\")" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "max_msg_termination = MaxMessageTermination(max_messages=10)\n", - "text_termination = TextMentionTermination(\"APPROVE\")\n", - "combined_termination = max_msg_termination | text_termination\n", - "\n", - "round_robin_team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=combined_termination)\n", - "\n", - "# Use asyncio.run(...) if you are running this script as a standalone script.\n", - "await Console(round_robin_team.run_stream(task=\"Write a unique, Haiku about the weather in Paris\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The conversation stopped after the critic agent approved the message, although it could have also stopped if 10 messages were generated.\n", - "\n", - "Alternatively, if we want to stop the run only when both conditions are met, we can use the AND (`&`) operator." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "combined_termination = max_msg_termination & text_termination" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/faq.md b/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/faq.md index 0894df2528d9..26c52628d380 100644 --- a/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/faq.md +++ b/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/faq.md @@ -13,8 +13,54 @@ A: You can specify the directory where files are stored by setting the `--appdir ## Q: Can I use other models with AutoGen Studio? -Yes. AutoGen standardizes on the openai model api format, and you can use any api server that offers an openai compliant endpoint. In the AutoGen Studio UI, each agent has an `model_client` field where you can input your model endpoint details including `model`, `api key`, `base url`, `model type` and `api version`. For Azure OpenAI models, you can find these details in the Azure portal. Note that for Azure OpenAI, the `model name` is the deployment id or engine, and the `model type` is "azure". -For other OSS models, we recommend using a server such as vllm, LMStudio, Ollama, to instantiate an openai compliant endpoint. +Yes. AutoGen standardizes on the openai model api format, and you can use any api server that offers an openai compliant endpoint. + +AutoGen Studio is based on declaritive specifications which applies to models as well. Agents can include a model_client field which specifies the model endpoint details including `model`, `api_key`, `base_url`, `model type`. + +An example of the openai model client is shown below: + +```json +{ + "model": "gpt-4o-mini", + "model_type": "OpenAIChatCompletionClient", + "api_key": "your-api-key" +} +``` + +An example of the azure openai model client is shown below: + +```json +{ + "model": "gpt-4o-mini", + "model_type": "AzureOpenAIChatCompletionClient", + "azure_deployment": "gpt-4o-mini", + "api_version": "2024-02-15-preview", + "azure_endpoint": "https://your-endpoint.openai.azure.com/", + "api_key": "your-api-key", + "component_type": "model" +} +``` + +Have a local model server like Ollama, vLLM or LMStudio that provide an OpenAI compliant endpoint? You can use that as well. + +```json +{ + "model": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF", + "model_type": "OpenAIChatCompletionClient", + "base_url": "http://localhost:1234/v1", + "api_version": "1.0", + "component_type": "model", + "model_capabilities": { + "vision": false, + "function_calling": true, + "json_output": false + } +} +``` + +```{caution} +It is important that you add the `model_capabilities` field to the model client specification for custom models. This is used by the framework instantiate and use the model correctly. Also, the `AssistantAgent` and many other agents in AgentChat require the model to have the `function_calling` capability. +``` ## Q: The server starts but I can't access the UI diff --git a/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md b/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md index f2ea45c1fdb3..12a409e157df 100644 --- a/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md +++ b/python/packages/autogen-core/docs/src/user-guide/autogenstudio-user-guide/usage.md @@ -13,27 +13,27 @@ The expected usage behavior is that developers use the provided Team Builder int ## Building an Agent Team -AutoGen Studio is tied very closely with all of the component abstractions provided by AutoGen AgentChat. This includes - {py:class}`~autogen_agentchat.teams`, {py:class}`~autogen_agentchat.agents`, {py:class}`~autogen_core.models`, {py:class}`~autogen_core.tools`, {py:class}`~autogen_agentchat.conditions`. +AutoGen Studio is tied very closely with all of the component abstractions provided by AutoGen AgentChat. This includes - {py:class}`~autogen_agentchat.teams`, {py:class}`~autogen_agentchat.agents`, {py:class}`~autogen_core.models`, {py:class}`~autogen_core.tools`, termination {py:class}`~autogen_agentchat.conditions`. Users can define these components in the Team Builder interface either via a declarative specification or by dragging and dropping components from a component library. -## Testing an Agent Team +## Interactively Running Teams -AutoGen Studio Playground allows users to interactively test teams on tasks and review resulting artifacts (such as images, code, and documents). +AutoGen Studio Playground allows users to interactively test teams on tasks and review resulting artifacts (such as images, code, and text). Users can also review the “inner monologue” of team as they address tasks, and view profiling information such as costs associated with the run (such as number of turns, number of tokens etc.), and agent actions (such as whether tools were called and the outcomes of code execution). ## Importing and Reusing Team Configurations -AutoGen Studio provides a Gallery view where users can import components from 3rd party community sources. This allows users to reuse and share team configurations with others. +AutoGen Studio provides a Gallery view which provides a built-in default gallery. A Gallery is simply is a collection of components - teams, agents, models tools etc. Furthermore, users can import components from 3rd party community sources either by providing a URL to a JSON Gallery spec or pasting in the gallery JSON. This allows users to reuse and share team configurations with others. - Gallery -> New Gallery -> Import -- Set as default gallery -- Reuse components in Team Builder +- Set as default gallery (in side bar, by clicking pin icon) +- Reuse components in Team Builder. Team Builder -> Sidebar -> From Gallery ### Using AutoGen Studio Teams in a Python Application -An exported team can be easily integrated into any Python application using the `TeamManager` class with just two lines of code. Underneath, the `TeamManager` rehydrates the workflow specification into AutoGen agents that are subsequently used to address tasks. +An exported team can be easily integrated into any Python application using the `TeamManager` class with just two lines of code. Underneath, the `TeamManager` rehydrates the team specification into AutoGen AgentChat agents that are subsequently used to address tasks. ```python @@ -44,12 +44,14 @@ result_stream = tm.run(task="What is the weather in New York?", team_config="te ``` +To export a team configuration, click on the export button in the Team Builder interface. This will generate a JSON file that can be used to rehydrate the team in a Python application. + +Similarly, the team launch command above can be wrapped into a Dockerfile that can be deployed on cloud services like Azure Container Apps or Azure Web Apps. --> diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/local-llms-ollama-litellm.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/local-llms-ollama-litellm.ipynb index 7e236d1006a5..400985577698 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/local-llms-ollama-litellm.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/local-llms-ollama-litellm.ipynb @@ -15,10 +15,10 @@ "```\n", "curl -fsSL https://ollama.com/install.sh | sh\n", "\n", - "ollama pull llama3:instruct\n", + "ollama pull llama3.2:1b\n", "\n", "pip install 'litellm[proxy]'\n", - "litellm --model ollama/llama3:instruct\n", + "litellm --model ollama/llama3.2:1b\n", "``` \n", "\n", "This will run the proxy server and it will be available at 'http://0.0.0.0:4000/'." @@ -48,7 +48,7 @@ " default_subscription,\n", " message_handler,\n", ")\n", - "from autogen_core.components.model_context import BufferedChatCompletionContext\n", + "from autogen_core.model_context import BufferedChatCompletionContext\n", "from autogen_core.models import (\n", " AssistantMessage,\n", " ChatCompletionClient,\n", @@ -74,9 +74,14 @@ "def get_model_client() -> OpenAIChatCompletionClient: # type: ignore\n", " \"Mimic OpenAI API using Local LLM Server.\"\n", " return OpenAIChatCompletionClient(\n", - " model=\"gpt-4o\", # Need to use one of the OpenAI models as a placeholder for now.\n", + " model=\"llama3.2:1b\",\n", " api_key=\"NotRequiredSinceWeAreLocal\",\n", " base_url=\"http://0.0.0.0:4000\",\n", + " model_capabilities={\n", + " \"json_output\": False,\n", + " \"vision\": False,\n", + " \"function_calling\": True,\n", + " },\n", " )" ] }, @@ -225,7 +230,7 @@ ], "metadata": { "kernelspec": { - "display_name": "pyautogen", + "display_name": ".venv", "language": "python", "name": "python3" }, diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/core-concepts/application-stack.md b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/core-concepts/application-stack.md index 71b380ebfd6f..074e4b5153a8 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/core-concepts/application-stack.md +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/core-concepts/application-stack.md @@ -20,7 +20,7 @@ implementation of the contracts determines how agents handle messages. The behavior contract is also sometimes referred to as the message protocol. It is the developer's responsibility to implement the behavior contract. Multi-agent patterns emerge from these behavior contracts -(see [Multi-Agent Design Patterns](../design-patterns/index.md)). +(see [Multi-Agent Design Patterns](../design-patterns/intro.md)). ## An Example Application diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/core-concepts/index.md b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/core-concepts/index.md deleted file mode 100644 index 0862976a4d9c..000000000000 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/core-concepts/index.md +++ /dev/null @@ -1,14 +0,0 @@ -# Core Concepts - -The following sections describe the main concepts of the Core API -and the system architecture. - -```{toctree} -:maxdepth: 1 - -agent-and-multi-agent-application -architecture -application-stack -agent-identity-and-lifecycle -topic-and-subscription -``` diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/code-execution-groupchat.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/code-execution-groupchat.ipynb new file mode 100644 index 000000000000..6daa85207aac --- /dev/null +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/code-execution-groupchat.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Code Execution\n", + "\n", + "In this section we explore creating custom agents to handle code generation and execution. These tasks can be handled using the provided Agent implementations found here {py:meth}`~autogen_agentchat.agents.AssistantAgent`, {py:meth}`~autogen_agentchat.agents.CodeExecutorAgent`; but this guide will show you how to implement custom, lightweight agents that can replace their functionality. This simple example implements two agents that create a plot of Tesla's and Nvidia's stock returns.\n", + "\n", + "We first define the agent classes and their respective procedures for \n", + "handling messages.\n", + "We create two agent classes: `Assistant` and `Executor`. The `Assistant`\n", + "agent writes code and the `Executor` agent executes the code.\n", + "We also create a `Message` data class, which defines the messages that are passed between\n", + "the agents.\n", + "\n", + "```{attention}\n", + "Code generated in this example is run within a [Docker](https://www.docker.com/) container. Please ensure Docker is [installed](https://docs.docker.com/get-started/get-docker/) and running prior to running the example. Local code execution is available ({py:class}`~autogen_ext.code_executors.local.LocalCommandLineCodeExecutor`) but is not recommended due to the risk of running LLM generated code in your local environment.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import re\n", + "from dataclasses import dataclass\n", + "from typing import List\n", + "\n", + "from autogen_core import DefaultTopicId, MessageContext, RoutedAgent, default_subscription, message_handler\n", + "from autogen_core.code_executor import CodeBlock, CodeExecutor\n", + "from autogen_core.models import (\n", + " AssistantMessage,\n", + " ChatCompletionClient,\n", + " LLMMessage,\n", + " SystemMessage,\n", + " UserMessage,\n", + ")\n", + "\n", + "\n", + "@dataclass\n", + "class Message:\n", + " content: str\n", + "\n", + "\n", + "@default_subscription\n", + "class Assistant(RoutedAgent):\n", + " def __init__(self, model_client: ChatCompletionClient) -> None:\n", + " super().__init__(\"An assistant agent.\")\n", + " self._model_client = model_client\n", + " self._chat_history: List[LLMMessage] = [\n", + " SystemMessage(\n", + " content=\"\"\"Write Python script in markdown block, and it will be executed.\n", + "Always save figures to file in the current directory. Do not use plt.show(). All code required to complete this task must be contained within a single response.\"\"\",\n", + " )\n", + " ]\n", + "\n", + " @message_handler\n", + " async def handle_message(self, message: Message, ctx: MessageContext) -> None:\n", + " self._chat_history.append(UserMessage(content=message.content, source=\"user\"))\n", + " result = await self._model_client.create(self._chat_history)\n", + " print(f\"\\n{'-'*80}\\nAssistant:\\n{result.content}\")\n", + " self._chat_history.append(AssistantMessage(content=result.content, source=\"assistant\")) # type: ignore\n", + " await self.publish_message(Message(content=result.content), DefaultTopicId()) # type: ignore\n", + "\n", + "\n", + "def extract_markdown_code_blocks(markdown_text: str) -> List[CodeBlock]:\n", + " pattern = re.compile(r\"```(?:\\s*([\\w\\+\\-]+))?\\n([\\s\\S]*?)```\")\n", + " matches = pattern.findall(markdown_text)\n", + " code_blocks: List[CodeBlock] = []\n", + " for match in matches:\n", + " language = match[0].strip() if match[0] else \"\"\n", + " code_content = match[1]\n", + " code_blocks.append(CodeBlock(code=code_content, language=language))\n", + " return code_blocks\n", + "\n", + "\n", + "@default_subscription\n", + "class Executor(RoutedAgent):\n", + " def __init__(self, code_executor: CodeExecutor) -> None:\n", + " super().__init__(\"An executor agent.\")\n", + " self._code_executor = code_executor\n", + "\n", + " @message_handler\n", + " async def handle_message(self, message: Message, ctx: MessageContext) -> None:\n", + " code_blocks = extract_markdown_code_blocks(message.content)\n", + " if code_blocks:\n", + " result = await self._code_executor.execute_code_blocks(\n", + " code_blocks, cancellation_token=ctx.cancellation_token\n", + " )\n", + " print(f\"\\n{'-'*80}\\nExecutor:\\n{result.output}\")\n", + " await self.publish_message(Message(content=result.output), DefaultTopicId())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You might have already noticed, the agents' logic, whether it is using model or code executor,\n", + "is completely decoupled from\n", + "how messages are delivered. This is the core idea: the framework provides\n", + "a communication infrastructure, and the agents are responsible for their own\n", + "logic. We call the communication infrastructure an **Agent Runtime**.\n", + "\n", + "Agent runtime is a key concept of this framework. Besides delivering messages,\n", + "it also manages agents' lifecycle. \n", + "So the creation of agents are handled by the runtime.\n", + "\n", + "The following code shows how to register and run the agents using \n", + "{py:class}`~autogen_core.SingleThreadedAgentRuntime`,\n", + "a local embedded agent runtime implementation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--------------------------------------------------------------------------------\n", + "Assistant:\n", + "```python\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import yfinance as yf\n", + "\n", + "# Define the ticker symbols for NVIDIA and Tesla\n", + "tickers = ['NVDA', 'TSLA']\n", + "\n", + "# Download the stock data from Yahoo Finance starting from 2024-01-01\n", + "start_date = '2024-01-01'\n", + "end_date = pd.to_datetime('today').strftime('%Y-%m-%d')\n", + "\n", + "# Download the adjusted closing prices\n", + "stock_data = yf.download(tickers, start=start_date, end=end_date)['Adj Close']\n", + "\n", + "# Calculate the daily returns\n", + "returns = stock_data.pct_change().dropna()\n", + "\n", + "# Plot the cumulative returns for each stock\n", + "cumulative_returns = (1 + returns).cumprod()\n", + "\n", + "plt.figure(figsize=(10, 6))\n", + "plt.plot(cumulative_returns.index, cumulative_returns['NVDA'], label='NVIDIA', color='green')\n", + "plt.plot(cumulative_returns.index, cumulative_returns['TSLA'], label='Tesla', color='red')\n", + "plt.title('NVIDIA vs Tesla Stock Returns YTD (2024)')\n", + "plt.xlabel('Date')\n", + "plt.ylabel('Cumulative Return')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "\n", + "# Save the plot to a file\n", + "plt.savefig('nvidia_vs_tesla_ytd_returns.png')\n", + "```\n", + "\n", + "--------------------------------------------------------------------------------\n", + "Executor:\n", + "Traceback (most recent call last):\n", + " File \"/workspace/tmp_code_fd7395dcad4fbb74d40c981411db604e78e1a17783ca1fab3aaec34ff2c3fdf0.python\", line 1, in \n", + " import pandas as pd\n", + "ModuleNotFoundError: No module named 'pandas'\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "Assistant:\n", + "It seems like the necessary libraries are not available in your environment. However, since I can't install packages or check the environment directly from here, you'll need to make sure that the appropriate packages are installed in your working environment. Once the modules are available, the script provided will execute properly.\n", + "\n", + "Here's how you can install the required packages using pip (make sure to run these commands in your terminal or command prompt):\n", + "\n", + "```bash\n", + "pip install pandas matplotlib yfinance\n", + "```\n", + "\n", + "Let me provide you the script again for reference:\n", + "\n", + "```python\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import yfinance as yf\n", + "\n", + "# Define the ticker symbols for NVIDIA and Tesla\n", + "tickers = ['NVDA', 'TSLA']\n", + "\n", + "# Download the stock data from Yahoo Finance starting from 2024-01-01\n", + "start_date = '2024-01-01'\n", + "end_date = pd.to_datetime('today').strftime('%Y-%m-%d')\n", + "\n", + "# Download the adjusted closing prices\n", + "stock_data = yf.download(tickers, start=start_date, end=end_date)['Adj Close']\n", + "\n", + "# Calculate the daily returns\n", + "returns = stock_data.pct_change().dropna()\n", + "\n", + "# Plot the cumulative returns for each stock\n", + "cumulative_returns = (1 + returns).cumprod()\n", + "\n", + "plt.figure(figsize=(10, 6))\n", + "plt.plot(cumulative_returns.index, cumulative_returns['NVDA'], label='NVIDIA', color='green')\n", + "plt.plot(cumulative_returns.index, cumulative_returns['TSLA'], label='Tesla', color='red')\n", + "plt.title('NVIDIA vs Tesla Stock Returns YTD (2024)')\n", + "plt.xlabel('Date')\n", + "plt.ylabel('Cumulative Return')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "\n", + "# Save the plot to a file\n", + "plt.savefig('nvidia_vs_tesla_ytd_returns.png')\n", + "```\n", + "\n", + "Make sure to install the packages in the environment where you run this script. Feel free to ask if you have further questions or issues!\n", + "\n", + "--------------------------------------------------------------------------------\n", + "Executor:\n", + "[*********************100%***********************] 2 of 2 completed\n", + "\n", + "\n", + "--------------------------------------------------------------------------------\n", + "Assistant:\n", + "It looks like the data fetching process completed successfully. You should now have a plot saved as `nvidia_vs_tesla_ytd_returns.png` in your current directory. If you have any additional questions or need further assistance, feel free to ask!\n" + ] + } + ], + "source": [ + "import tempfile\n", + "\n", + "from autogen_core import SingleThreadedAgentRuntime\n", + "from autogen_ext.code_executors.docker import DockerCommandLineCodeExecutor\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", + "\n", + "work_dir = tempfile.mkdtemp()\n", + "\n", + "# Create an local embedded runtime.\n", + "runtime = SingleThreadedAgentRuntime()\n", + "\n", + "async with DockerCommandLineCodeExecutor(work_dir=work_dir) as executor: # type: ignore[syntax]\n", + " # Register the assistant and executor agents by providing\n", + " # their agent types, the factory functions for creating instance and subscriptions.\n", + " await Assistant.register(\n", + " runtime,\n", + " \"assistant\",\n", + " lambda: Assistant(\n", + " OpenAIChatCompletionClient(\n", + " model=\"gpt-4o\",\n", + " # api_key=\"YOUR_API_KEY\"\n", + " )\n", + " ),\n", + " )\n", + " await Executor.register(runtime, \"executor\", lambda: Executor(executor))\n", + "\n", + " # Start the runtime and publish a message to the assistant.\n", + " runtime.start()\n", + " await runtime.publish_message(\n", + " Message(\"Create a plot of NVIDA vs TSLA stock returns YTD from 2024-01-01.\"), DefaultTopicId()\n", + " )\n", + " await runtime.stop_when_idle()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From the agent's output, we can see the plot of Tesla's and Nvidia's stock returns\n", + "has been created." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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U716dTw9PWnQoAF33313odc+fvw4Dz74IM2aNcPT05Nq1aoxcuRIIiIiLhtXeHg4QK7yYisPDw9bZcOdd97J559/DuQu6bdKTk7mySefpG7duri7u9OsWTP+97//YRhGnuv+/PPPXHXVVXh5eREYGEivXr1Yvnx5oXFOmzYNFxcXnn766cu+p/zeR5cuXUhMTCQ6OjpPLJ06dcLT05OgoCDGjBnDyZMnbfv79OnD77//zvHjx23vuX79+sDFKSOXfp9Xr16dZ0qDddrGtm3b6NWrF15eXjz//PO2cvH//e9/TJkyhUaNGuHu7k6XLl3YsmVLrutGRUVx1113ERoairu7O7Vr12bo0KGX/TlPmjSJ6tWr85///AfDMEhKSuKxxx6je/fu/Oc//yn297Mw9erVY+rUqWRkZPDuu+9e9viFCxfStWtXfHx8cm3v06dPnmOHDx8OwP79+23b9u3bx759+7j//vtxcblYqPnggw9iGEaukvRevXrh5JT7V9FevXoRFBSU65o5PfroowwfPpxrrrmmwPfQqVMngoKCivRvkYhIRaMSdxGRCqBBgwbccccdfPPNNzz33HO2Eub8juvRowezZ8/mww8/xNnZ2bbPmrTfeuutpYrl9ttvZ/ny5axYsYKmTZsWeuyiRYtISkpizJgx1KpViz59+jB9+vTLxtC5c2caNmzI7NmzGT9+fK59s2bNIjAwkP79+wPwn//8h7lz5/LQQw/RsmVLYmNjWb9+Pfv376djx44lfp8PPPAAU6dO5a677uKRRx7h2LFjfPbZZ+zYsYMNGzbg6upKdHQ0/fr1o0aNGjz33HMEBAQQERHB/PnzC732li1b2LhxI2PGjCE0NJSIiAi+/PJL+vTpw759+/Dy8irw3Hr16gGWkuYXX3yxwBLdBx54gDNnzuRb9mwYBkOGDOGvv/7innvuoX379ixbtoynn36a06dP8+GHH9qOfe2113j11Vfp0aMHr7/+Om5ubvz999/8+eef9OvXL997T5kyhf/85z88//zzBU63uBxrIhwQEGDb9tZbb/HSSy8xatQo7r33XmJiYvj000/p1asXO3bsICAggBdeeIELFy5w6tQp2/u4NJksqtjYWAYOHMiYMWMYN24cNWvWtO2bMWMGiYmJPPDAA5hMJt59911uvvlmjh49iqurKwC33HILe/fu5eGHH6Z+/fpER0ezYsUKTpw4YfvQID/+/v588sknjBw5km+//ZZ9+/Zx9uxZ/vjjjzIpye7evTuNGjVixYoVhR6XmZnJli1b+O9//1uk60ZFRQFQvXp127YdO3YAlr/jOYWEhBAaGmrbX5CkpCSSkpJyXdNqzpw5bNy4kf3791/2Q5COHTtWyiZ3IiIqcRcRcSBrifuWLVuM8PBww8XFxXjkkUds+/MrD/78888NwFi2bJltW3Z2tlGnTh2je/fuuY4tbom7YRjGjh07DMB4/PHHC43DMAzjpptuMq6++mrb6ylTphguLi5GdHT0Zd/7xIkTDVdXV+P8+fO2benp6UZAQIBx991327b5+/sbEyZMuOz1CnNpefC6devyLSNfunRpru0LFiyw/XwKwyUl7ikpKXmO2bRpkwEYP/74Y6HXSklJMZo1a2YARr169Yw777zT+O6774yzZ89e9n1ZLVy40ACMN998M9f2ESNGGCaTyThy5IhhGIZx+PBhw8nJyRg+fHieUmOz2Wz7OmeJ+8cff2yYTCbjjTfeKPR9WPXu3dto3ry5ERMTY8TExBgHDhwwnn76aQPIVTYfERFhODs7G2+99Vau860lzTm3F1Tibv37dOzYsVzb//rrLwPIVabdu3dvAzC++uqrXMday8WrVauW69n89ddfDcD47bffDMMwjLi4uALLyovqpptuMvz9/Q1nZ2dj4sSJhR5b0hJ3q6FDhxpArikSlzpy5IgBGJ9++mmR4r/nnnsMZ2dn49ChQ7Zt7733ngEYJ06cyHN8ly5djG7duhV6Tes0nkvL01NSUoywsDDb98n6M82vxN0wDOP+++83PD09i/Q+REQqEpW4i4hUEA0bNuT2229nypQphXYxHj16NK6urrnK3NesWcPp06dLVd5uZR2NTExMLPS42NhYli1bxtixY23bbrnlFkwmE7Nnz77sfUaPHk1mZmau0ejly5cTHx/P6NGjbdsCAgL4+++/c3UvL605c+bg7+9P3759OXfunO1Pp06d8PHx4a+//rLdG2Dx4sVkZmYW+fo5G2ZlZmYSGxtL48aNCQgIuGxpvqenJ3///betdHzq1Kncc8891K5dm4cffrhIy0ctWbIEZ2dnHnnkkVzbn3zySQzDsHXbX7hwIWazmZdffjlPqXF+I7nvvvsujz76KJMnTy50NYFLHThwgBo1alCjRg2aN2/Oe++9x5AhQ5g6dartmPnz52M2mxk1alSun0mtWrVo0qSJ7WdiT+7u7tx111357hs9ejSBgYG219aS6qNHjwKWn5ObmxurV6/OMy2iqD7//HMyMjKoW7cuL730UomuUVRF+XsdGxsLkOt9F2TGjBl89913PPnkkzRp0sS23dqwzt3dPc85Hh4e+Xavt1q7di2vvfYao0aN4rrrrsu175133iEzM5Pnn3/+srFZ30NqamqBzeRERCoqJegiIhXIiy++SFZWVqFz0atVq0b//v1ZsGABaWlpgOWXZRcXF0aNGlXqGJKSkgDw9fUt9LhZs2aRmZlJhw4dOHLkCEeOHOH8+fN07dqV6dOnX/Y+7dq1o3nz5syaNSvXNatXr57rl/N3332XPXv2ULduXa666ipeffVVW5JUUocPH+bChQsEBwfbEkfrn6SkJNu86N69e3PLLbfw2muvUb16dYYOHcoPP/xw2SQ5NTWVl19+2Tb/u3r16tSoUYP4+HguXLhw2fj8/f159913iYiIICIigu+++45mzZrx2Wef8cYbb1z2/OPHjxMSEpLnZ2jtWn/8+HHAMt/dycmJli1bXvaaa9as4dlnn+XZZ58t9rzz+vXrs2LFCpYtW8YXX3xBnTp1iImJwcPDw3bM4cOHMQyDJk2a5PmZ7N+/P89cdXuoU6cObm5u+e4LCwvL9dqatFqTcXd3dyZPnswff/xBzZo16dWrF++++66t7LsowsLCCA4OplWrVrk+1CkLRf17DeTbpyCndevWcc8999C/f/88Xfit7yO/vyNpaWkFvs8DBw4wfPhwWrdubVthwioiIoL33nuPt956q8jTGazvQV3cRaSy0Rx0EZEKpGHDhowbN44pU6bw3HPPFXjcuHHjWLx4MYsXL2bIkCHMmzfPNle6tPbs2QNA48aNCz3OmoTn18wMLCONDRs2LPQao0eP5q233uLcuXP4+vqyaNEixo4dm6u51KhRo7jmmmtYsGABy5cv57333mPy5MnMnz+/xEu6mc1mgoODC/wgwfp9tK6zvHnzZn777TeWLVvG3Xffzfvvv8/mzZsLTBYefvhhfvjhB1vjL39/f0wmE2PGjCn22t/16tXj7rvvZvjw4TRs2JDp06eXeN53abRq1Yr4+Hh++uknHnjgARo0aFDkc729vbnhhhtsr6+++mo6duzI888/zyef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RGRuLDtJREREREREZAEYoBMRERERERFZAAboRERERERERBaAAToRERERERGRBWCATkRERERERGQBGKATERERERERWQAG6EREREREREQWgAE6ERERERERkQVggE5ERERERERkARigExERZSF9+/aFRqOBRqOBnZ0dvLy80KxZMyxZsgR6vT7F+1m6dCnc3d3Tr6FERERZEAN0IiKiLKZly5YICAjAvXv3sH37djRq1AjDhw9H27ZtERcXZ+7mERERZVkM0ImIiLIYBwcH5MmTB/ny5UOVKlUwbtw4bNy4Edu3b8fSpUsBALNnz0b58uWRLVs2FChQAEOHDkV4eDgAYP/+/ejXrx9CQ0Pjs/ETJ04EAERHR+Pzzz9Hvnz5kC1bNtSsWRP79+83zxslIiLKZBigExERERo3boyKFStiw4YNAACtVot58+bh8uXLWLZsGfbu3YsvvvgCAFCnTh38+OOPcHV1RUBAAAICAvD5558DAD7++GMcO3YMa9aswYULF9C1a1e0bNkSN2/eNNt7IyIiyiw0iqIo5m4EERERZYy+ffsiJCQE//7772vPde/eHRcuXMCVK1dee27dunUYPHgwnjx5AkDGoH/22WcICQmJ38bPzw9FihSBn58fvL2949c3bdoUNWrUwJQpU1R/P0RERNbE1twNICIiIsugKAo0Gg0AYPfu3Zg6dSquXbuGsLAwxMXFISoqChEREXB2dk7y9RcvXoROp0OJEiUSrY+OjkauXLnSvf1ERESZHQN0IiIiAgBcvXoVPj4+uHfvHtq2bYshQ4Zg8uTJyJkzJw4fPowBAwYgJibmjQF6eHg4bGxscObMGdjY2CR6Lnv27BnxFoiIiDI1BuhERESEvXv34uLFixgxYgTOnDkDvV6PWbNmQauVcjVr165NtL29vT10Ol2idZUrV4ZOp0NwcDDeeeedDGs7ERGRtWCATkRElMVER0cjMDAQOp0OQUFB2LFjB6ZOnYq2bduid+/euHTpEmJjY/HTTz+hXbt2OHLkCObPn59oH4ULF0Z4eDj27NmDihUrwtnZGSVKlECvXr3Qu3dvzJo1C5UrV8bjx4+xZ88eVKhQAW3atDHTOyYiIsocWMWdiIgoi9mxYwfy5s2LwoULo2XLlti3bx/mzZuHjRs3wsbGBhUrVsTs2bMxffp0lCtXDqtWrcLUqVMT7aNOnToYPHgwunXrBg8PD/zwww8AgD/++AO9e/fGqFGjULJkSXTo0AGnTp1CwYIFzfFWiYiIMhVWcSciIiIiIiKyAMygExEREREREVkABuhEREREREREFoABOhEREREREZEFYIBOREREREREZAEYoBMRERERERFZAAboRERERERERBaAAToRERERERGRBWCATkRERERERGQBGKATERERERERWQAG6EREREREREQWgAE6ERERERERkQVggE5ERERERERkARigExEREREREVkABuhEREREREREFoABOhEREREREZEFYIBOREREREREZAEYoBMRERERERFZgP8B9f6wiLEP2lsAAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import Image\n", + "\n", + "Image(filename=f\"{work_dir}/nvidia_vs_tesla_ytd_returns.png\") # type: ignore" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "AutoGen also supports a distributed agent runtime, which can host agents running on\n", + "different processes or machines, with different identities, languages and dependencies.\n", + "\n", + "To learn how to use agent runtime, communication, message handling, and subscription, please continue\n", + "reading the sections following this quick start." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/index.md b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/intro.md similarity index 84% rename from python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/index.md rename to python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/intro.md index e1dada4147bb..5fad8db2506c 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/index.md +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/intro.md @@ -1,4 +1,4 @@ -# Multi-Agent Design Patterns +# Intro Agents can work together in a variety of ways to solve problems. Research works like [AutoGen](https://aka.ms/autogen-paper), @@ -17,14 +17,4 @@ You can implement any multi-agent design pattern using AutoGen agents. In the next two sections, we will discuss two common design patterns: group chat for task decomposition, and reflection for robustness. -```{toctree} -:maxdepth: 1 -concurrent-agents -sequential-workflow -group-chat -handoffs -mixture-of-agents -multi-agent-debate -reflection -``` diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/sequential-workflow.svg b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/sequential-workflow.svg index d68612ec333b..f14aa379a2c2 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/sequential-workflow.svg +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/sequential-workflow.svg @@ -1,3 +1,3 @@ -
Concept Extractor
Agent
Concept Extractor...
Writer Agent
Writer Agent
Format Proof
Agent
Format Proof...
User Agent
User Agent
Product
Concepts
Product...
Marketing 
Copy
Marketing...
Final 
Copy
Final...
Product Description
Product Description
\ No newline at end of file +
Concept Extractor
Agent
Concept Extractor...
Writer Agent
Writer Agent
Format Proof
Agent
Format Proof...
User Agent
User Agent
Product
Concepts
Product...
Marketing 
Copy
Marketing...
Final 
Copy
Final...
Product Description
Product Description
\ No newline at end of file diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/agent-and-agent-runtime.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/agent-and-agent-runtime.ipynb index 84853479e9bb..bfff326b56d0 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/agent-and-agent-runtime.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/agent-and-agent-runtime.ipynb @@ -9,7 +9,16 @@ "In this and the following section, we focus on the core concepts of AutoGen:\n", "agents, agent runtime, messages, and communication.\n", "You will not find any AI models or tools here, just the foundational\n", - "building blocks for building multi-agent applications." + "building blocks for building multi-agent applications.\n", + "\n", + "```{note}\n", + "The Core API is designed to be unopinionated and flexible. So at times, you\n", + "may find it challenging. Continue if you are building\n", + "an interactive, scalable and distributed multi-agent system and want full control\n", + "of all workflows.\n", + "If you just want to get something running\n", + "quickly, you may take a look at the [AgentChat API](../../agentchat-user-guide/index.md).\n", + "```" ] }, { diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/index.md b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/index.md deleted file mode 100644 index 21f9cd00d8f2..000000000000 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/index.md +++ /dev/null @@ -1,30 +0,0 @@ -# Framework Guide - -The following sections guides you through the usage of the Core API. -At minimum, read [Agent and Agent Runtime](agent-and-agent-runtime.ipynb) -and [Message and Communication](message-and-communication.ipynb) to get the -basic understanding. - -```{note} -The Core API is designed to be unopinionated and flexible. So at times, you -may find it challenging. Continue if you are building -an interactive, scalable and distributed multi-agent system and want full control -of all workflows. -If you just want to get something running -quickly, you may take a look at the [AgentChat API](../../agentchat-user-guide/index.md). -``` - -## List of content - -```{toctree} -:maxdepth: 1 - -agent-and-agent-runtime -message-and-communication -model-clients -tools -logging -telemetry -command-line-code-executors -distributed-agent-runtime -``` diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/index.md b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/index.md index 7782d0e4fd89..6a2f31211a0b 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/index.md +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/index.md @@ -11,16 +11,60 @@ myst: :maxdepth: 1 :hidden: +installation quickstart -core-concepts/index -framework/index -design-patterns/index -cookbook/index -faqs ``` -```{warning} -This project and documentation is a work in progress. If you have any questions or need help, please reach out to us on GitHub. +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Core Concepts + +core-concepts/agent-and-multi-agent-application +core-concepts/architecture +core-concepts/application-stack +core-concepts/agent-identity-and-lifecycle +core-concepts/topic-and-subscription +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Framework Guide + +framework/agent-and-agent-runtime +framework/message-and-communication +framework/model-clients +framework/tools +framework/logging +framework/telemetry +framework/command-line-code-executors +framework/distributed-agent-runtime +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Multi-Agent Design Patterns + +design-patterns/intro +design-patterns/concurrent-agents +design-patterns/sequential-workflow +design-patterns/group-chat +design-patterns/handoffs +design-patterns/mixture-of-agents +design-patterns/multi-agent-debate +design-patterns/reflection +design-patterns/code-execution-groupchat +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: More + +cookbook/index +faqs ``` AutoGen core offers an easy way to quickly build event-driven, distributed, scalable, resilient AI agent systems. Agents are developed by using the [Actor model](https://en.wikipedia.org/wiki/Actor_model). You can build and run your agent system locally and easily move to a distributed system in the cloud when you are ready. diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/installation.md b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/installation.md new file mode 100644 index 000000000000..ba39aa481da5 --- /dev/null +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/installation.md @@ -0,0 +1,14 @@ +# Installation + +## Install using pip + +Install the `autogen-core` package using pip: + +```bash + +pip install 'autogen-core==0.4.0.dev11' +``` + +```{note} +Python 3.10 or later is required. +``` diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/quickstart.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/quickstart.ipynb index 232547db18e4..f193979c856d 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/quickstart.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/quickstart.ipynb @@ -7,97 +7,60 @@ "# Quick Start\n", "\n", ":::{note}\n", - "See [here](pkg-info-autogen-core) for installation instructions.\n", + "See [here](installation) for installation instructions.\n", ":::\n", "\n", - "Before diving into the core APIs, let's start with a simple example of two\n", - "agents creating a plot of Tesla's and Nvidia's stock returns.\n", + "Before diving into the core APIs, let's start with a simple example of two agents that count down from 10 to 1.\n", "\n", "We first define the agent classes and their respective procedures for \n", "handling messages.\n", - "We create two agent classes: `Assistant` and `Executor`. The `Assistant`\n", - "agent writes code and the `Executor` agent executes the code.\n", - "We also create a `Message` data class, which defines the messages that are passed between\n", - "the agents.\n", - "\n", - "```{attention}\n", - "Code generated in this example is run within a [Docker](https://www.docker.com/) container. Please ensure Docker is [installed](https://docs.docker.com/get-started/get-docker/) and running prior to running the example. Local code execution is available ({py:class}`~autogen_ext.code_executors.local.LocalCommandLineCodeExecutor`) but is not recommended due to the risk of running LLM generated code in your local environment.\n", - "```" + "We create two agent classes: `Modifier` and `Checker`. The `Modifier` agent modifies a number that is given and the `Check` agent checks the value against a condition.\n", + "We also create a `Message` data class, which defines the messages that are passed between the agents." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ - "import re\n", "from dataclasses import dataclass\n", - "from typing import List\n", + "from typing import Callable\n", "\n", "from autogen_core import DefaultTopicId, MessageContext, RoutedAgent, default_subscription, message_handler\n", - "from autogen_core.code_executor import CodeBlock, CodeExecutor\n", - "from autogen_core.models import (\n", - " AssistantMessage,\n", - " ChatCompletionClient,\n", - " LLMMessage,\n", - " SystemMessage,\n", - " UserMessage,\n", - ")\n", "\n", "\n", "@dataclass\n", "class Message:\n", - " content: str\n", + " content: int\n", "\n", "\n", "@default_subscription\n", - "class Assistant(RoutedAgent):\n", - " def __init__(self, model_client: ChatCompletionClient) -> None:\n", - " super().__init__(\"An assistant agent.\")\n", - " self._model_client = model_client\n", - " self._chat_history: List[LLMMessage] = [\n", - " SystemMessage(\n", - " content=\"\"\"Write Python script in markdown block, and it will be executed.\n", - "Always save figures to file in the current directory. Do not use plt.show(). All code required to complete this task must be contained within a single response.\"\"\",\n", - " )\n", - " ]\n", + "class Modifier(RoutedAgent):\n", + " def __init__(self, modify_val: Callable[[int], int]) -> None:\n", + " super().__init__(\"A modifier agent.\")\n", + " self._modify_val = modify_val\n", "\n", " @message_handler\n", " async def handle_message(self, message: Message, ctx: MessageContext) -> None:\n", - " self._chat_history.append(UserMessage(content=message.content, source=\"user\"))\n", - " result = await self._model_client.create(self._chat_history)\n", - " print(f\"\\n{'-'*80}\\nAssistant:\\n{result.content}\")\n", - " self._chat_history.append(AssistantMessage(content=result.content, source=\"assistant\")) # type: ignore\n", - " await self.publish_message(Message(content=result.content), DefaultTopicId()) # type: ignore\n", - "\n", - "\n", - "def extract_markdown_code_blocks(markdown_text: str) -> List[CodeBlock]:\n", - " pattern = re.compile(r\"```(?:\\s*([\\w\\+\\-]+))?\\n([\\s\\S]*?)```\")\n", - " matches = pattern.findall(markdown_text)\n", - " code_blocks: List[CodeBlock] = []\n", - " for match in matches:\n", - " language = match[0].strip() if match[0] else \"\"\n", - " code_content = match[1]\n", - " code_blocks.append(CodeBlock(code=code_content, language=language))\n", - " return code_blocks\n", + " val = self._modify_val(message.content)\n", + " print(f\"{'-'*80}\\nModifier:\\nModified {message.content} to {val}\")\n", + " await self.publish_message(Message(content=val), DefaultTopicId()) # type: ignore\n", "\n", "\n", "@default_subscription\n", - "class Executor(RoutedAgent):\n", - " def __init__(self, code_executor: CodeExecutor) -> None:\n", - " super().__init__(\"An executor agent.\")\n", - " self._code_executor = code_executor\n", + "class Checker(RoutedAgent):\n", + " def __init__(self, run_until: Callable[[int], bool]) -> None:\n", + " super().__init__(\"A checker agent.\")\n", + " self._run_until = run_until\n", "\n", " @message_handler\n", " async def handle_message(self, message: Message, ctx: MessageContext) -> None:\n", - " code_blocks = extract_markdown_code_blocks(message.content)\n", - " if code_blocks:\n", - " result = await self._code_executor.execute_code_blocks(\n", - " code_blocks, cancellation_token=ctx.cancellation_token\n", - " )\n", - " print(f\"\\n{'-'*80}\\nExecutor:\\n{result.output}\")\n", - " await self.publish_message(Message(content=result.output), DefaultTopicId())" + " if not self._run_until(message.content):\n", + " print(f\"{'-'*80}\\nChecker:\\n{message.content} passed the check, continue.\")\n", + " await self.publish_message(Message(content=message.content), DefaultTopicId())\n", + " else:\n", + " print(f\"{'-'*80}\\nChecker:\\n{message.content} failed the check, stopping.\")" ] }, { @@ -121,275 +84,106 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\n", "--------------------------------------------------------------------------------\n", - "Assistant:\n", - "```python\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "import yfinance as yf\n", - "\n", - "# Define the stock tickers\n", - "ticker_symbols = ['NVDA', 'TSLA']\n", - "\n", - "# Download the stock data from Yahoo Finance starting from 2024-01-01\n", - "start_date = '2024-01-01'\n", - "stock_data = yf.download(ticker_symbols, start=start_date)['Adj Close']\n", - "\n", - "# Calculate daily returns\n", - "returns = stock_data.pct_change().dropna()\n", - "\n", - "# Plot the stock returns\n", - "plt.figure(figsize=(10, 6))\n", - "for ticker in ticker_symbols:\n", - " returns[ticker].cumsum().plot(label=ticker)\n", - "\n", - "plt.title('NVIDIA vs TSLA Stock Returns YTD from 2024-01-01')\n", - "plt.xlabel('Date')\n", - "plt.ylabel('Cumulative Returns')\n", - "plt.legend()\n", - "plt.grid(True)\n", - "\n", - "# Save the plot to a file\n", - "plt.savefig('nvidia_vs_tsla_stock_returns_ytd_2024.png')\n", - "```\n", - "\n", + "Checker:\n", + "10 passed the check, continue.\n", + "--------------------------------------------------------------------------------\n", + "Modifier:\n", + "Modified 10 to 9\n", + "--------------------------------------------------------------------------------\n", + "Checker:\n", + "9 passed the check, continue.\n", + "--------------------------------------------------------------------------------\n", + "Modifier:\n", + "Modified 9 to 8\n", "--------------------------------------------------------------------------------\n", - "Executor:\n", - "Traceback (most recent call last):\n", - " File \"/workspace/tmp_code_f562e5e3c313207b9ec10ca87094085f.python\", line 1, in \n", - " import pandas as pd\n", - "ModuleNotFoundError: No module named 'pandas'\n", - "\n", - "\n", + "Checker:\n", + "8 passed the check, continue.\n", "--------------------------------------------------------------------------------\n", - "Assistant:\n", - "It looks like some required modules are not installed. Let me proceed by installing the necessary libraries before running the script.\n", - "\n", - "```python\n", - "!pip install pandas matplotlib yfinance\n", - "```\n", - "\n", + "Modifier:\n", + "Modified 8 to 7\n", "--------------------------------------------------------------------------------\n", - "Executor:\n", - " File \"/workspace/tmp_code_78ffa711e7b0ff8738fdeec82404018c.python\", line 1\n", - " !pip install -qqq pandas matplotlib yfinance\n", - " ^\n", - "SyntaxError: invalid syntax\n", - "\n", - "\n", + "Checker:\n", + "7 passed the check, continue.\n", "--------------------------------------------------------------------------------\n", - "Assistant:\n", - "It appears that I'm unable to run installation commands within the code execution environment. However, you can install the necessary libraries using the following commands in your local environment:\n", - "\n", - "```sh\n", - "pip install pandas matplotlib yfinance\n", - "```\n", - "\n", - "After installing the libraries, you can then run the previous plotting script. Here is the combined process:\n", - "\n", - "1. First, install the required libraries (run this in your terminal or command prompt):\n", - " ```sh\n", - " pip install pandas matplotlib yfinance\n", - " ```\n", - "\n", - "2. Now, you can run the script to generate the plot:\n", - " ```python\n", - " import pandas as pd\n", - " import matplotlib.pyplot as plt\n", - " import yfinance as yf\n", - "\n", - " # Define the stock tickers\n", - " ticker_symbols = ['NVDA', 'TSLA']\n", - "\n", - " # Download the stock data from Yahoo Finance starting from 2024-01-01\n", - " start_date = '2024-01-01'\n", - " stock_data = yf.download(ticker_symbols, start=start_date)['Adj Close']\n", - "\n", - " # Calculate daily returns\n", - " returns = stock_data.pct_change().dropna()\n", - "\n", - " # Plot the stock returns\n", - " plt.figure(figsize=(10, 6))\n", - " for ticker in ticker_symbols:\n", - " returns[ticker].cumsum().plot(label=ticker)\n", - "\n", - " plt.title('NVIDIA vs TSLA Stock Returns YTD from 2024-01-01')\n", - " plt.xlabel('Date')\n", - " plt.ylabel('Cumulative Returns')\n", - " plt.legend()\n", - " plt.grid(True)\n", - "\n", - " # Save the plot to a file\n", - " plt.savefig('nvidia_vs_tsla_stock_returns_ytd_2024.png')\n", - " ```\n", - "\n", - "This should generate and save the desired plot in your current directory as `nvidia_vs_tsla_stock_returns_ytd_2024.png`.\n", - "\n", + "Modifier:\n", + "Modified 7 to 6\n", "--------------------------------------------------------------------------------\n", - "Executor:\n", - "Requirement already satisfied: pandas in /usr/local/lib/python3.12/site-packages (2.2.2)\n", - "Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/site-packages (3.9.2)\n", - "Requirement already satisfied: yfinance in /usr/local/lib/python3.12/site-packages (0.2.43)\n", - "Requirement already satisfied: numpy>=1.26.0 in /usr/local/lib/python3.12/site-packages (from pandas) (2.1.1)\n", - "Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/site-packages (from pandas) (2.9.0.post0)\n", - "Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/site-packages (from pandas) (2024.2)\n", - "Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/site-packages (from pandas) (2024.1)\n", - "Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/site-packages (from matplotlib) (1.3.0)\n", - "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/site-packages (from matplotlib) (0.12.1)\n", - "Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/site-packages (from matplotlib) (4.53.1)\n", - "Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/site-packages (from matplotlib) (1.4.7)\n", - "Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/site-packages (from matplotlib) (24.1)\n", - "Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/site-packages (from matplotlib) (10.4.0)\n", - "Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/site-packages (from matplotlib) (3.1.4)\n", - "Requirement already satisfied: requests>=2.31 in /usr/local/lib/python3.12/site-packages (from yfinance) (2.32.3)\n", - "Requirement already satisfied: multitasking>=0.0.7 in /usr/local/lib/python3.12/site-packages (from yfinance) (0.0.11)\n", - "Requirement already satisfied: lxml>=4.9.1 in /usr/local/lib/python3.12/site-packages (from yfinance) (5.3.0)\n", - "Requirement already satisfied: platformdirs>=2.0.0 in /usr/local/lib/python3.12/site-packages (from yfinance) (4.3.6)\n", - "Requirement already satisfied: frozendict>=2.3.4 in /usr/local/lib/python3.12/site-packages (from yfinance) (2.4.4)\n", - "Requirement already satisfied: peewee>=3.16.2 in /usr/local/lib/python3.12/site-packages (from yfinance) (3.17.6)\n", - "Requirement already satisfied: beautifulsoup4>=4.11.1 in /usr/local/lib/python3.12/site-packages (from yfinance) (4.12.3)\n", - "Requirement already satisfied: html5lib>=1.1 in /usr/local/lib/python3.12/site-packages (from yfinance) (1.1)\n", - "Requirement already satisfied: soupsieve>1.2 in /usr/local/lib/python3.12/site-packages (from beautifulsoup4>=4.11.1->yfinance) (2.6)\n", - "Requirement already satisfied: six>=1.9 in /usr/local/lib/python3.12/site-packages (from html5lib>=1.1->yfinance) (1.16.0)\n", - "Requirement already satisfied: webencodings in /usr/local/lib/python3.12/site-packages (from html5lib>=1.1->yfinance) (0.5.1)\n", - "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.12/site-packages (from requests>=2.31->yfinance) (3.3.2)\n", - "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/site-packages (from requests>=2.31->yfinance) (3.10)\n", - "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.12/site-packages (from requests>=2.31->yfinance) (2.2.3)\n", - "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.12/site-packages (from requests>=2.31->yfinance) (2024.8.30)\n", - "WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable.It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.\n", - " File \"/workspace/tmp_code_d094fa6242b4268e4812bf9902aa1374.python\", line 1\n", - " import pandas as pd\n", - "IndentationError: unexpected indent\n", - "\n", - "\n", + "Checker:\n", + "6 passed the check, continue.\n", "--------------------------------------------------------------------------------\n", - "Assistant:\n", - "Thank you for the confirmation. As the required packages are installed, let's proceed with the script to plot the NVIDIA vs TSLA stock returns YTD starting from 2024-01-01.\n", - "\n", - "Here's the updated script:\n", - "\n", - "```python\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "import yfinance as yf\n", - "\n", - "# Define the stock tickers\n", - "ticker_symbols = ['NVDA', 'TSLA']\n", - "\n", - "# Download the stock data from Yahoo Finance starting from 2024-01-01\n", - "start_date = '2024-01-01'\n", - "stock_data = yf.download(ticker_symbols, start=start_date)['Adj Close']\n", - "\n", - "# Calculate daily returns\n", - "returns = stock_data.pct_change().dropna()\n", - "\n", - "# Plot the stock returns\n", - "plt.figure(figsize=(10, 6))\n", - "for ticker in ticker_symbols:\n", - " returns[ticker].cumsum().plot(label=ticker)\n", - "\n", - "plt.title('NVIDIA vs TSLA Stock Returns YTD from 2024-01-01')\n", - "plt.xlabel('Date')\n", - "plt.ylabel('Cumulative Returns')\n", - "plt.legend()\n", - "plt.grid(True)\n", - "\n", - "# Save the plot to a file\n", - "plt.savefig('nvidia_vs_tsla_stock_returns_ytd_2024.png')\n", - "```\n", - "\n", + "Modifier:\n", + "Modified 6 to 5\n", "--------------------------------------------------------------------------------\n", - "Executor:\n", - "[*********************100%***********************] 2 of 2 completed\n", - "\n", - "\n", + "Checker:\n", + "5 passed the check, continue.\n", "--------------------------------------------------------------------------------\n", - "Assistant:\n", - "It looks like the stock data was successfully downloaded, and the plot has been generated and saved as `nvidia_vs_tsla_stock_returns_ytd_2024.png` in the current directory.\n", - "\n", - "If you have any further questions or need additional assistance, feel free to ask!\n" + "Modifier:\n", + "Modified 5 to 4\n", + "--------------------------------------------------------------------------------\n", + "Checker:\n", + "4 passed the check, continue.\n", + "--------------------------------------------------------------------------------\n", + "Modifier:\n", + "Modified 4 to 3\n", + "--------------------------------------------------------------------------------\n", + "Checker:\n", + "3 passed the check, continue.\n", + "--------------------------------------------------------------------------------\n", + "Modifier:\n", + "Modified 3 to 2\n", + "--------------------------------------------------------------------------------\n", + "Checker:\n", + "2 passed the check, continue.\n", + "--------------------------------------------------------------------------------\n", + "Modifier:\n", + "Modified 2 to 1\n", + "--------------------------------------------------------------------------------\n", + "Checker:\n", + "1 failed the check, stopping.\n" ] } ], "source": [ - "import tempfile\n", - "\n", - "from autogen_core import SingleThreadedAgentRuntime\n", - "from autogen_ext.code_executors import DockerCommandLineCodeExecutor\n", - "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", - "\n", - "work_dir = tempfile.mkdtemp()\n", + "from autogen_core import AgentId, SingleThreadedAgentRuntime\n", "\n", "# Create an local embedded runtime.\n", "runtime = SingleThreadedAgentRuntime()\n", "\n", - "async with DockerCommandLineCodeExecutor(work_dir=work_dir) as executor: # type: ignore[syntax]\n", - " # Register the assistant and executor agents by providing\n", - " # their agent types, the factory functions for creating instance and subscriptions.\n", - " await Assistant.register(\n", - " runtime,\n", - " \"assistant\",\n", - " lambda: Assistant(\n", - " OpenAIChatCompletionClient(\n", - " model=\"gpt-4o\",\n", - " # api_key=\"YOUR_API_KEY\"\n", - " )\n", - " ),\n", - " )\n", - " await Executor.register(runtime, \"executor\", lambda: Executor(executor))\n", + "# Register the modifier and checker agents by providing\n", + "# their agent types, the factory functions for creating instance and subscriptions.\n", + "await Modifier.register(\n", + " runtime,\n", + " \"modifier\",\n", + " # Modify the value by subtracting 1\n", + " lambda: Modifier(modify_val=lambda x: x - 1),\n", + ")\n", "\n", - " # Start the runtime and publish a message to the assistant.\n", - " runtime.start()\n", - " await runtime.publish_message(\n", - " Message(\"Create a plot of NVIDA vs TSLA stock returns YTD from 2024-01-01.\"), DefaultTopicId()\n", - " )\n", - " await runtime.stop_when_idle()" + "await Checker.register(\n", + " runtime,\n", + " \"checker\",\n", + " # Run until the value is less than or equal to 1\n", + " lambda: Checker(run_until=lambda x: x <= 1),\n", + ")\n", + "\n", + "# Start the runtime and send a direct message to the checker.\n", + "runtime.start()\n", + "await runtime.send_message(Message(10), AgentId(\"checker\", \"default\"))\n", + "await runtime.stop_when_idle()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "From the agent's output, we can see the plot of Tesla's and Nvidia's stock returns\n", - "has been created." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/var/folders/cs/b9_18p1s2rd56_s2jl65rxwc0000gn/T/tmp_9c2ylon\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+gAAAJYCAYAAADxHswlAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAD4YUlEQVR4nOzdd3iTVRsG8PtN2nQPuidd0JZSaNl7L9kgUzYIThBBUVFcqAiiKA4EGZ+AIltBRPbee5YWWlroHnTvNHm/P9JESgctpE1p7t919brIO5/3NC19cp5zjiCKoggiIiIiIiIi0imJrgMgIiIiIiIiIiboRERERERERLUCE3QiIiIiIiKiWoAJOhEREREREVEtwASdiIiIiIiIqBZggk5ERERERERUCzBBJyIiIiIiIqoFmKATERERERER1QJM0ImIiIiIiIhqASboRERERERERLUAE3QiIiIiIiKiWoAJOhEREREREVEtwASdiIiIiIiIqBZggk5ERERERERUCzBBJyIiIiIiIqoFmKATERERERER1QJM0ImIiIiIiIhqASboRERERERERLUAE3QiIiIiIiKiWoAJOhEREREREVEtwASdiIiIiIiIqBZggk5ERERERERUCzBBJyIiIiIiIqoFmKATERERERER1QJM0ImIiIiIiIhqASboRERERERERLUAE3QiIiIiIiKiWoAJOhEREREREVEtwASdiIiIiIiIqBZggk5ERERERERUCzBBJyIiIiIiIqoFmKATERERERER1QJM0ImIiIiIiIhqASboRERERERERLUAE3QiIiIiIiKiWoAJOhEREREREVEtwASdiIiIiIiIqBZggk5ERERERERUCzBBJyIiIiIiIqoFmKATERERERER1QJM0ImIiIiIiIhqASboRERERERERLUAE3QiIiIiIiKiWoAJOhEREREREVEtwASdiIiIiIiIqBZggk5ERERERERUCzBBJyIiIiIiIqoFmKATERERERER1QJM0ImIiIiIiIhqASboRERET+nIkSMQBAFbt27VdShUA86fP4/27dvDzMwMgiDgypUrug6JiIjqCCboRFSr/frrrxAEAcbGxoiNjS21v2vXrggMDAQAXLp0CYIgYN68eeVe786dOxAEAbNnzwYAfPLJJxAEASkpKZpjJk2aBEEQNF/m5ubw9vbG8OHDsW3bNiiV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KtBFREREREREvIAKdBEvpLkbRUQkN5w9e9bTIYhkSH8LSUGlAl2ui9vtBiApKcnDkeQfZ86cweVypd7XL6isi4mJITo6mgsXLgBpn0/JnoMHDzJr1ixPh5Hn7d69m1deeYVdu3Z5OpR8Yd++fTz++OP8/vvvng4lT9u/fz9du3bl+eefB/R9eb2ioqJYtWoVhw4d8nQo+cLJkyfTXTzS30JSkKhAl2wbOnQo9913HwA+Pj4ejibvsyyLIUOG0LVrV3r06MHIkSOJj4/H4XDoF9NVWJbFU089RcuWLbnlllvo3r07p06dwul0KnfZtHPnTipUqMDtt9/Ozp07PR1OnmRZFo8//jjVqlXjyJEjlC1b1tMh5XkvvPACtWrV4vjx48TFxennOxssy+LRRx+latWqLFu2jD/++AO3243TqT8Js+upp56iXr16PPzww9SrV4+5c+d6OqQ87cknn+SGG26gV69e3H///Rw5cgSHw+HpsERyjb6N5ZqtXbuWzp078+233/LDDz+ktmKoFT37/vzzT+rWrcuyZct49tlnqVy5MlOmTGHEiBGeDs3rzZgxg9q1a7Nq1Sr+85//MGjQIKKionjyyScB9Es9m1wuF127dqV48eK8+uqrng4nz/nuu+8oUaIEK1asYMWKFXz88ccEBgYCagnKrvnz5/PHH38wdepUfvrpJ2677Tb9fF+jt99+m6JFi7Ju3TrWrFnD6NGj8fPzIzo62tOh5Unnz5+nb9++rF69mpkzZ/LDDz9w0003MXz4cE+HliedPXuWXr16sXbtWj7//HPuv/9+9u7dy80338ymTZs8HZ5IrvH1dACS96xcuZLIyEiGDBnCd999x7PPPkvXrl3x8fHBsiz9wXSN4uLi+Omnn2jZsiXvv/8+hQoV4tZbb+Wtt97it99+4/Tp04SGhno6TK+1cOFCevbsyWuvvYa/vz9gLiJdPExArt369evx9/fnp59+onXr1gwYMID27dt7Oqw846uvviIkJITp06dTunRpNm3axOHDh6latSoREREEBQXp+/Iaffnll1SpUoWuXbuybNkypk+fTpUqVWjTpg3VqlXzdHheb+fOnfz666+8++67PPjgg4DpRrx+/frUC+z6TF6bnTt3sm7dOt566y1uuOEGAPr27ctHH32Ey+XCz8/PwxHmLevWrWPPnj1MmjSJBg0acOON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f/3Fvn37GDlyJH/88QcTJkygQoUKADz22GOMGzeOypUre/IteIVr/VweOHCAffv2MXjwYKZOncoDDzwAaCxliivls2nTpixatIgNGzbQtWtX3nvvPSZNmkSHDh2488476du3L506ddLszlzb5zKlN1bHjh1Zv359au84SZOVfG7YsIHSpUtz//3307VrVwYNGsTAgQO588476dSpE82bN/fwu/AeV/s5X7FiRbrvzZ07d7J7927+8Y9/sGjRIu6//35A35tSAHiwe714ocTERMuyLGvBggWW0+m01q5da1mWlTpWcuHChVbVqlWtH3/8MfU5K1eutKpXr241bNjQ2rx5c67H7O2yk9O6deumjsFavXp1rsfsLa41dzt37rRGjBhhlS9f3mrVqpW1fv16j8TtrbKSz8qVK1s//fSTp0LME671c7ljxw5r2LBhVkREhNWyZUtrw4YNHonbW2Uln1WqVLF++OGH1OesXLnS+vjjj63nn39eP+fJsvO7xrIsa/LkydbAgQOt48ePawz6RbL6uUz5vjx//rz1wgsvWA899JB177336nN5iex8Pv/73/9a1atXt5o3b67vTSlQHJaly1AFzf79+/Hx8aFs2bIkJSWldjMC0xLu6+vL+fPn6datG35+fsyZMyfd2qhVq1alf//+vPTSSwCcOHGCbdu20bp1a4+8H29gR04feOABRo4cyZkzZ5gwYQI1atSgZ8+ennpLucbO3J0/f541a9aQmJjIjTfe6Km35FF2/nxbBXhNZDs/l/Hx8Sxfvhy3211gh1vY/XunoLIzjynP18+5PT/nKS49T0Fi9895TEwMe/bsoWnTph55PyKeoi7uBcyvv/5KpUqVePLJJwFSvzxTuhmlTL5z+vRpRo0axR9//MFHH32U2p3o5MmTFC5cmLCwMMB0MypevHiBLs7tymnKuKoiRYowbNiwAlGc2527wMBAWrVqVWCLc7t/vgvqH+12fy4LFSpE+/btC2xxbvfnsqCyO48pz9fPuT0/5ykKanGeEz/nYWFhKs6lQFKBXsCsWLGC5s2bc+DAAX7++Wcg/dXe9957j6CgIGbNmkW7du1SJ4Z69NFH+euvv/j3v//NmTNn6NixI1Bwf7FfzO6cFiTKnb2UT3soj/ZSPu2hPNpL+bSX8iliH3VxLyBSllQZPHgwTqeTuLg4duzYwbx58/Dz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o0YcO3aMefPmUb9+fW6++WYPvWMREZH8S7O4i4iI5GGzZs2idOnSVKxYkW7durFgwQLee+89fv31V3x8fGjQoAFvv/0248aNo27dukycOJExY8akO0erVq147LHH6NOnDyVLluT1118H4IsvvuCBBx5g2LBh1KhRg969e7Ny5UrKly/vibcqIiKS72kWdxEREREREREvoBZ0ERERERERES+gAl1ERERERETEC6hAFxEREREREfECKtBFREREREREvIAKdBEREREREREvoAJdRERERERExAuoQBcRERERERHxAirQRURERERERLyACnQRERERERERL6ACXURERERERMQLqEAXERERERER8QIq0EVERERERES8gAp0ERERERERES+gAl1ERERERETEC6hAFxEREREREfECKtBFREREREREvIAKdBEREREREREvoAJdRERERERExAuoQBcRERERERHxAirQRURERERERLyACnQRERERERERL6ACXURERERERMQLqEAXERERERER8QIq0EVERERERES8gAp0ERERERERES+gAl1ERERERETEC6hAFxEREREREfECKtBFREREREREvIAKdBEREREREREvoAJdRERERERExAuoQBcRERERERHxAirQRURERERERLyACnQRERERERERL6ACXURERERERMQLqEAXERERERER8QIq0EVERERERES8gAp0ERERERERES+gAl1ERERERETEC6hAFxEREREREfECKtBFREREREREvIAKdBEREREREREvoAJdRERERERExAuoQBcRERERERHxAirQRURERERERLyACnQRERERERERL6ACXURERERERMQLqEAXERERERER8QIq0EVERERERES8gAp0ERERERERES+gAl1ERERERETEC6hAFxEREREREfECKtBFREREREREvMD/A9DsdFoeg9eAAAAAAElFTkSuQmCC", - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from IPython.display import Image\n", - "\n", - "Image(filename=f\"{work_dir}/NVIDIA_vs_TSLA_Stock_Returns_YTD_2024.png\") # type: ignore" + "From the agent's output, we can see the value was successfully decremented from 10 to 1 as the modifier and checker conditions dictate." ] }, { @@ -406,7 +200,7 @@ ], "metadata": { "kernelspec": { - "display_name": "agnext", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -420,7 +214,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/create-your-own.md b/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/create-your-own.md new file mode 100644 index 000000000000..6d4c797c6005 --- /dev/null +++ b/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/create-your-own.md @@ -0,0 +1,44 @@ +# Creating your own extension + +With the new package structure in 0.4, it is easier than ever to create and publish your own extension to the AutoGen ecosystem. This page details some best practices so that your extension package integrates well with the AutoGen ecosystem. + +## Best practices + +### Naming + +There is no requirement about naming. But prefixing the package name with `autogen-` makes it easier to find. + +### Common interfaces + +Whenever possible, extensions should implement the provided interfaces from the `autogen_core` package. This will allow for a more consistent experience for users. + +#### Dependency on AutoGen + +To ensure that the extension works with the version of AutoGen that it was designed for, it is recommended to specify the version of AutoGen the dependency section of the `pyproject.toml` with adequate constraints. + +```toml +[project] +# ... +dependencies = [ + "autogen-core>=0.4,<0.5" +] +``` + +### Usage of typing + +AutoGen embraces the use of type hints to provide a better development experience. Extensions should use type hints whenever possible. + +## Discovery + +To make it easier for users to find your extension, sample, service or package, you can [add the topic](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/classifying-your-repository-with-topics) [`autogen`](https://github.com/topics/autogen) to the GitHub repo. + +More specific topics are also available: + +- [`autogen-extension`](https://github.com/topics/autogen-extension) for extensions +- [`autogen-sample`](https://github.com/topics/autogen-sample) for samples + +## Changes from 0.2 + +In AutoGen 0.2 it was common to merge 3rd party extensions and examples into the main repo. We are super appreciative of all of the users who have contributed to the ecosystem notebooks, modules and pages in 0.2. However, in general we are moving away from this model to allow for more flexibility and to reduce maintenance burden. + +There is the `autogen-ext` package for 1st party supported extensions, but we want to be selective to manage maintenance load. If you would like to see if your extension makes sense to add into `autogen-ext`, please open an issue and let's discuss. Otherwise, we encourage you to publish your extension as a separate package and follow the guidance under [discovery](#discovery) to make it easy for users to find. diff --git a/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/discover.md b/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/discover.md new file mode 100644 index 000000000000..f0cb99529c7a --- /dev/null +++ b/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/discover.md @@ -0,0 +1,46 @@ +# Discover community projects + +::::{grid} 1 2 2 2 +:margin: 4 4 0 0 +:gutter: 1 + +:::{grid-item-card} {fas}`globe;pst-color-primary`
Ecosystem +:link: https://github.com/topics/autogen +:class-item: api-card +:columns: 12 + +Find samples, services and other things that work with AutoGen + +::: + +:::{grid-item-card} {fas}`puzzle-piece;pst-color-primary`
Community Extensions +:link: https://github.com/topics/autogen-extension +:class-item: api-card + +Find AutoGen extensions for 3rd party tools, components and services + +::: + +:::{grid-item-card} {fas}`vial;pst-color-primary`
Community Samples +:link: https://github.com/topics/autogen-sample +:class-item: api-card + +Find community samples and examples of how to use AutoGen + +::: + +:::: + + +## List of community projects + +| Name | Package | Description | +|---|---|---| +| [autogen-watsonx-client](https://github.com/tsinggggg/autogen-watsonx-client) | [PyPi](https://pypi.org/project/autogen-watsonx-client/) | Model client for [IBM watsonx.ai](https://www.ibm.com/products/watsonx-ai) | +| [autogen-openaiext-client](https://github.com/vballoli/autogen-openaiext-client) | [PyPi](https://pypi.org/project/autogen-openaiext-client/) | Model client for other LLMs like Gemini, etc. through the OpenAI API | + + + + diff --git a/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/index.md b/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/index.md index f8013dae5433..a60b9447ad1e 100644 --- a/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/index.md +++ b/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/index.md @@ -11,124 +11,34 @@ myst: :maxdepth: 3 :hidden: -azure-container-code-executor +installation +discover +create-your-own ``` +```{toctree} +:maxdepth: 3 +:hidden: +:caption: Guides -## Discover community projects: - -::::{grid} 1 2 2 2 -:margin: 4 4 0 0 -:gutter: 1 - -:::{grid-item-card} {fas}`globe;pst-color-primary`
Ecosystem -:link: https://github.com/topics/autogen -:class-item: api-card -:columns: 12 +azure-container-code-executor +``` -Find samples, services and other things that work with AutoGen +AutoGen is designed to be extensible. The `autogen-ext` package contains many different component implementations maintained by the AutoGen project. However, we strongly encourage others to build their own components and publish them as part of the ecosytem. -::: -:::{grid-item-card} {fas}`puzzle-piece;pst-color-primary`
Community Extensions -:link: https://github.com/topics/autogen-extension -:class-item: api-card +::::{grid} 2 2 2 2 +:gutter: 3 -Find AutoGen extensions for 3rd party tools, components and services +:::{grid-item-card} {fas}`magnifying-glass;pst-color-primary` Discover +:link: ./installation.html +How to install AgentChat ::: -:::{grid-item-card} {fas}`vial;pst-color-primary`
Community Samples -:link: https://github.com/topics/autogen-sample -:class-item: api-card - -Find community samples and examples of how to use AutoGen +:::{grid-item-card} {fas}`code;pst-color-primary` Create your own +:link: ./quickstart.html +Build your first agent ::: - :::: - - -### List of community projects - -| Name | Package | Description | -|---|---|---| -| [autogen-watsonx-client](https://github.com/tsinggggg/autogen-watsonx-client) | [PyPi](https://pypi.org/project/autogen-watsonx-client/) | Model client for [IBM watsonx.ai](https://www.ibm.com/products/watsonx-ai) | -| [autogen-openaiext-client](https://github.com/vballoli/autogen-openaiext-client) | [PyPi](https://pypi.org/project/autogen-openaiext-client/) | Model client for other LLMs like Gemini, etc. through the OpenAI API | - - - - - - -## Built-in extensions - -Read docs for built in extensions: - -```{note} -WIP -``` - - - - -## Creating your own community extension - -With the new package structure in 0.4, it is easier than ever to create and publish your own extension to the AutoGen ecosystem. This page details some best practices so that your extension package integrates well with the AutoGen ecosystem. - -### Best practices - -#### Naming - -There is no requirement about naming. But prefixing the package name with `autogen-` makes it easier to find. - -#### Common interfaces - -Whenever possible, extensions should implement the provided interfaces from the `autogen_core` package. This will allow for a more consistent experience for users. - -##### Dependency on AutoGen - -To ensure that the extension works with the version of AutoGen that it was designed for, it is recommended to specify the version of AutoGen the dependency section of the `pyproject.toml` with adequate constraints. - -```toml -[project] -# ... -dependencies = [ - "autogen-core>=0.4,<0.5" -] -``` - -#### Usage of typing - -AutoGen embraces the use of type hints to provide a better development experience. Extensions should use type hints whenever possible. - -### Discovery - -To make it easier for users to find your extension, sample, service or package, you can [add the topic](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/classifying-your-repository-with-topics) [`autogen`](https://github.com/topics/autogen) to the GitHub repo. - -More specific topics are also available: - -- [`autogen-extension`](https://github.com/topics/autogen-extension) for extensions -- [`autogen-sample`](https://github.com/topics/autogen-sample) for samples - -### Changes from 0.2 - -In AutoGen 0.2 it was common to merge 3rd party extensions and examples into the main repo. We are super appreciative of all of the users who have contributed to the ecosystem notebooks, modules and pages in 0.2. However, in general we are moving away from this model to allow for more flexibility and to reduce maintenance burden. - -There is the `autogen-ext` package for 1st party supported extensions, but we want to be selective to manage maintenance load. If you would like to see if your extension makes sense to add into `autogen-ext`, please open an issue and let's discuss. Otherwise, we encourage you to publish your extension as a separate package and follow the guidance under [discovery](#discovery) to make it easy for users to find. diff --git a/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/installation.md b/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/installation.md new file mode 100644 index 000000000000..f98d4361d918 --- /dev/null +++ b/python/packages/autogen-core/docs/src/user-guide/extensions-user-guide/installation.md @@ -0,0 +1,23 @@ +--- +myst: + html_meta: + "description lang=en": | + User Guide for AutoGen Extensions, a framework for building multi-agent applications with AI agents. +--- + +# Installation + +First-part maintained extensions are available in the `autogen-ext` package. + + +```sh +pip install 'autogen-ext==0.4.0.dev11' +``` + +Extras: + +- `langchain` needed for {py:class}`~autogen_ext.tools.langchain.LangChainToolAdapter` +- `azure` needed for {py:class}`~autogen_ext.code_executors.azure.ACADynamicSessionsCodeExecutor` +- `docker` needed for {py:class}`~autogen_ext.code_executors.docker.DockerCommandLineCodeExecutor` +- `openai` needed for {py:class}`~autogen_ext.models.openai.OpenAIChatCompletionClient` + diff --git a/python/packages/autogen-core/docs/src/user-guide/index.md b/python/packages/autogen-core/docs/src/user-guide/index.md deleted file mode 100644 index eef6281b7362..000000000000 --- a/python/packages/autogen-core/docs/src/user-guide/index.md +++ /dev/null @@ -1,47 +0,0 @@ -# User Guide - -```{toctree} -:maxdepth: 3 -:hidden: - -agentchat-user-guide/index -autogenstudio-user-guide/index -core-user-guide/index -extensions-user-guide/index -``` - -::::{grid} 1 2 2 3 -:margin: 4 4 0 0 -:gutter: 1 - -:::{grid-item-card} {fas}`people-group;pst-color-primary`
AutoGen AgentChat -:link: agentchat-user-guide/index -:link-type: doc -:class-item: api-card -::: - -:::{grid-item-card} {fas}`people-group;pst-color-primary`
AutoGen AgentChat -:link: autogenstudio-user-guide/index -:link-type: doc -:class-item: api-card -::: - -:::{grid-item-card} {fas}`display;pst-color-primary`
AutoGen Studio -:link: autogenstudio-user-guide/index -:link-type: doc -:class-item: api-card -::: - -:::{grid-item-card} {fas}`cube;pst-color-primary`
AutoGen Core -:link: core-user-guide/index -:link-type: doc -:class-item: api-card -::: - -:::: - - diff --git a/python/packages/autogen-core/samples/xlang/hello_python_agent/hello_python_agent.py b/python/packages/autogen-core/samples/xlang/hello_python_agent/hello_python_agent.py index c50aacedd5b9..178b91de8826 100644 --- a/python/packages/autogen-core/samples/xlang/hello_python_agent/hello_python_agent.py +++ b/python/packages/autogen-core/samples/xlang/hello_python_agent/hello_python_agent.py @@ -42,11 +42,11 @@ async def main() -> None: agnext_logger.info("2") - await UserProxy.register(runtime, "HelloAgents", lambda: UserProxy()) - await runtime.add_subscription(DefaultSubscription(agent_type="HelloAgents")) - await runtime.add_subscription(TypeSubscription(topic_type="agents.NewMessageReceived", agent_type="HelloAgents")) - await runtime.add_subscription(TypeSubscription(topic_type="agents.ConversationClosed", agent_type="HelloAgents")) - await runtime.add_subscription(TypeSubscription(topic_type="agents.Output", agent_type="HelloAgents")) + await UserProxy.register(runtime, "HelloAgent", lambda: UserProxy()) + await runtime.add_subscription(DefaultSubscription(agent_type="HelloAgent")) + await runtime.add_subscription(TypeSubscription(topic_type="agents.NewMessageReceived", agent_type="HelloAgent")) + await runtime.add_subscription(TypeSubscription(topic_type="agents.ConversationClosed", agent_type="HelloAgent")) + await runtime.add_subscription(TypeSubscription(topic_type="agents.Output", agent_type="HelloAgent")) agnext_logger.info("3") new_message = NewMessageReceived(message="from Python!") diff --git a/python/packages/autogen-core/src/autogen_core/code_executor/_func_with_reqs.py b/python/packages/autogen-core/src/autogen_core/code_executor/_func_with_reqs.py index b7f4fcaef8db..77fc0b831427 100644 --- a/python/packages/autogen-core/src/autogen_core/code_executor/_func_with_reqs.py +++ b/python/packages/autogen-core/src/autogen_core/code_executor/_func_with_reqs.py @@ -9,7 +9,7 @@ from importlib.abc import SourceLoader from importlib.util import module_from_spec, spec_from_loader from textwrap import dedent, indent -from typing import Any, Callable, Generic, List, Sequence, Set, TypeVar, Union +from typing import Any, Callable, Generic, List, Sequence, Set, Tuple, TypeVar, Union from typing_extensions import ParamSpec @@ -21,23 +21,38 @@ def _to_code(func: Union[FunctionWithRequirements[T, P], Callable[P, T], Functio if isinstance(func, FunctionWithRequirementsStr): return func.func - code = inspect.getsource(func) + if isinstance(func, FunctionWithRequirements): + code = inspect.getsource(func.func) + else: + code = inspect.getsource(func) # Strip the decorator if code.startswith("@"): code = code[code.index("\n") + 1 :] return code -@dataclass +@dataclass(frozen=True) class Alias: name: str alias: str -@dataclass +@dataclass(frozen=True) class ImportFromModule: module: str - imports: List[Union[str, Alias]] + imports: Tuple[Union[str, Alias], ...] + + ## backward compatibility + def __init__( + self, + module: str, + imports: Union[Tuple[Union[str, Alias], ...], List[Union[str, Alias]]], + ): + object.__setattr__(self, "module", module) + if isinstance(imports, list): + object.__setattr__(self, "imports", tuple(imports)) + else: + object.__setattr__(self, "imports", imports) Import = Union[str, ImportFromModule, Alias] diff --git a/python/packages/autogen-core/tests/test_code_executor.py b/python/packages/autogen-core/tests/test_code_executor.py new file mode 100644 index 000000000000..a8412c58d790 --- /dev/null +++ b/python/packages/autogen-core/tests/test_code_executor.py @@ -0,0 +1,53 @@ +import textwrap + +import pytest +from autogen_core.code_executor import ( + Alias, + FunctionWithRequirements, + FunctionWithRequirementsStr, + ImportFromModule, +) +from autogen_core.code_executor._func_with_reqs import build_python_functions_file +from pandas import DataFrame, concat + + +def template_function() -> DataFrame: # type: ignore + data1 = { + "name": ["John", "Anna"], + "location": ["New York", "Paris"], + "age": [24, 13], + } + data2 = { + "name": ["Peter", "Linda"], + "location": ["Berlin", "London"], + "age": [53, 33], + } + df1 = DataFrame.from_dict(data1) # type: ignore + df2 = DataFrame.from_dict(data2) # type: ignore + return concat([df1, df2]) # type: ignore + + +@pytest.mark.asyncio +async def test_hashability_Import() -> None: + function = FunctionWithRequirements.from_callable( # type: ignore + template_function, + ["pandas"], + [ImportFromModule("pandas", ["DataFrame", "concat"])], + ) + functions_module = build_python_functions_file([function]) # type: ignore + + assert "from pandas import DataFrame, concat" in functions_module + + function2: FunctionWithRequirementsStr = FunctionWithRequirements.from_str( + textwrap.dedent( + """ + def template_function2(): + return pd.Series([1, 2]) + """ + ), + "pandas", + [Alias("pandas", "pd")], + ) + functions_module2 = build_python_functions_file([function2]) + + assert "import pandas as pd" in functions_module2 diff --git a/python/packages/autogen-ext/src/autogen_ext/agents/openai/_openai_assistant_agent.py b/python/packages/autogen-ext/src/autogen_ext/agents/openai/_openai_assistant_agent.py index 4e16d7001d6c..f6058588a494 100644 --- a/python/packages/autogen-ext/src/autogen_ext/agents/openai/_openai_assistant_agent.py +++ b/python/packages/autogen-ext/src/autogen_ext/agents/openai/_openai_assistant_agent.py @@ -23,14 +23,14 @@ from autogen_agentchat.agents import BaseChatAgent from autogen_agentchat.base import Response from autogen_agentchat.messages import ( - AgentMessage, + AgentEvent, ChatMessage, HandoffMessage, MultiModalMessage, StopMessage, TextMessage, - ToolCallMessage, - ToolCallResultMessage, + ToolCallExecutionEvent, + ToolCallRequestEvent, ) from autogen_core import CancellationToken, FunctionCall from autogen_core.models._types import FunctionExecutionResult @@ -350,7 +350,7 @@ async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: async def on_messages_stream( self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken - ) -> AsyncGenerator[AgentMessage | Response, None]: + ) -> AsyncGenerator[AgentEvent | ChatMessage | Response, None]: """Handle incoming messages and return a response.""" await self._ensure_initialized() @@ -362,7 +362,7 @@ async def on_messages_stream( await self.handle_text_message(message.content, cancellation_token) # Inner messages for tool calls - inner_messages: List[AgentMessage] = [] + inner_messages: List[AgentEvent | ChatMessage] = [] # Create and start a run run: Run = await cancellation_token.link_future( @@ -402,7 +402,7 @@ async def on_messages_stream( ) # Add tool call message to inner messages - tool_call_msg = ToolCallMessage(source=self.name, content=tool_calls) + tool_call_msg = ToolCallRequestEvent(source=self.name, content=tool_calls) inner_messages.append(tool_call_msg) event_logger.debug(tool_call_msg) yield tool_call_msg @@ -414,7 +414,7 @@ async def on_messages_stream( tool_outputs.append(FunctionExecutionResult(content=result, call_id=tool_call.id)) # Add tool result message to inner messages - tool_result_msg = ToolCallResultMessage(source=self.name, content=tool_outputs) + tool_result_msg = ToolCallExecutionEvent(source=self.name, content=tool_outputs) inner_messages.append(tool_result_msg) event_logger.debug(tool_result_msg) yield tool_result_msg diff --git a/python/packages/autogen-ext/src/autogen_ext/agents/web_surfer/_multimodal_web_surfer.py b/python/packages/autogen-ext/src/autogen_ext/agents/web_surfer/_multimodal_web_surfer.py index 26907ca0b8fe..6cbcfd65f609 100644 --- a/python/packages/autogen-ext/src/autogen_ext/agents/web_surfer/_multimodal_web_surfer.py +++ b/python/packages/autogen-ext/src/autogen_ext/agents/web_surfer/_multimodal_web_surfer.py @@ -23,7 +23,7 @@ import PIL.Image from autogen_agentchat.agents import BaseChatAgent from autogen_agentchat.base import Response -from autogen_agentchat.messages import AgentMessage, ChatMessage, MultiModalMessage, TextMessage +from autogen_agentchat.messages import AgentEvent, ChatMessage, MultiModalMessage, TextMessage from autogen_core import EVENT_LOGGER_NAME, CancellationToken, FunctionCall from autogen_core import Image as AGImage from autogen_core.models import ( @@ -68,7 +68,7 @@ class MultimodalWebSurfer(BaseChatAgent): When :meth:`on_messages` or :meth:`on_messages_stream` is called, the following occurs: - 1) If this is the first call, the browser is initialized and the page is loaded. This is done in :meth:`_lazy_init`. + 1) If this is the first call, the browser is initialized and the page is loaded. This is done in :meth:`_lazy_init`. The browser is only closed when :meth:`close` is called. 2) The method :meth:`_generate_reply` is called, which then creates the final response as below. 3) The agent takes a screenshot of the page, extracts the interactive elements, and prepares a set-of-mark screenshot with bounding boxes around the interactive elements. 4) The agent makes a call to the :attr:`model_client` with the SOM screenshot, history of messages, and the list of available tools. @@ -131,6 +131,8 @@ async def main() -> None: # Run the team and stream messages to the console stream = agent_team.run_stream(task="Navigate to the AutoGen readme on GitHub.") await Console(stream) + # Close the browser controlled by the agent + await web_surfer_agent.close() asyncio.run(main()) @@ -283,6 +285,21 @@ async def _lazy_init( await self._set_debug_dir(self.debug_dir) self.did_lazy_init = True + async def close(self) -> None: + """ + Close the browser and the page. + Should be called when the agent is no longer needed. + """ + if self._page is not None: + await self._page.close() + self._page = None + if self._context is not None: + await self._context.close() + self._context = None + if self._playwright is not None: + await self._playwright.stop() + self._playwright = None + async def _set_debug_dir(self, debug_dir: str | None) -> None: assert self._page is not None if self.debug_dir is None: @@ -348,13 +365,13 @@ async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: async def on_messages_stream( self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken - ) -> AsyncGenerator[AgentMessage | Response, None]: + ) -> AsyncGenerator[AgentEvent | ChatMessage | Response, None]: for chat_message in messages: if isinstance(chat_message, TextMessage | MultiModalMessage): self._chat_history.append(UserMessage(content=chat_message.content, source=chat_message.source)) else: raise ValueError(f"Unexpected message in MultiModalWebSurfer: {chat_message}") - self.inner_messages: List[AgentMessage] = [] + self.inner_messages: List[AgentEvent | ChatMessage] = [] self.model_usage: List[RequestUsage] = [] try: content = await self._generate_reply(cancellation_token=cancellation_token) diff --git a/python/packages/autogen-ext/src/autogen_ext/tools/__init__.py b/python/packages/autogen-ext/src/autogen_ext/tools/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/packages/autogen-ext/tests/test_websurfer_agent.py b/python/packages/autogen-ext/tests/test_websurfer_agent.py index dfc863057b31..d8a36e4d9549 100644 --- a/python/packages/autogen-ext/tests/test_websurfer_agent.py +++ b/python/packages/autogen-ext/tests/test_websurfer_agent.py @@ -140,7 +140,7 @@ async def test_run_websurfer(monkeypatch: pytest.MonkeyPatch) -> None: result.messages[2] # type: ignore .content[0] # type: ignore .startswith( # type: ignore - "I am waiting a short period of time before taking further action.\n\n Here is a screenshot of the webpage: [Search - Microsoft Bing](https://www.bing.com/)" + "I am waiting a short period of time before taking further action.\n\n Here is a screenshot of the webpage:" ) ) # type: ignore url_after_sleep = agent._page.url # type: ignore diff --git a/python/packages/autogen-studio/autogenstudio/cli.py b/python/packages/autogen-studio/autogenstudio/cli.py index 90f4331f0b75..24e0d5549beb 100644 --- a/python/packages/autogen-studio/autogenstudio/cli.py +++ b/python/packages/autogen-studio/autogenstudio/cli.py @@ -54,7 +54,7 @@ def ui( @app.command() def serve( - workflow: str = "", + team: str = "", host: str = "127.0.0.1", port: int = 8084, workers: int = 1, @@ -64,9 +64,9 @@ def serve( Serve an API Endpoint based on an AutoGen Studio workflow json file. Args: - workflow (str): Path to the workflow json file. + team (str): Path to the team json file. host (str, optional): Host to run the UI on. Defaults to 127.0.0.1 (localhost). - port (int, optional): Port to run the UI on. Defaults to 8081. + port (int, optional): Port to run the UI on. Defaults to 8084 workers (int, optional): Number of workers to run the UI with. Defaults to 1. reload (bool, optional): Whether to reload the UI on code changes. Defaults to False. docs (bool, optional): Whether to generate API docs. Defaults to False. @@ -74,7 +74,11 @@ def serve( """ os.environ["AUTOGENSTUDIO_API_DOCS"] = str(docs) - os.environ["AUTOGENSTUDIO_WORKFLOW_FILE"] = workflow + os.environ["AUTOGENSTUDIO_TEAM_FILE"] = team + + # validate the team file + if not os.path.exists(team): + raise ValueError(f"Team file not found: {team}") uvicorn.run( "autogenstudio.web.serve:app", diff --git a/python/packages/autogen-studio/autogenstudio/database/component_factory.py b/python/packages/autogen-studio/autogenstudio/database/component_factory.py index b9cfdba56783..b954b39c0f4b 100644 --- a/python/packages/autogen-studio/autogenstudio/database/component_factory.py +++ b/python/packages/autogen-studio/autogenstudio/database/component_factory.py @@ -18,26 +18,37 @@ TokenUsageTermination, ) from autogen_agentchat.teams import MagenticOneGroupChat, RoundRobinGroupChat, SelectorGroupChat -from autogen_core.components.tools import FunctionTool +from autogen_core.tools import FunctionTool from autogen_ext.agents.file_surfer import FileSurfer from autogen_ext.agents.magentic_one import MagenticOneCoderAgent from autogen_ext.agents.web_surfer import MultimodalWebSurfer -from autogen_ext.models import OpenAIChatCompletionClient +from autogen_ext.models.openai import AzureOpenAIChatCompletionClient, OpenAIChatCompletionClient from ..datamodel.types import ( AgentConfig, AgentTypes, + AssistantAgentConfig, + AzureOpenAIModelConfig, + CombinationTerminationConfig, ComponentConfig, ComponentConfigInput, ComponentTypes, + MagenticOneTeamConfig, + MaxMessageTerminationConfig, ModelConfig, ModelTypes, + MultimodalWebSurferAgentConfig, + OpenAIModelConfig, + RoundRobinTeamConfig, + SelectorTeamConfig, TeamConfig, TeamTypes, TerminationConfig, TerminationTypes, + TextMentionTerminationConfig, ToolConfig, ToolTypes, + UserProxyAgentConfig, ) from ..utils.utils import Version @@ -45,7 +56,7 @@ TeamComponent = Union[RoundRobinGroupChat, SelectorGroupChat, MagenticOneGroupChat] AgentComponent = Union[AssistantAgent, MultimodalWebSurfer, UserProxyAgent, FileSurfer, MagenticOneCoderAgent] -ModelComponent = Union[OpenAIChatCompletionClient] +ModelComponent = Union[OpenAIChatCompletionClient, AzureOpenAIChatCompletionClient] ToolComponent = Union[FunctionTool] # Will grow with more tool types TerminationComponent = Union[ MaxMessageTermination, @@ -87,7 +98,7 @@ class ComponentFactory: } def __init__(self): - self._model_cache: Dict[str, OpenAIChatCompletionClient] = {} + self._model_cache: Dict[str, ModelComponent] = {} self._tool_cache: Dict[str, FunctionTool] = {} self._last_cache_clear = datetime.now() @@ -172,23 +183,58 @@ async def load_directory( return components def _dict_to_config(self, config_dict: dict) -> ComponentConfig: - """Convert dictionary to appropriate config type based on component_type""" + """Convert dictionary to appropriate config type based on component_type and type discriminator""" if "component_type" not in config_dict: raise ValueError("component_type is required in configuration") - config_types = { - ComponentTypes.TEAM: TeamConfig, - ComponentTypes.AGENT: AgentConfig, - ComponentTypes.MODEL: ModelConfig, + component_type = ComponentTypes(config_dict["component_type"]) + + # Define mapping structure + type_mappings = { + ComponentTypes.MODEL: { + "discriminator": "model_type", + ModelTypes.OPENAI.value: OpenAIModelConfig, + ModelTypes.AZUREOPENAI.value: AzureOpenAIModelConfig, + }, + ComponentTypes.AGENT: { + "discriminator": "agent_type", + AgentTypes.ASSISTANT.value: AssistantAgentConfig, + AgentTypes.USERPROXY.value: UserProxyAgentConfig, + AgentTypes.MULTIMODAL_WEBSURFER.value: MultimodalWebSurferAgentConfig, + }, + ComponentTypes.TEAM: { + "discriminator": "team_type", + TeamTypes.ROUND_ROBIN.value: RoundRobinTeamConfig, + TeamTypes.SELECTOR.value: SelectorTeamConfig, + TeamTypes.MAGENTIC_ONE.value: MagenticOneTeamConfig, + }, ComponentTypes.TOOL: ToolConfig, - ComponentTypes.TERMINATION: TerminationConfig, # Add mapping for termination + ComponentTypes.TERMINATION: { + "discriminator": "termination_type", + TerminationTypes.MAX_MESSAGES.value: MaxMessageTerminationConfig, + TerminationTypes.TEXT_MENTION.value: TextMentionTerminationConfig, + TerminationTypes.COMBINATION.value: CombinationTerminationConfig, + }, } - component_type = ComponentTypes(config_dict["component_type"]) - config_class = config_types.get(component_type) + mapping = type_mappings.get(component_type) + if not mapping: + raise ValueError(f"Unknown component type: {component_type}") + + # Handle simple cases (no discriminator) + if isinstance(mapping, type): + return mapping(**config_dict) + + # Get discriminator field value + discriminator = mapping["discriminator"] + if discriminator not in config_dict: + raise ValueError(f"Missing {discriminator} in configuration") + + type_value = config_dict[discriminator] + config_class = mapping.get(type_value) if not config_class: - raise ValueError(f"Unknown component type: {component_type}") + raise ValueError(f"Unknown {discriminator}: {type_value}") return config_class(**config_dict) @@ -241,11 +287,6 @@ async def load_team(self, config: TeamConfig, input_func: Optional[Callable] = N agent = await self.load(participant, input_func=input_func) participants.append(agent) - # Load model client if specified - model_client = None - if config.model_client: - model_client = await self.load(config.model_client) - # Load termination condition if specified termination = None if config.termination_condition: @@ -255,6 +296,7 @@ async def load_team(self, config: TeamConfig, input_func: Optional[Callable] = N if config.team_type == TeamTypes.ROUND_ROBIN: return RoundRobinGroupChat(participants=participants, termination_condition=termination) elif config.team_type == TeamTypes.SELECTOR: + model_client = await self.load(config.model_client) if not model_client: raise ValueError("SelectorGroupChat requires a model_client") selector_prompt = config.selector_prompt if config.selector_prompt else DEFAULT_SELECTOR_PROMPT @@ -265,6 +307,7 @@ async def load_team(self, config: TeamConfig, input_func: Optional[Callable] = N selector_prompt=selector_prompt, ) elif config.team_type == TeamTypes.MAGENTIC_ONE: + model_client = await self.load(config.model_client) if not model_client: raise ValueError("MagenticOneGroupChat requires a model_client") return MagenticOneGroupChat( @@ -282,21 +325,20 @@ async def load_team(self, config: TeamConfig, input_func: Optional[Callable] = N async def load_agent(self, config: AgentConfig, input_func: Optional[Callable] = None) -> AgentComponent: """Create agent instance from configuration.""" - try: - # Load model client if specified - model_client = None - if config.model_client: - model_client = await self.load(config.model_client) - system_message = config.system_message if config.system_message else "You are a helpful assistant" - - # Load tools if specified - tools = [] - if config.tools: - for tool_config in config.tools: - tool = await self.load(tool_config) - tools.append(tool) + model_client = None + system_message = None + tools = [] + if hasattr(config, "system_message") and config.system_message: + system_message = config.system_message + if hasattr(config, "model_client") and config.model_client: + model_client = await self.load(config.model_client) + if hasattr(config, "tools") and config.tools: + for tool_config in config.tools: + tool = await self.load(tool_config) + tools.append(tool) + try: if config.agent_type == AgentTypes.USERPROXY: return UserProxyAgent( name=config.name, @@ -304,6 +346,8 @@ async def load_agent(self, config: AgentConfig, input_func: Optional[Callable] = input_func=input_func, # Pass through to UserProxyAgent ) elif config.agent_type == AgentTypes.ASSISTANT: + system_message = config.system_message if config.system_message else "You are a helpful assistant" + return AssistantAgent( name=config.name, description=config.description or "A helpful assistant", @@ -316,8 +360,8 @@ async def load_agent(self, config: AgentConfig, input_func: Optional[Callable] = name=config.name, model_client=model_client, headless=config.headless if config.headless is not None else True, - debug_dir=config.logs_dir if config.logs_dir is not None else "logs", - downloads_folder=config.logs_dir if config.logs_dir is not None else "logs", + debug_dir=config.logs_dir if config.logs_dir is not None else None, + downloads_folder=config.logs_dir if config.logs_dir is not None else None, to_save_screenshots=config.to_save_screenshots if config.to_save_screenshots is not None else False, use_ocr=config.use_ocr if config.use_ocr is not None else False, animate_actions=config.animate_actions if config.animate_actions is not None else False, @@ -349,7 +393,26 @@ async def load_model(self, config: ModelConfig) -> ModelComponent: return self._model_cache[cache_key] if config.model_type == ModelTypes.OPENAI: - model = OpenAIChatCompletionClient(model=config.model, api_key=config.api_key, base_url=config.base_url) + args = { + "model": config.model, + "api_key": config.api_key, + "base_url": config.base_url, + } + + if hasattr(config, "model_capabilities") and config.model_capabilities is not None: + args["model_capabilities"] = config.model_capabilities + + model = OpenAIChatCompletionClient(**args) + self._model_cache[cache_key] = model + return model + elif config.model_type == ModelTypes.AZUREOPENAI: + model = AzureOpenAIChatCompletionClient( + azure_deployment=config.azure_deployment, + model=config.model, + api_version=config.api_version, + azure_endpoint=config.azure_endpoint, + api_key=config.api_key, + ) self._model_cache[cache_key] = model return model else: diff --git a/python/packages/autogen-studio/autogenstudio/datamodel/db.py b/python/packages/autogen-studio/autogenstudio/datamodel/db.py index 6395a535fd6b..45f439d33910 100644 --- a/python/packages/autogen-studio/autogenstudio/datamodel/db.py +++ b/python/packages/autogen-studio/autogenstudio/datamodel/db.py @@ -118,7 +118,7 @@ class Tool(SQLModel, table=True): ) # pylint: disable=not-callable user_id: Optional[str] = None version: Optional[str] = "0.0.1" - config: Union[ToolConfig, dict] = Field(default_factory=ToolConfig, sa_column=Column(JSON)) + config: Union[ToolConfig, dict] = Field(sa_column=Column(JSON)) agents: List["Agent"] = Relationship(back_populates="tools", link_model=AgentToolLink) @@ -135,7 +135,7 @@ class Model(SQLModel, table=True): ) # pylint: disable=not-callable user_id: Optional[str] = None version: Optional[str] = "0.0.1" - config: Union[ModelConfig, dict] = Field(default_factory=ModelConfig, sa_column=Column(JSON)) + config: Union[ModelConfig, dict] = Field(sa_column=Column(JSON)) agents: List["Agent"] = Relationship(back_populates="models", link_model=AgentModelLink) @@ -152,7 +152,7 @@ class Team(SQLModel, table=True): ) # pylint: disable=not-callable user_id: Optional[str] = None version: Optional[str] = "0.0.1" - config: Union[TeamConfig, dict] = Field(default_factory=TeamConfig, sa_column=Column(JSON)) + config: Union[TeamConfig, dict] = Field(sa_column=Column(JSON)) agents: List["Agent"] = Relationship(back_populates="teams", link_model=TeamAgentLink) @@ -169,7 +169,7 @@ class Agent(SQLModel, table=True): ) # pylint: disable=not-callable user_id: Optional[str] = None version: Optional[str] = "0.0.1" - config: Union[AgentConfig, dict] = Field(default_factory=AgentConfig, sa_column=Column(JSON)) + config: Union[AgentConfig, dict] = Field(sa_column=Column(JSON)) tools: List[Tool] = Relationship(back_populates="agents", link_model=AgentToolLink) models: List[Model] = Relationship(back_populates="agents", link_model=AgentModelLink) teams: List[Team] = Relationship(back_populates="agents", link_model=TeamAgentLink) diff --git a/python/packages/autogen-studio/autogenstudio/datamodel/types.py b/python/packages/autogen-studio/autogenstudio/datamodel/types.py index ea66c342b0c3..eb02fb121ebe 100644 --- a/python/packages/autogen-studio/autogenstudio/datamodel/types.py +++ b/python/packages/autogen-studio/autogenstudio/datamodel/types.py @@ -1,14 +1,16 @@ from datetime import datetime from enum import Enum from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, List, Optional, Union from autogen_agentchat.base import TaskResult -from pydantic import BaseModel +from autogen_core.models import ModelCapabilities +from pydantic import BaseModel, Field class ModelTypes(str, Enum): OPENAI = "OpenAIChatCompletionClient" + AZUREOPENAI = "AzureOpenAIChatCompletionClient" class ToolTypes(str, Enum): @@ -56,12 +58,30 @@ class MessageConfig(BaseModel): message_type: Optional[str] = "text" -class ModelConfig(BaseConfig): +class BaseModelConfig(BaseConfig): model: str model_type: ModelTypes api_key: Optional[str] = None base_url: Optional[str] = None component_type: ComponentTypes = ComponentTypes.MODEL + model_capabilities: Optional[ModelCapabilities] = None + + +class OpenAIModelConfig(BaseModelConfig): + model_type: ModelTypes = ModelTypes.OPENAI + + +class AzureOpenAIModelConfig(BaseModelConfig): + azure_deployment: str + model: str + api_version: str + azure_endpoint: str + azure_ad_token_provider: Optional[str] = None + api_key: Optional[str] = None + model_type: ModelTypes = ModelTypes.AZUREOPENAI + + +ModelConfig = OpenAIModelConfig | AzureOpenAIModelConfig class ToolConfig(BaseConfig): @@ -72,43 +92,100 @@ class ToolConfig(BaseConfig): component_type: ComponentTypes = ComponentTypes.TOOL -class AgentConfig(BaseConfig): +class BaseAgentConfig(BaseConfig): name: str agent_type: AgentTypes - system_message: Optional[str] = None - model_client: Optional[ModelConfig] = None - tools: Optional[List[ToolConfig]] = None description: Optional[str] = None component_type: ComponentTypes = ComponentTypes.AGENT - headless: Optional[bool] = None - logs_dir: Optional[str] = None - to_save_screenshots: Optional[bool] = None - use_ocr: Optional[bool] = None - animate_actions: Optional[bool] = None -class TerminationConfig(BaseConfig): +class AssistantAgentConfig(BaseAgentConfig): + agent_type: AgentTypes = AgentTypes.ASSISTANT + model_client: ModelConfig + tools: Optional[List[ToolConfig]] = None + system_message: Optional[str] = None + + +class UserProxyAgentConfig(BaseAgentConfig): + agent_type: AgentTypes = AgentTypes.USERPROXY + + +class MultimodalWebSurferAgentConfig(BaseAgentConfig): + agent_type: AgentTypes = AgentTypes.MULTIMODAL_WEBSURFER + model_client: ModelConfig + headless: bool = True + logs_dir: str = None + to_save_screenshots: bool = False + use_ocr: bool = False + animate_actions: bool = False + tools: Optional[List[ToolConfig]] = None + + +AgentConfig = AssistantAgentConfig | UserProxyAgentConfig | MultimodalWebSurferAgentConfig + + +class BaseTerminationConfig(BaseConfig): termination_type: TerminationTypes - # Fields for basic terminations - max_messages: Optional[int] = None - text: Optional[str] = None - # Fields for combinations - operator: Optional[Literal["and", "or"]] = None - conditions: Optional[List["TerminationConfig"]] = None component_type: ComponentTypes = ComponentTypes.TERMINATION -class TeamConfig(BaseConfig): +class MaxMessageTerminationConfig(BaseTerminationConfig): + termination_type: TerminationTypes = TerminationTypes.MAX_MESSAGES + max_messages: int + + +class TextMentionTerminationConfig(BaseTerminationConfig): + termination_type: TerminationTypes = TerminationTypes.TEXT_MENTION + text: str + + +class StopMessageTerminationConfig(BaseTerminationConfig): + termination_type: TerminationTypes = TerminationTypes.STOP_MESSAGE + + +class CombinationTerminationConfig(BaseTerminationConfig): + termination_type: TerminationTypes = TerminationTypes.COMBINATION + operator: str + conditions: List["TerminationConfig"] + + +TerminationConfig = ( + MaxMessageTerminationConfig + | TextMentionTerminationConfig + | CombinationTerminationConfig + | StopMessageTerminationConfig +) + + +class BaseTeamConfig(BaseConfig): name: str participants: List[AgentConfig] team_type: TeamTypes - model_client: Optional[ModelConfig] = None - selector_prompt: Optional[str] = None termination_condition: Optional[TerminationConfig] = None component_type: ComponentTypes = ComponentTypes.TEAM max_turns: Optional[int] = None +class RoundRobinTeamConfig(BaseTeamConfig): + team_type: TeamTypes = TeamTypes.ROUND_ROBIN + + +class SelectorTeamConfig(BaseTeamConfig): + team_type: TeamTypes = TeamTypes.SELECTOR + selector_prompt: Optional[str] = None + model_client: ModelConfig + + +class MagenticOneTeamConfig(BaseTeamConfig): + team_type: TeamTypes = TeamTypes.MAGENTIC_ONE + model_client: ModelConfig + max_stalls: int = 3 + final_answer_prompt: Optional[str] = None + + +TeamConfig = RoundRobinTeamConfig | SelectorTeamConfig | MagenticOneTeamConfig + + class TeamResult(BaseModel): task_result: TaskResult usage: str diff --git a/python/packages/autogen-studio/autogenstudio/teammanager.py b/python/packages/autogen-studio/autogenstudio/teammanager.py index bc3e01577082..b4ba92460ef3 100644 --- a/python/packages/autogen-studio/autogenstudio/teammanager.py +++ b/python/packages/autogen-studio/autogenstudio/teammanager.py @@ -2,7 +2,7 @@ from typing import AsyncGenerator, Callable, Optional, Union from autogen_agentchat.base import TaskResult -from autogen_agentchat.messages import AgentMessage, ChatMessage +from autogen_agentchat.messages import AgentEvent, ChatMessage from autogen_core import CancellationToken from .database import Component, ComponentFactory @@ -27,7 +27,7 @@ async def run_stream( team_config: ComponentConfigInput, input_func: Optional[Callable] = None, cancellation_token: Optional[CancellationToken] = None, - ) -> AsyncGenerator[Union[AgentMessage, ChatMessage, TaskResult], None]: + ) -> AsyncGenerator[Union[AgentEvent | ChatMessage, ChatMessage, TaskResult], None]: """Stream the team's execution results""" start_time = time.time() @@ -44,6 +44,11 @@ async def run_stream( else: yield message + # close agent resources + for agent in team._participants: + if hasattr(agent, "close"): + await agent.close() + except Exception as e: raise e @@ -60,4 +65,9 @@ async def run( team = await self._create_team(team_config, input_func) result = await team.run(task=task, cancellation_token=cancellation_token) + # close agent resources + for agent in team._participants: + if hasattr(agent, "close"): + await agent.close() + return self._create_result(result, start_time) diff --git a/python/packages/autogen-studio/autogenstudio/web/managers/connection.py b/python/packages/autogen-studio/autogenstudio/web/managers/connection.py index 23e835ca5ca1..271b53d87675 100644 --- a/python/packages/autogen-studio/autogenstudio/web/managers/connection.py +++ b/python/packages/autogen-studio/autogenstudio/web/managers/connection.py @@ -6,14 +6,14 @@ from autogen_agentchat.base._task import TaskResult from autogen_agentchat.messages import ( - AgentMessage, + AgentEvent, ChatMessage, HandoffMessage, MultiModalMessage, StopMessage, TextMessage, - ToolCallMessage, - ToolCallResultMessage, + ToolCallExecutionEvent, + ToolCallRequestEvent, ) from autogen_core import CancellationToken from autogen_core import Image as AGImage @@ -108,8 +108,8 @@ async def start_stream(self, run_id: UUID, task: str, team_config: dict) -> None MultiModalMessage, StopMessage, HandoffMessage, - ToolCallMessage, - ToolCallResultMessage, + ToolCallRequestEvent, + ToolCallExecutionEvent, ), ): await self._save_message(run_id, message) @@ -141,7 +141,7 @@ async def start_stream(self, run_id: UUID, task: str, team_config: dict) -> None finally: self._cancellation_tokens.pop(run_id, None) - async def _save_message(self, run_id: UUID, message: Union[AgentMessage, ChatMessage]) -> None: + async def _save_message(self, run_id: UUID, message: Union[AgentEvent | ChatMessage, ChatMessage]) -> None: """Save a message to the database""" run = await self._get_run(run_id) if run: @@ -276,7 +276,8 @@ async def _handle_stream_error(self, run_id: UUID, error: Exception) -> None: if run_id not in self._closed_connections: error_result = TeamResult( task_result=TaskResult( - messages=[TextMessage(source="system", content=str(error))], stop_reason="error" + messages=[TextMessage(source="system", content=str(error))], + stop_reason="An error occurred while processing this run", ), usage="", duration=0, @@ -324,7 +325,7 @@ def _format_message(self, message: Any) -> Optional[dict]: } elif isinstance( - message, (TextMessage, StopMessage, HandoffMessage, ToolCallMessage, ToolCallResultMessage) + message, (TextMessage, StopMessage, HandoffMessage, ToolCallRequestEvent, ToolCallExecutionEvent) ): return {"type": "message", "data": message.model_dump()} diff --git a/python/packages/autogen-studio/autogenstudio/web/serve.py b/python/packages/autogen-studio/autogenstudio/web/serve.py index 462615378b8a..e75285f15f86 100644 --- a/python/packages/autogen-studio/autogenstudio/web/serve.py +++ b/python/packages/autogen-studio/autogenstudio/web/serve.py @@ -6,23 +6,23 @@ from fastapi import FastAPI from ..datamodel import Response -from ..workflowmanager import WorkflowManager +from ..teammanager import TeamManager app = FastAPI() -workflow_file_path = os.environ.get("AUTOGENSTUDIO_WORKFLOW_FILE", None) +team_file_path = os.environ.get("AUTOGENSTUDIO_TEAM_FILE", None) -if workflow_file_path: - workflow_manager = WorkflowManager(workflow=workflow_file_path) +if team_file_path: + team_manager = TeamManager() else: - raise ValueError("Workflow file must be specified") + raise ValueError("Team file must be specified") @app.get("/predict/{task}") async def predict(task: str): response = Response(message="Task successfully completed", status=True, data=None) try: - result_message = workflow_manager.run(message=task, clear_history=False) + result_message = await team_manager.run(task=task, team_config=team_file_path) response.data = result_message except Exception as e: response.message = str(e) diff --git a/python/packages/autogen-studio/frontend/package.json b/python/packages/autogen-studio/frontend/package.json index 5beb6e6205b5..98d7d00ec00d 100644 --- a/python/packages/autogen-studio/frontend/package.json +++ b/python/packages/autogen-studio/frontend/package.json @@ -42,6 +42,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.6.1", "tailwindcss": "^3.4.14", "yarn": "^1.22.22", "zustand": "^5.0.1" @@ -51,7 +52,7 @@ "@types/node": "^22.9.0", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", - "@types/react-syntax-highlighter": "^15.5.10", + "@types/react-syntax-highlighter": "^15.5.13", "@types/uuid": "^10.0.0", "typescript": "^5.3.3" } diff --git a/python/packages/autogen-studio/frontend/src/components/sidebar.tsx b/python/packages/autogen-studio/frontend/src/components/sidebar.tsx index 7fbef8c57369..8071cb50294d 100644 --- a/python/packages/autogen-studio/frontend/src/components/sidebar.tsx +++ b/python/packages/autogen-studio/frontend/src/components/sidebar.tsx @@ -10,6 +10,7 @@ import { PanelLeftClose, PanelLeftOpen, GalleryHorizontalEnd, + Rocket, } from "lucide-react"; import Icon from "./icons"; @@ -43,6 +44,12 @@ const navigation: INavItem[] = [ icon: GalleryHorizontalEnd, breadcrumbs: [{ name: "Gallery", href: "/gallery", current: true }], }, + { + name: "Deploy", + href: "/deploy", + icon: Rocket, + breadcrumbs: [{ name: "Deploy", href: "/deploy", current: true }], + }, ]; const classNames = (...classes: (string | undefined | boolean)[]) => { diff --git a/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/docker.tsx b/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/docker.tsx new file mode 100644 index 000000000000..64b7d520ba12 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/docker.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { Alert } from "antd"; +import { CodeSection, copyToClipboard } from "./guides"; + +const DockerGuide: React.FC = () => { + return ( +
+

Docker Container Setup

+ + +
  • Docker installed on your system
  • + + } + type="info" + /> + + AutoGen Studio provides a + + Dockerfile + + that you can use to build your Docker container.{" "} +
    + code={`FROM mcr.microsoft.com/devcontainers/python:3.10 + +WORKDIR /code + +RUN pip install -U gunicorn autogenstudio + +RUN useradd -m -u 1000 user +USER user +ENV HOME=/home/user + PATH=/home/user/.local/bin:$PATH + AUTOGENSTUDIO_APPDIR=/home/user/app + +WORKDIR $HOME/app + +COPY --chown=user . $HOME/app + +CMD gunicorn -w $((2 * $(getconf _NPROCESSORS_ONLN) + 1)) --timeout 12600 -k uvicorn.workers.UvicornWorker autogenstudio.web.app:app --bind "0.0.0.0:8081"`} + onCopy={copyToClipboard} + /> + + {/* Build and Run */} + +
    + ); +}; + +export default DockerGuide; diff --git a/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/guides.tsx b/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/guides.tsx new file mode 100644 index 000000000000..4da98faf77c2 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/guides.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { Copy } from "lucide-react"; +import { Guide } from "../types"; +import PythonGuide from "./python"; +import DockerGuide from "./docker"; +import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; +import js from "react-syntax-highlighter/dist/esm/languages/prism/javascript"; +import python from "react-syntax-highlighter/dist/esm/languages/prism/python"; + +import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; + +SyntaxHighlighter.registerLanguage("javascript", js); +SyntaxHighlighter.registerLanguage("python", python); + +interface GuideContentProps { + guide: Guide; +} + +export const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); +}; +export const GuideContent: React.FC = ({ guide }) => { + // Render different content based on guide type and id + switch (guide.id) { + case "python-setup": + return ; + + case "docker-setup": + return ; + + // Add more cases for other guides... + + default: + return ( +
    + A Guide with the title {guide.title} is work in + progress! +
    + ); + } +}; + +interface CodeSectionProps { + title: string; + description?: string | React.ReactNode; + code?: string; + onCopy: (text: string) => void; + language?: string; +} + +export const CodeSection: React.FC = ({ + title, + description, + code, + onCopy, + language = "python", +}) => ( +
    +

    {title}

    + {description &&

    {description}

    } + {code && ( +
    + + {/* overflow scroll custom style */} + + {code} + +
    + )} +
    +); + +export default GuideContent; diff --git a/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/python.tsx b/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/python.tsx new file mode 100644 index 000000000000..de3b7e4803f0 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/deploy/guides/python.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { Alert } from "antd"; +import { CodeSection, copyToClipboard } from "./guides"; +import { Download } from "lucide-react"; + +const PythonGuide: React.FC = () => { + return ( +
    +

    + Using AutoGen Studio Teams in Python Code and REST API +

    + + +
  • AutoGen Studio installed
  • + + } + type="info" + /> + +
    + {" "} + You can reuse the declarative specifications of agent teams created in + AutoGen studio in your python application by using the TeamManager + class. . In TeamBuilder, select a team configuration and click download.{" "} + {" "} +
    + + {/* Installation Steps */} +
    + {/* Basic Usage */} + + + + AutoGen Studio offers a convenience CLI command to serve a team as a + REST API endpoint.{" "} +
    + code={` +autogenstudio serve --team path/to/team.json --port 8084 + `} + onCopy={copyToClipboard} + /> +
    + + ); +}; + +export default PythonGuide; diff --git a/python/packages/autogen-studio/frontend/src/components/views/deploy/manager.tsx b/python/packages/autogen-studio/frontend/src/components/views/deploy/manager.tsx new file mode 100644 index 000000000000..b52f5d9ec18a --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/deploy/manager.tsx @@ -0,0 +1,86 @@ +import React, { useState, useEffect } from "react"; +import { ChevronRight, TriangleAlert } from "lucide-react"; +import { DeploySidebar } from "./sidebar"; +import { Guide, defaultGuides } from "./types"; +import { GuideContent } from "./guides/guides"; + +export const DeployManager: React.FC = () => { + const [isLoading, setIsLoading] = useState(false); + const [guides, setGuides] = useState(defaultGuides); + const [currentGuide, setCurrentGuide] = useState(null); + const [isSidebarOpen, setIsSidebarOpen] = useState(() => { + if (typeof window !== "undefined") { + const stored = localStorage.getItem("deploySidebar"); + return stored !== null ? JSON.parse(stored) : true; + } + return true; + }); + + // Persist sidebar state + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem("deploySidebar", JSON.stringify(isSidebarOpen)); + } + }, [isSidebarOpen]); + + // Set first guide as current if none selected + useEffect(() => { + if (!currentGuide && guides.length > 0) { + setCurrentGuide(guides[0]); + } + }, [guides, currentGuide]); + + return ( +
    + {/* Sidebar */} +
    + setIsSidebarOpen(!isSidebarOpen)} + onSelectGuide={setCurrentGuide} + isLoading={isLoading} + /> +
    + + {/* Main Content */} +
    +
    + {/* Breadcrumb */} +
    + Deploy + {currentGuide && ( + <> + + {currentGuide.title} + + )} +
    +
    + {" "} + The deployment guide section is work in progress. +
    + {/* Content Area */} + {currentGuide ? ( + + ) : ( +
    + Select a guide from the sidebar to get started +
    + )} +
    +
    +
    + ); +}; + +export default DeployManager; diff --git a/python/packages/autogen-studio/frontend/src/components/views/deploy/sidebar.tsx b/python/packages/autogen-studio/frontend/src/components/views/deploy/sidebar.tsx new file mode 100644 index 000000000000..6b59c62c33f4 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/deploy/sidebar.tsx @@ -0,0 +1,111 @@ +import React from "react"; +import { Button, Tooltip } from "antd"; +import { + PanelLeftClose, + PanelLeftOpen, + Book, + InfoIcon, + RefreshCcw, +} from "lucide-react"; +import type { Guide } from "./types"; + +interface DeploySidebarProps { + isOpen: boolean; + guides: Guide[]; + currentGuide: Guide | null; + onToggle: () => void; + onSelectGuide: (guide: Guide) => void; + isLoading?: boolean; +} + +export const DeploySidebar: React.FC = ({ + isOpen, + guides, + currentGuide, + onToggle, + onSelectGuide, + isLoading = false, +}) => { + // Render collapsed state + if (!isOpen) { + return ( +
    +
    + + + +
    +
    + ); + } + + return ( +
    + {/* Header */} +
    +
    + {/* */} + Guides + {/* + {guides.length} + */} +
    + + + +
    + + {/* Loading State */} + {isLoading && ( +
    + +
    + )} + + {/* Empty State */} + {!isLoading && guides.length === 0 && ( +
    + + No deployment guide available +
    + )} + + {/* Guides List */} +
    + {guides.map((guide) => ( +
    +
    +
    onSelectGuide(guide)} + > + {/* Guide Title */} +
    + {guide.title} +
    +
    +
    + ))} +
    +
    + ); +}; diff --git a/python/packages/autogen-studio/frontend/src/components/views/deploy/types.tsx b/python/packages/autogen-studio/frontend/src/components/views/deploy/types.tsx new file mode 100644 index 000000000000..dc9ab0c73357 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/deploy/types.tsx @@ -0,0 +1,23 @@ +export interface Guide { + id: string; + title: string; + type: "python" | "docker" | "cloud"; +} + +export const defaultGuides: Guide[] = [ + { + id: "python-setup", + title: "Python", + type: "python", + }, + { + id: "docker-setup", + title: "Docker", + type: "docker", + }, + // { + // id: "cloud-deploy", + // title: "Cloud", + // type: "cloud", + // }, +]; diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx index 67c1ad8bead6..57557ec1527a 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx @@ -117,6 +117,9 @@ const RunView: React.FC = ({ } }; + const lastResultMessage = run.team_result?.task_result.messages.slice(-1)[0]; + const lastMessage = run.messages.slice(-1)[0]; + return (
    {/* Run Header */} @@ -193,14 +196,23 @@ const RunView: React.FC = ({ Stop reason: {run.team_result?.task_result?.stop_reason}
    - + {lastMessage ? ( + + ) : ( + <> + {lastResultMessage && ( + + )} + + )}
    )} diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx index 92e813efbc7b..6abcb4cfcf69 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx @@ -292,7 +292,7 @@ export const TeamBuilder: React.FC = ({
    - +
    {/* Section Label */} -
    - Recents - {isLoading && ( - - )} +
    +
    Recents
    + {isLoading && }
    {/* Teams List */} @@ -137,9 +135,7 @@ export const TeamSidebar: React.FC = ({ {teams.length > 0 && (
    {" "} {teams.map((team) => ( diff --git a/python/packages/autogen-studio/frontend/src/pages/deploy.tsx b/python/packages/autogen-studio/frontend/src/pages/deploy.tsx new file mode 100644 index 000000000000..5f301ae75293 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/pages/deploy.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import Layout from "../components/layout"; +import { graphql } from "gatsby"; +import DeployManager from "../components/views/deploy/manager"; + +// markup +const DeployPage = ({ data }: any) => { + return ( + +
    + +
    +
    + ); +}; + +export const query = graphql` + query HomePageQuery { + site { + siteMetadata { + description + title + } + } + } +`; + +export default DeployPage; diff --git a/python/packages/autogen-studio/frontend/yarn.lock b/python/packages/autogen-studio/frontend/yarn.lock index e5c7d8823620..dc00db5a4633 100644 --- a/python/packages/autogen-studio/frontend/yarn.lock +++ b/python/packages/autogen-studio/frontend/yarn.lock @@ -22,19 +22,19 @@ dependencies: "@ctrl/tinycolor" "^3.6.1" -"@ant-design/cssinjs-utils@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.1.tgz#57abb43671023f937348bd33442862c60ac8e8b2" - integrity sha512-2HAiyGGGnM0es40SxdszeQAU5iWp41wBIInq+ONTCKjlSKOrzQfnw4JDtB8IBmqE6tQaEKwmzTP2LGdt5DSwYQ== +"@ant-design/cssinjs-utils@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz#5dd79126057920a6992d57b38dd84e2c0b707977" + integrity sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg== dependencies: "@ant-design/cssinjs" "^1.21.0" "@babel/runtime" "^7.23.2" rc-util "^5.38.0" "@ant-design/cssinjs@^1.21.0", "@ant-design/cssinjs@^1.21.1": - version "1.22.0" - resolved "https://registry.yarnpkg.com/@ant-design/cssinjs/-/cssinjs-1.22.0.tgz#c2eea2490d2405e55c64f3c1af1e09524d62a228" - integrity sha512-W9XSFeRPR0mAN3OuxfuS/xhENCYKf+8s+QyNNER0FSWoK9OpISTag6CCweg6lq0hASQ/2Vcza0Z8/kGivCP0Ng== + version "1.22.1" + resolved "https://registry.yarnpkg.com/@ant-design/cssinjs/-/cssinjs-1.22.1.tgz#00e943a6387a8080aba8b927df8236df3e07e964" + integrity sha512-SLuXM4wiEE1blOx94iXrkOgseMZHzdr4ngdFu3VVDq6AOWh7rlwqTkMAtJho3EsBF6x/eUGOtK53VZXGQG7+sQ== dependencies: "@babel/runtime" "^7.11.1" "@emotion/hash" "^0.8.0" @@ -56,10 +56,10 @@ resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz#ed2be7fb4d82ac7e1d45a54a5b06d6cecf8be6f6" integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA== -"@ant-design/icons@^5.5.1": - version "5.5.1" - resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-5.5.1.tgz#4ff57b2a0d3bafae3d990c2781fd857ead36c935" - integrity sha512-0UrM02MA2iDIgvLatWrj6YTCYe0F/cwXvVE0E2SqGrL7PZireQwgEKTKBisWpZyal5eXZLvuM98kju6YtYne8w== +"@ant-design/icons@^5.5.2": + version "5.5.2" + resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-5.5.2.tgz#c4567943cc2b7c6dbe9cae68c06ffa35f755dc0d" + integrity sha512-xc53rjVBl9v2BqFxUjZGti/RfdDeA8/6KYglmInM2PNqSXc/WfuGDTifJI/ZsokJK0aeKvOIbXc9y2g8ILAhEA== dependencies: "@ant-design/colors" "^7.0.0" "@ant-design/icons-svg" "^4.4.0" @@ -108,7 +108,7 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2", "@babel/code-frame@^7.8.3": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -118,9 +118,9 @@ picocolors "^1.0.0" "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.9", "@babel/compat-data@^7.26.0": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" - integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" + integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== "@babel/core@^7.14.0", "@babel/core@^7.20.12": version "7.26.0" @@ -152,13 +152,13 @@ eslint-visitor-keys "^2.1.0" semver "^6.3.1" -"@babel/generator@^7.14.0", "@babel/generator@^7.20.14", "@babel/generator@^7.25.9", "@babel/generator@^7.26.0": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" - integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== +"@babel/generator@^7.14.0", "@babel/generator@^7.20.14", "@babel/generator@^7.26.0", "@babel/generator@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019" + integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== dependencies: - "@babel/parser" "^7.26.2" - "@babel/types" "^7.26.0" + "@babel/parser" "^7.26.3" + "@babel/types" "^7.26.3" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" @@ -170,14 +170,6 @@ dependencies: "@babel/types" "^7.25.9" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz#f41752fe772a578e67286e6779a68a5a92de1ee9" - integrity sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" @@ -203,12 +195,12 @@ semver "^6.3.1" "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz#3e8999db94728ad2b2458d7a470e7770b7764e26" - integrity sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz#5169756ecbe1d95f7866b90bb555b022595302a0" + integrity sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong== dependencies: "@babel/helper-annotate-as-pure" "^7.25.9" - regexpu-core "^6.1.1" + regexpu-core "^6.2.0" semver "^6.3.1" "@babel/helper-define-polyfill-provider@^0.6.2", "@babel/helper-define-polyfill-provider@^0.6.3": @@ -277,14 +269,6 @@ "@babel/helper-optimise-call-expression" "^7.25.9" "@babel/traverse" "^7.25.9" -"@babel/helper-simple-access@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz#6d51783299884a2c74618d6ef0f86820ec2e7739" - integrity sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers@^7.20.0", "@babel/helper-skip-transparent-expression-wrappers@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" @@ -335,12 +319,12 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.14.0", "@babel/parser@^7.16.8", "@babel/parser@^7.20.13", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" - integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== +"@babel/parser@^7.14.0", "@babel/parser@^7.16.8", "@babel/parser@^7.20.13", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== dependencies: - "@babel/types" "^7.26.0" + "@babel/types" "^7.26.3" "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": version "7.25.9" @@ -628,11 +612,10 @@ "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-exponentiation-operator@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz#ece47b70d236c1d99c263a1e22b62dc20a4c8b0f" - integrity sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc" + integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.25.9" "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-export-namespace-from@^7.25.9": @@ -704,13 +687,12 @@ "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz#d165c8c569a080baf5467bda88df6425fc060686" - integrity sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz#8f011d44b20d02c3de44d8850d971d8497f981fb" + integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== dependencies: - "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-module-transforms" "^7.26.0" "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-simple-access" "^7.25.9" "@babel/plugin-transform-modules-systemjs@^7.25.9": version "7.25.9" @@ -927,9 +909,9 @@ "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-typescript@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz#69267905c2b33c2ac6d8fe765e9dc2ddc9df3849" - integrity sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz#3d6add9c78735623317387ee26d5ada540eee3fd" + integrity sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA== dependencies: "@babel/helper-annotate-as-pure" "^7.25.9" "@babel/helper-create-class-features-plugin" "^7.25.9" @@ -1053,9 +1035,9 @@ esutils "^2.0.2" "@babel/preset-react@^7.18.6": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.25.9.tgz#5f473035dc2094bcfdbc7392d0766bd42dce173e" - integrity sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.26.3.tgz#7c5e028d623b4683c1f83a0bd4713b9100560caa" + integrity sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw== dependencies: "@babel/helper-plugin-utils" "^7.25.9" "@babel/helper-validator-option" "^7.25.9" @@ -1075,7 +1057,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.25.9" "@babel/plugin-transform-typescript" "^7.25.9" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.7", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== @@ -1092,22 +1074,22 @@ "@babel/types" "^7.25.9" "@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.20.13", "@babel/traverse@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" - integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== + version "7.26.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.4.tgz#ac3a2a84b908dde6d463c3bfa2c5fdc1653574bd" + integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w== dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/generator" "^7.25.9" - "@babel/parser" "^7.25.9" + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.3" + "@babel/parser" "^7.26.3" "@babel/template" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/types" "^7.26.3" debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.16.8", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.4.4": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" - integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== +"@babel/types@^7.0.0", "@babel/types@^7.16.8", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3", "@babel/types@^7.4.4": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== dependencies: "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" @@ -1142,9 +1124,9 @@ tslib "^2.0.0" "@dnd-kit/core@^6.2.0": - version "6.2.0" - resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.2.0.tgz#4c10e2242e007c33ddade090c25b470b5c11abfd" - integrity sha512-KVK/CJmaYGTxTPU6P0+Oy4itgffTUa80B8317sXzfOr1qUzSL29jE7Th11llXiu2haB7B9Glpzo2CDElin+geQ== + version "6.3.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.3.1.tgz#4c36406a62c7baac499726f899935f93f0e6d003" + integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ== dependencies: "@dnd-kit/accessibility" "^3.1.1" "@dnd-kit/utilities" "^3.2.2" @@ -1487,9 +1469,9 @@ wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" "@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== dependencies: "@jridgewell/set-array" "^1.2.1" "@jridgewell/sourcemap-codec" "^1.4.10" @@ -1518,7 +1500,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": +"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -2200,17 +2182,17 @@ classnames "^2.3.2" rc-util "^5.24.4" -"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.5": - version "2.2.5" - resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.5.tgz#5ebe383e563e667b3fa24b6b32afedbab378a92e" - integrity sha512-F1EJ4KjFpGAHAjuKvOyZB/6IZDkVx0bHl0M4fQM5wXcmm7lgTgVSSnR3bXwdmS6jOJGHOqfDxIJW3WUvwMIXhQ== +"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.6.tgz#bfe6602313b3fadd659687746511f813299d5ea4" + integrity sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q== dependencies: "@babel/runtime" "^7.23.2" "@rc-component/portal" "^1.1.0" classnames "^2.3.2" rc-motion "^2.0.0" rc-resize-observer "^1.3.1" - rc-util "^5.38.0" + rc-util "^5.44.0" "@react-aria/focus@^3.17.1": version "3.19.0" @@ -2374,16 +2356,16 @@ postcss-selector-parser "6.0.10" "@tanstack/react-virtual@^3.8.1": - version "3.10.9" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.10.9.tgz#40606b6dd8aba8e977f576d8f7df07f69ca63eea" - integrity sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g== + version "3.11.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322" + integrity sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ== dependencies: - "@tanstack/virtual-core" "3.10.9" + "@tanstack/virtual-core" "3.11.2" -"@tanstack/virtual-core@3.10.9": - version "3.10.9" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz#55710c92b311fdaa8d8c66682a0dbdd684bc77c4" - integrity sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw== +"@tanstack/virtual-core@3.11.2": + version "3.11.2" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" + integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw== "@tokenizer/token@^0.3.0": version "0.3.0" @@ -2639,11 +2621,11 @@ integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== "@types/node@*", "@types/node@>=10.0.0", "@types/node@^22.9.0": - version "22.9.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.2.tgz#51e58f2bf29cc38f529dacbb0eafca890464138e" - integrity sha512-wwuxAVEbsRvDD9x7buvAl7DyQ7Oj+va/d/Veug7higYzp9MF0CINbfWTBgDFMpcVwcdUiYuNmX2KfnvY3N70mw== + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== dependencies: - undici-types "~6.19.8" + undici-types "~6.20.0" "@types/node@^17.0.5": version "17.0.45" @@ -2661,9 +2643,9 @@ integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== "@types/prop-types@*": - version "15.7.13" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" - integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== + version "15.7.14" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" + integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== "@types/reach__router@^1.3.10": version "1.3.15" @@ -2673,23 +2655,28 @@ "@types/react" "*" "@types/react-dom@^18.2.19": - version "18.3.1" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" - integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== - dependencies: - "@types/react" "*" + version "18.3.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.5.tgz#45f9f87398c5dcea085b715c58ddcf1faf65f716" + integrity sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q== -"@types/react-syntax-highlighter@^15.5.10": +"@types/react-syntax-highlighter@^15.5.13": version "15.5.13" resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz#c5baf62a3219b3bf28d39cfea55d0a49a263d1f2" integrity sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18.2.55": - version "18.3.12" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60" - integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw== +"@types/react@*": + version "19.0.2" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.2.tgz#9363e6b3ef898c471cb182dd269decc4afc1b4f6" + integrity sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg== + dependencies: + csstype "^3.0.2" + +"@types/react@^18.2.55": + version "18.3.17" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.17.tgz#d86ca0e081c7a5e979b7db175f9515a41038cea7" + integrity sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -2831,9 +2818,9 @@ eslint-visitor-keys "^3.3.0" "@ungap/structured-clone@^1.0.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + version "1.2.1" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.1.tgz#28fa185f67daaf7b7a1a8c1d445132c5d979f8bd" + integrity sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA== "@vercel/webpack-asset-relocator-loader@1.7.3": version "1.7.3" @@ -2842,7 +2829,7 @@ dependencies: resolve "^1.10.0" -"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.12.1": +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== @@ -2908,7 +2895,7 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== -"@webassemblyjs/wasm-edit@^1.12.1": +"@webassemblyjs/wasm-edit@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== @@ -2943,7 +2930,7 @@ "@webassemblyjs/wasm-gen" "1.14.1" "@webassemblyjs/wasm-parser" "1.14.1" -"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.12.1": +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== @@ -2974,18 +2961,18 @@ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== "@xyflow/react@^12.3.5": - version "12.3.5" - resolved "https://registry.yarnpkg.com/@xyflow/react/-/react-12.3.5.tgz#88ca2efe2ddf1300bc171a2ef797f7cb41386ca4" - integrity sha512-wAYqpicdrVo1rxCu0X3M9s3YIF45Agqfabw0IBryTGqjWvr2NyfciI8gIP4MB+NKpWWN5kxZ9tiZ9u8lwC7iAg== + version "12.3.6" + resolved "https://registry.yarnpkg.com/@xyflow/react/-/react-12.3.6.tgz#b122af1be35fcaaf78da79a3538f67fdd12b694e" + integrity sha512-9GS+cz8hDZahpvTrVCmySAEgKUL8oN4b2q1DluHrKtkqhAMWfH2s7kblhbM4Y4Y4SUnH2lt4drXKZ/4/Lot/2Q== dependencies: - "@xyflow/system" "0.0.46" + "@xyflow/system" "0.0.47" classcat "^5.0.3" zustand "^4.4.0" -"@xyflow/system@0.0.46": - version "0.0.46" - resolved "https://registry.yarnpkg.com/@xyflow/system/-/system-0.0.46.tgz#b0a5915d59c0ea5ca6d24e1eb90c5a0d7eda7864" - integrity sha512-bmFXvboVdiydIFZmDCjrbBCYgB0d5pYdkcZPWbAxGmhMRUZ+kW3CksYgYxWabrw51rwpWitLEadvLrivG0mVfA== +"@xyflow/system@0.0.47": + version "0.0.47" + resolved "https://registry.yarnpkg.com/@xyflow/system/-/system-0.0.47.tgz#c2443e6778ffae9af05b2dc61cb2145be5803405" + integrity sha512-aUXJPIvsCFxGX70ccRG8LPsR+A8ExYXfh/noYNpqn8udKerrLdSHxMG2VsvUrQ1PGex10fOpbJwFU4A+I/Xv8w== dependencies: "@types/d3-drag" "^3.0.7" "@types/d3-selection" "^3.0.10" @@ -2996,9 +2983,9 @@ d3-zoom "^3.0.0" abortcontroller-polyfill@^1.1.9: - version "1.7.6" - resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.6.tgz#7be8d35b5ed7dfa1a51b36f221720b23deb13f36" - integrity sha512-Zypm+LjYdWAzvuypZvDN0smUJrhOurcuBWhhMRBExqVLRvdjp3Z9mASxKyq19K+meZMshwjjy5S0lkm388zE4Q== + version "1.7.8" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.8.tgz#fe8d4370403f02e2aa37e3d2b0b178bae9d83f49" + integrity sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ== accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" @@ -3160,14 +3147,14 @@ ansi-styles@^6.1.0: integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== antd@^5.22.1: - version "5.22.2" - resolved "https://registry.yarnpkg.com/antd/-/antd-5.22.2.tgz#9f5d38c09685c018c368329f1a1107d5417536d6" - integrity sha512-vihhiJbm9VG3d6boUeD1q2MXMax+qBrXhgqCEC+45v8iGUF6m4Ct+lFiCW4oWaN3EABOsbVA6Svy3Rj/QkQFKw== + version "5.22.5" + resolved "https://registry.yarnpkg.com/antd/-/antd-5.22.5.tgz#e959381faca86c984cc853a0ab5cb3f140178336" + integrity sha512-+0UP8w+ULVv2OIzCDVz7j6I0UfH6mMLHSWO6qzpBc+9psOoVQLRbyAE21XnZM/eGrt2MNsEDL5fmlhXL/V8JyQ== dependencies: "@ant-design/colors" "^7.1.0" "@ant-design/cssinjs" "^1.21.1" - "@ant-design/cssinjs-utils" "^1.1.1" - "@ant-design/icons" "^5.5.1" + "@ant-design/cssinjs-utils" "^1.1.3" + "@ant-design/icons" "^5.5.2" "@ant-design/react-slick" "~1.1.2" "@babel/runtime" "^7.25.7" "@ctrl/tinycolor" "^3.6.1" @@ -3175,7 +3162,7 @@ antd@^5.22.1: "@rc-component/mutate-observer" "^1.1.0" "@rc-component/qrcode" "~1.0.0" "@rc-component/tour" "~1.15.1" - "@rc-component/trigger" "^2.2.5" + "@rc-component/trigger" "^2.2.6" classnames "^2.5.1" copy-to-clipboard "^3.3.3" dayjs "^1.11.11" @@ -3184,33 +3171,33 @@ antd@^5.22.1: rc-collapse "~3.9.0" rc-dialog "~9.6.0" rc-drawer "~7.2.0" - rc-dropdown "~4.2.0" - rc-field-form "~2.5.1" + rc-dropdown "~4.2.1" + rc-field-form "~2.7.0" rc-image "~7.11.0" - rc-input "~1.6.3" + rc-input "~1.6.4" rc-input-number "~9.3.0" rc-mentions "~2.17.0" rc-menu "~9.16.0" - rc-motion "^2.9.3" + rc-motion "^2.9.4" rc-notification "~5.6.2" - rc-pagination "~4.3.0" - rc-picker "~4.8.1" + rc-pagination "~5.0.0" + rc-picker "~4.8.3" rc-progress "~4.0.0" rc-rate "~2.13.0" - rc-resize-observer "^1.4.0" + rc-resize-observer "^1.4.1" rc-segmented "~2.5.0" - rc-select "~14.16.3" + rc-select "~14.16.4" rc-slider "~11.1.7" rc-steps "~6.0.1" rc-switch "~4.1.0" - rc-table "~7.48.1" + rc-table "~7.49.0" rc-tabs "~15.4.0" rc-textarea "~1.8.2" rc-tooltip "~6.2.1" rc-tree "~5.10.1" - rc-tree-select "~5.24.4" + rc-tree-select "~5.24.5" rc-upload "~4.8.1" - rc-util "^5.43.0" + rc-util "^5.44.2" scroll-into-view-if-needed "^3.1.0" throttle-debounce "^5.0.2" @@ -3314,24 +3301,24 @@ array.prototype.findlastindex@^1.2.5: es-shim-unscopables "^1.0.2" array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" - integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" array.prototype.flatmap@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" - integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" array.prototype.tosorted@^1.1.4: version "1.1.4" @@ -3344,19 +3331,18 @@ array.prototype.tosorted@^1.1.4: es-errors "^1.3.0" es-shim-unscopables "^1.0.2" -arraybuffer.prototype.slice@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" - integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== dependencies: array-buffer-byte-length "^1.0.1" - call-bind "^1.0.5" + call-bind "^1.0.8" define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.2.1" - get-intrinsic "^1.2.3" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" is-array-buffer "^3.0.4" - is-shared-array-buffer "^1.0.2" arrify@^2.0.1: version "2.0.1" @@ -3433,9 +3419,9 @@ axe-core@^4.10.0: integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w== axios@^1.6.4: - version "1.7.7" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" - integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + version "1.7.9" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -3621,11 +3607,11 @@ bare-path@^2.0.0, bare-path@^2.1.0: bare-os "^2.1.0" bare-stream@^2.0.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.4.0.tgz#2bd49033b69f69a8e2be80c0676c6e27967789d8" - integrity sha512-sd96/aZ8LjF1uJbEHzIo1LrERPKRFPEy1nZ1eOILftBxrVsFDAQkimHIIq87xrHcubzjNeETsD9PwN0wp+vLiQ== + version "2.6.1" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.6.1.tgz#b3b9874fab05b662c9aea2706a12fb0698c46836" + integrity sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g== dependencies: - streamx "^2.20.0" + streamx "^2.21.0" base-x@^3.0.8: version "3.0.10" @@ -3740,13 +3726,13 @@ braces@^3.0.3, braces@~3.0.2: fill-range "^7.1.1" browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.4, browserslist@^4.21.5, browserslist@^4.21.9, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2, browserslist@^4.6.6: - version "4.24.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" - integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + version "4.24.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2" + integrity sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA== dependencies: - caniuse-lite "^1.0.30001669" - electron-to-chromium "^1.5.41" - node-releases "^2.0.18" + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" update-browserslist-db "^1.1.1" bser@2.1.1: @@ -3826,16 +3812,31 @@ cacheable-request@^7.0.2: normalize-url "^6.0.1" responselike "^2.0.0" -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" + integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== dependencies: - es-define-property "^1.0.0" es-errors "^1.3.0" function-bind "^1.1.2" + +call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" get-intrinsic "^1.2.4" - set-function-length "^1.2.1" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" + integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== + dependencies: + call-bind-apply-helpers "^1.0.1" + get-intrinsic "^1.2.6" callsites@^3.0.0: version "3.1.0" @@ -3875,10 +3876,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: - version "1.0.30001683" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001683.tgz#7f026a2d5d319a9cf8915a1451173052caaadc81" - integrity sha512-iqmNnThZ0n70mNwvxpEC2nBJ037ZHZUoBI5Gorh1Mw6IlEAZujEoU1tXA628iZfzm7R9FvFzxbfdgml82a3k8Q== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: + version "1.0.30001689" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz#67ca960dd5f443903e19949aeacc9d28f6e10910" + integrity sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g== capital-case@^1.0.4: version "1.0.4" @@ -3966,16 +3967,31 @@ character-entities-html4@^2.0.0: resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== +character-entities-legacy@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" + integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== + character-entities-legacy@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== +character-entities@^1.0.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" + integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== + character-entities@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== +character-reference-invalid@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" + integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== + character-reference-invalid@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" @@ -4144,6 +4160,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^1.0.0: + version "1.0.8" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" + integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== + comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" @@ -4654,10 +4675,10 @@ debug@2, debug@2.6.9, debug@^2.6.0: dependencies: ms "2.0.0" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== dependencies: ms "^2.1.3" @@ -4668,6 +4689,13 @@ debug@^3.0.1, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" +debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -4726,7 +4754,7 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: +define-properties@^1.1.3, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -4930,6 +4958,15 @@ dotenv@^8.6.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== +dunder-proto@^1.0.0, dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -4945,10 +4982,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.5.41: - version "1.5.64" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.64.tgz#ac8c4c89075d35a1514b620f47dfe48a71ec3697" - integrity sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ== +electron-to-chromium@^1.5.73: + version "1.5.74" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz#cb886b504a6467e4c00bea3317edb38393c53413" + integrity sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw== emoji-regex@^8.0.0: version "8.0.0" @@ -5059,66 +5096,66 @@ error-stack-parser@^2.0.6, error-stack-parser@^2.1.4: dependencies: stackframe "^1.3.4" -es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: - version "1.23.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.5.tgz#f4599a4946d57ed467515ed10e4f157289cd52fb" - integrity sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ== +es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6: + version "1.23.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.6.tgz#55f0e1ce7128995cc04ace0a57d7dca348345108" + integrity sha512-Ifco6n3yj2tMZDWNLyloZrytt9lqqlwvS83P3HtaETR0NUOYnIULGGHpktqYGObGy+8wc1okO25p8TjemhImvA== dependencies: array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" + arraybuffer.prototype.slice "^1.0.4" available-typed-arrays "^1.0.7" - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" data-view-buffer "^1.0.1" data-view-byte-length "^1.0.1" data-view-byte-offset "^1.0.0" - es-define-property "^1.0.0" + es-define-property "^1.0.1" es-errors "^1.3.0" es-object-atoms "^1.0.0" es-set-tostringtag "^2.0.3" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.7" + get-intrinsic "^1.2.6" get-symbol-description "^1.0.2" globalthis "^1.0.4" - gopd "^1.0.1" + gopd "^1.2.0" has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" + has-proto "^1.2.0" + has-symbols "^1.1.0" hasown "^2.0.2" - internal-slot "^1.0.7" + internal-slot "^1.1.0" is-array-buffer "^3.0.4" is-callable "^1.2.7" - is-data-view "^1.0.1" + is-data-view "^1.0.2" is-negative-zero "^2.0.3" - is-regex "^1.1.4" + is-regex "^1.2.1" is-shared-array-buffer "^1.0.3" - is-string "^1.0.7" + is-string "^1.1.1" is-typed-array "^1.1.13" - is-weakref "^1.0.2" + is-weakref "^1.1.0" + math-intrinsics "^1.0.0" object-inspect "^1.13.3" object-keys "^1.1.1" object.assign "^4.1.5" regexp.prototype.flags "^1.5.3" - safe-array-concat "^1.1.2" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.9" - string.prototype.trimend "^1.0.8" + safe-array-concat "^1.1.3" + safe-regex-test "^1.1.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" string.prototype.trimstart "^1.0.8" typed-array-buffer "^1.0.2" typed-array-byte-length "^1.0.1" - typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.6" + typed-array-byte-offset "^1.0.3" + typed-array-length "^1.0.7" unbox-primitive "^1.0.2" - which-typed-array "^1.1.15" + which-typed-array "^1.1.16" -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== -es-errors@^1.2.1, es-errors@^1.3.0: +es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== @@ -5165,21 +5202,21 @@ es-set-tostringtag@^2.0.3: has-tostringtag "^1.0.2" hasown "^2.0.1" -es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: +es-shim-unscopables@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== dependencies: hasown "^2.0.0" -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2: version "0.10.64" @@ -5613,9 +5650,9 @@ express-http-proxy@^1.6.3: raw-body "^2.3.0" express@^4.18.2: - version "4.21.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" - integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== dependencies: accepts "~1.3.8" array-flatten "1.1.1" @@ -5636,7 +5673,7 @@ express@^4.18.2: methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.10" + path-to-regexp "0.1.12" proxy-addr "~2.0.7" qs "6.13.0" range-parser "~1.2.1" @@ -5725,6 +5762,13 @@ fastq@^1.15.0, fastq@^1.16.0, fastq@^1.6.0: dependencies: reusify "^1.0.4" +fault@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" + integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== + dependencies: + format "^0.2.0" + fb-watchman@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" @@ -5929,6 +5973,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5993,15 +6042,16 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" - integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== +function.prototype.name@^1.1.6, function.prototype.name@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.7.tgz#9df48ea5f746bf577d7e15b5da89df8952a98e7b" + integrity sha512-2g4x+HqTJKM9zcJqBSpjoRmdcPFtJM60J3xJisTQSXBWka5XqyBN/2tNUgma1mztTXyDuUsEtYe5qcs7xYzYQA== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.8" + define-properties "^1.2.1" functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" functional-red-black-tree@^1.0.1: version "1.0.1" @@ -6515,16 +6565,21 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.6.tgz#43dd3dd0e7b49b82b2dfcad10dc824bf7fc265d5" + integrity sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA== dependencies: + call-bind-apply-helpers "^1.0.1" + dunder-proto "^1.0.0" + es-define-property "^1.0.1" es-errors "^1.3.0" + es-object-atoms "^1.0.0" function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.0.0" get-port@^3.2.0: version "3.2.0" @@ -6549,13 +6604,13 @@ get-stream@^8.0.1: integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== get-symbol-description@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" - integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== dependencies: - call-bind "^1.0.5" + call-bound "^1.0.3" es-errors "^1.3.0" - get-intrinsic "^1.2.4" + get-intrinsic "^1.2.6" github-from-package@0.0.0: version "0.0.0" @@ -6638,7 +6693,7 @@ globals@^13.2.0, globals@^13.6.0, globals@^13.9.0: dependencies: type-fest "^0.20.2" -globalthis@^1.0.3, globalthis@^1.0.4: +globalthis@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== @@ -6658,12 +6713,10 @@ globby@^11.0.3, globby@^11.0.4, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== got@^11.8.6: version "11.8.6" @@ -6739,9 +6792,9 @@ graphql-type-json@0.3.2: integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg== graphql@^16.7.1: - version "16.9.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.9.0.tgz#1c310e63f16a49ce1fbb230bd0a000e99f6f115f" - integrity sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw== + version "16.10.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c" + integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ== gray-matter@^4.0.3: version "4.0.3" @@ -6760,10 +6813,10 @@ gzip-size@^6.0.0: dependencies: duplexer "^0.1.2" -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== +has-bigints@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== has-flag@^3.0.0: version "3.0.0" @@ -6782,15 +6835,17 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1, has-proto@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== +has-proto@^1.0.3, has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" @@ -6840,6 +6895,11 @@ hast-util-is-element@^2.0.0: "@types/hast" "^2.0.0" "@types/unist" "^2.0.0" +hast-util-parse-selector@^2.0.0: + version "2.2.5" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" + integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== + hast-util-select@^5.0.0: version "5.0.5" resolved "https://registry.yarnpkg.com/hast-util-select/-/hast-util-select-5.0.5.tgz#be9ccb71d2278681ca024727f12abd4f93b3e9bc" @@ -6940,6 +7000,17 @@ hast-util-whitespace@^3.0.0: dependencies: "@types/hast" "^3.0.0" +hastscript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" + integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w== + dependencies: + "@types/hast" "^2.0.0" + comma-separated-tokens "^1.0.0" + hast-util-parse-selector "^2.0.0" + property-information "^5.0.0" + space-separated-tokens "^1.0.0" + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -6953,6 +7024,16 @@ header-case@^2.0.4: capital-case "^1.0.4" tslib "^2.0.3" +highlight.js@^10.4.1, highlight.js@~10.7.0: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== + +highlightjs-vue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz#fdfe97fbea6354e70ee44e3a955875e114db086d" + integrity sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA== + hosted-git-info@^3.0.8: version "3.0.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d" @@ -7129,14 +7210,14 @@ install@^0.13.0: resolved "https://registry.yarnpkg.com/install/-/install-0.13.0.tgz#6af6e9da9dd0987de2ab420f78e60d9c17260776" integrity sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA== -internal-slot@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" - integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== +internal-slot@^1.0.7, internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== dependencies: es-errors "^1.3.0" - hasown "^2.0.0" - side-channel "^1.0.4" + hasown "^2.0.2" + side-channel "^1.1.0" invariant@^2.2.4: version "2.2.4" @@ -7163,11 +7244,24 @@ is-absolute@^1.0.0: is-relative "^1.0.0" is-windows "^1.0.1" +is-alphabetical@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" + integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== + is-alphabetical@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== +is-alphanumerical@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" + integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-alphanumerical@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" @@ -7177,12 +7271,13 @@ is-alphanumerical@^2.0.0: is-decimal "^2.0.0" is-array-buffer@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" - integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" is-arrayish@^0.2.1: version "0.2.1" @@ -7201,12 +7296,12 @@ is-async-function@^2.0.0: dependencies: has-tostringtag "^1.0.0" -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== dependencies: - has-bigints "^1.0.1" + has-bigints "^1.0.2" is-binary-path@~2.1.0: version "2.1.0" @@ -7215,20 +7310,20 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== +is-boolean-object@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.1.tgz#c20d0c654be05da4fbc23c562635c019e93daf89" + integrity sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bound "^1.0.2" + has-tostringtag "^1.0.2" is-buffer@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: +is-callable@^1.1.3, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -7240,26 +7335,34 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.13.0, is-core-module@^2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== +is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.0.tgz#6c01ffdd5e33c49c1d2abfa93334a85cb56bd81c" + integrity sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g== dependencies: hasown "^2.0.2" -is-data-view@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" - integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" is-typed-array "^1.1.13" -is-date-object@^1.0.1, is-date-object@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-decimal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" + integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== is-decimal@^2.0.0: version "2.0.1" @@ -7291,12 +7394,12 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-finalizationregistry@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" - integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.3" is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -7324,6 +7427,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-hexadecimal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" + integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== + is-hexadecimal@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" @@ -7360,12 +7468,13 @@ is-negative-zero@^2.0.3: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.3" + has-tostringtag "^1.0.2" is-number@^7.0.0: version "7.0.0" @@ -7401,13 +7510,15 @@ is-reference@^3.0.0: dependencies: "@types/estree" "^1.0.6" -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" is-relative-url@^3.0.0: version "3.0.0" @@ -7433,12 +7544,12 @@ is-set@^2.0.3: resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== -is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" - integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== +is-shared-array-buffer@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" is-stream@^2.0.0: version "2.0.1" @@ -7450,26 +7561,29 @@ is-stream@^3.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== +is-string@^1.0.7, is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.3" + has-tostringtag "^1.0.2" -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== dependencies: - has-symbols "^1.0.2" + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" -is-typed-array@^1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== +is-typed-array@^1.1.13, is-typed-array@^1.1.14: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== dependencies: - which-typed-array "^1.1.14" + which-typed-array "^1.1.16" is-typedarray@^1.0.0: version "1.0.0" @@ -7509,20 +7623,20 @@ is-weakmap@^2.0.2: resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== +is-weakref@^1.0.2, is-weakref@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.0.tgz#47e3472ae95a63fa9cf25660bcf0c181c39770ef" + integrity sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.2" is-weakset@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" - integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" is-windows@^1.0.1: version "1.0.2" @@ -7571,15 +7685,16 @@ isobject@^3.0.1: integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== iterator.prototype@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.3.tgz#016c2abe0be3bbdb8319852884f60908ac62bf9c" - integrity sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ== + version "1.1.4" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.4.tgz#4ae6cf98b97fdc717b7e159d79dc25f8fc9482f1" + integrity sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA== dependencies: - define-properties "^1.2.1" - get-intrinsic "^1.2.1" - has-symbols "^1.0.3" - reflect.getprototypeof "^1.0.4" - set-function-name "^2.0.1" + define-data-property "^1.1.4" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + reflect.getprototypeof "^1.0.8" + set-function-name "^2.0.2" jackspeak@^3.1.2: version "3.4.3" @@ -7614,9 +7729,9 @@ jest-worker@^27.4.5, jest-worker@^27.5.1: supports-color "^8.0.0" jiti@^1.20.0, jiti@^1.21.6: - version "1.21.6" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" - integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== + version "1.21.7" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9" + integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== joi@^17.11.0, joi@^17.9.2: version "17.13.3" @@ -7649,7 +7764,12 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsesc@^3.0.2, jsesc@~3.0.2: +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +jsesc@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== @@ -7776,15 +7896,15 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lilconfig@^2.0.3, lilconfig@^2.1.0: +lilconfig@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== -lilconfig@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" - integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== +lilconfig@^3.0.0, lilconfig@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== lines-and-columns@^1.1.6: version "1.2.4" @@ -7989,6 +8109,14 @@ lowercase-keys@^3.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== +lowlight@^1.17.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" + integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== + dependencies: + fault "^1.0.0" + highlight.js "~10.7.0" + lru-cache@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.0.tgz#b5cbf01556c16966febe54ceec0fb4dc90df6c28" @@ -8045,6 +8173,11 @@ markdown-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-1.1.1.tgz#fea03b539faeaee9b4ef02a3769b455b189f7fc3" integrity sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q== +math-intrinsics@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.0.0.tgz#4e04bf87c85aa51e90d078dac2252b4eb5260817" + integrity sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA== + mdast-util-definitions@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" @@ -9142,10 +9275,10 @@ node-object-hash@^2.3.10: resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-2.3.10.tgz#4b0c1a3a8239e955f0db71f8e00b38b5c0b33992" integrity sha512-jY5dPJzw6NHd/KPSfPKJ+IHoFS81/tJ43r34ZeNMXGzCOM8jwQDCD12HYayKIB6MuznrnqIYy2e891NA2g0ibA== -node-releases@^2.0.18: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== normalize-path@^2.1.1: version "2.1.1" @@ -9223,7 +9356,7 @@ object-hash@^3.0.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object-inspect@^1.13.1, object-inspect@^1.13.3: +object-inspect@^1.13.3: version "1.13.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== @@ -9234,13 +9367,15 @@ object-keys@^1.1.1: integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object.assign@^4.1.0, object.assign@^4.1.4, object.assign@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" - has-symbols "^1.0.3" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" object-keys "^1.1.1" object.entries@^1.1.8: @@ -9447,13 +9582,24 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" + integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-entities@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" - integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== + version "4.0.2" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== dependencies: "@types/unist" "^2.0.0" - character-entities "^2.0.0" character-entities-legacy "^3.0.0" character-reference-invalid "^2.0.0" decode-named-character-reference "^1.0.0" @@ -9564,6 +9710,11 @@ path-to-regexp@0.1.10: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -9781,9 +9932,9 @@ postcss-modules-extract-imports@^3.0.0: integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== postcss-modules-local-by-default@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.1.0.tgz#b0db6bc81ffc7bdc52eb0f84d6ca0bedf0e36d21" - integrity sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q== + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368" + integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^7.0.0" @@ -9985,6 +10136,16 @@ pretty-error@^2.1.2: lodash "^4.17.20" renderkid "^2.0.4" +prismjs@^1.27.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + +prismjs@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" + integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== + probe-image-size@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-7.2.3.tgz#d49c64be540ec8edea538f6f585f65a9b3ab4309" @@ -10037,6 +10198,13 @@ proper-lockfile@^4.1.2: retry "^0.12.0" signal-exit "^3.0.2" +property-information@^5.0.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" + integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== + dependencies: + xtend "^4.0.0" + property-information@^6.0.0: version "6.5.0" resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" @@ -10192,20 +10360,20 @@ rc-drawer@~7.2.0: rc-motion "^2.6.1" rc-util "^5.38.1" -rc-dropdown@~4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/rc-dropdown/-/rc-dropdown-4.2.0.tgz#c6052fcfe9c701487b141e411cdc277dc7c6f061" - integrity sha512-odM8Ove+gSh0zU27DUj5cG1gNKg7mLWBYzB5E4nNLrLwBmYEgYP43vHKDGOVZcJSVElQBI0+jTQgjnq0NfLjng== +rc-dropdown@~4.2.0, rc-dropdown@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/rc-dropdown/-/rc-dropdown-4.2.1.tgz#44729eb2a4272e0353d31ac060da21e606accb1c" + integrity sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA== dependencies: "@babel/runtime" "^7.18.3" "@rc-component/trigger" "^2.0.0" classnames "^2.2.6" - rc-util "^5.17.0" + rc-util "^5.44.1" -rc-field-form@~2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/rc-field-form/-/rc-field-form-2.5.1.tgz#30f3c529f86aec6af27589052df9c66cec94ceb4" - integrity sha512-33hunXwynQJyeae7LS3hMGTXNeRBjiPyPYgB0824EbmLHiXC1EBGyUwRh6xjLRy9c+en5WARYN0gJz5+JAqwig== +rc-field-form@~2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/rc-field-form/-/rc-field-form-2.7.0.tgz#22413e793f35bfc1f35b0ec462774d7277f5a399" + integrity sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA== dependencies: "@babel/runtime" "^7.18.0" "@rc-component/async-validator" "^5.0.3" @@ -10234,10 +10402,10 @@ rc-input-number@~9.3.0: rc-input "~1.6.0" rc-util "^5.40.1" -rc-input@~1.6.0, rc-input@~1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-1.6.3.tgz#f1708fc3d5e68f95cb20faeb3eed1df8543cd444" - integrity sha512-wI4NzuqBS8vvKr8cljsvnTUqItMfG1QbJoxovCgL+DX4eVUcHIjVwharwevIxyy7H/jbLryh+K7ysnJr23aWIA== +rc-input@~1.6.0, rc-input@~1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-1.6.4.tgz#08d91460f6b75b3fa5294154e89775784c233129" + integrity sha512-lBZhfRD4NSAUW0zOKLUeI6GJuXkxeZYi0hr8VcJgJpyTNOvHw1ysrKWAHcEOAAHj7guxgmWYSi6xWrEdfrSAsA== dependencies: "@babel/runtime" "^7.11.1" classnames "^2.2.1" @@ -10268,14 +10436,14 @@ rc-menu@~9.16.0: rc-overflow "^1.3.1" rc-util "^5.27.0" -rc-motion@^2.0.0, rc-motion@^2.0.1, rc-motion@^2.3.0, rc-motion@^2.3.4, rc-motion@^2.4.3, rc-motion@^2.4.4, rc-motion@^2.6.1, rc-motion@^2.6.2, rc-motion@^2.9.0, rc-motion@^2.9.3: - version "2.9.3" - resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.9.3.tgz#b1bdaf816f1ccb3e4b3b0c531c3037a59286379e" - integrity sha512-rkW47ABVkic7WEB0EKJqzySpvDqwl60/tdkY7hWP7dYnh5pm0SzJpo54oW3TDUGXV5wfxXFmMkxrzRRbotQ0+w== +rc-motion@^2.0.0, rc-motion@^2.0.1, rc-motion@^2.3.0, rc-motion@^2.3.4, rc-motion@^2.4.3, rc-motion@^2.4.4, rc-motion@^2.6.1, rc-motion@^2.6.2, rc-motion@^2.9.0, rc-motion@^2.9.4: + version "2.9.4" + resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.9.4.tgz#55fcf465a582a7e9f40f2687c2ebf2572e31bc2b" + integrity sha512-TAPUUufDqhPO669qJobI0d9U0XZ/VPNQyZTivUxxzU1EyuPe3PtHSx7Kb902KuzQgu7sS18z8GguaxZEALV/ww== dependencies: "@babel/runtime" "^7.11.1" classnames "^2.2.1" - rc-util "^5.43.0" + rc-util "^5.44.0" rc-notification@~5.6.2: version "5.6.2" @@ -10297,19 +10465,19 @@ rc-overflow@^1.3.1, rc-overflow@^1.3.2: rc-resize-observer "^1.0.0" rc-util "^5.37.0" -rc-pagination@~4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-4.3.0.tgz#c6022f820aa3a45fd734ae33a2915d39597dce1d" - integrity sha512-UubEWA0ShnroQ1tDa291Fzw6kj0iOeF26IsUObxYTpimgj4/qPCWVFl18RLZE+0Up1IZg0IK4pMn6nB3mjvB7g== +rc-pagination@~5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-5.0.0.tgz#7633e1f0ff372ad78c03e86bcef78b660374d196" + integrity sha512-QjrPvbAQwps93iluvFM62AEYglGYhWW2q/nliQqmvkTi4PXP4HHoh00iC1Sa5LLVmtWQHmG73fBi2x6H6vFHRg== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.3.2" rc-util "^5.38.0" -rc-picker@~4.8.1: - version "4.8.1" - resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-4.8.1.tgz#105cfae323bf1db5e9f9e6fdc773ff3250e837de" - integrity sha512-lj9hXXMSkbjFUIhfQh8XH698ybxnoBOfq7pdM1FvfSyDwdFhdQa7dvsIYwo6Uz7Zp1wVkfw5rOJO3MpdWzoHsg== +rc-picker@~4.8.3: + version "4.8.3" + resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-4.8.3.tgz#06cffd5a2201fc8d274e12f7ee32ea8ba6f3f60f" + integrity sha512-hJ45qoEs4mfxXPAJdp1n3sKwADul874Cd0/HwnsEOE60H+tgiJUGgbOD62As3EG/rFVNS5AWRfBCDJJfmRqOVQ== dependencies: "@babel/runtime" "^7.24.7" "@rc-component/trigger" "^2.0.0" @@ -10336,14 +10504,14 @@ rc-rate@~2.13.0: classnames "^2.2.5" rc-util "^5.0.1" -rc-resize-observer@^1.0.0, rc-resize-observer@^1.1.0, rc-resize-observer@^1.3.1, rc-resize-observer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.4.0.tgz#7bba61e6b3c604834980647cce6451914750d0cc" - integrity sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q== +rc-resize-observer@^1.0.0, rc-resize-observer@^1.1.0, rc-resize-observer@^1.3.1, rc-resize-observer@^1.4.0, rc-resize-observer@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.4.1.tgz#1f6b75ecf27ee900f8a2dbd122c0b222d31aeb49" + integrity sha512-JbAeFDsaaZRPwaTlXnCqgeO9c6E7qoaE/hxsub08cdnnPn6767c/j9+r/TifUdfvwXtdcfHygKbZ7ecM/PXo/Q== dependencies: "@babel/runtime" "^7.20.7" classnames "^2.2.1" - rc-util "^5.38.0" + rc-util "^5.44.1" resize-observer-polyfill "^1.5.1" rc-segmented@~2.5.0: @@ -10356,10 +10524,10 @@ rc-segmented@~2.5.0: rc-motion "^2.4.4" rc-util "^5.17.0" -rc-select@~14.16.2, rc-select@~14.16.3: - version "14.16.3" - resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.16.3.tgz#cd5395ef724d693a8f782ddd7bcc6253c31c65a9" - integrity sha512-51+j6s3fJJJXB7E+B6W1hM4Tjzv1B/Decooz9ilgegDBt3ZAth1b/xMwYCTrT5BbG2e53XACQsyDib2+3Ro1fg== +rc-select@~14.16.2, rc-select@~14.16.4: + version "14.16.4" + resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.16.4.tgz#a98840c4cfb96e263c750e59334ea0a2862e04fc" + integrity sha512-jP6qf7+vjnxGvPpfPWbGYfFlSl3h8L2XcD4O7g2GYXmEeBC0mw+nPD7i++OOE8v3YGqP8xtYjRKAWCMLfjgxlw== dependencies: "@babel/runtime" "^7.10.1" "@rc-component/trigger" "^2.1.1" @@ -10396,10 +10564,10 @@ rc-switch@~4.1.0: classnames "^2.2.1" rc-util "^5.30.0" -rc-table@~7.48.1: - version "7.48.1" - resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.48.1.tgz#16ee3d82fa17284628d7883933b5d481238ea013" - integrity sha512-Z4mDKjWg+xz/Ezdw6ivWcbqRpaJ0QfCORRoRrlrw65KSGZLK8OcTdacH22/fyGb8L4It/0/9qcMm8VrVAk/WBw== +rc-table@~7.49.0: + version "7.49.0" + resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.49.0.tgz#f5a4880d9527d2c9e42f5f721b5423e7a1ca475b" + integrity sha512-/FoPLX94muAQOxVpi1jhnpKjOIqUbT81eELQPAzSXOke4ky4oCWYUXOcVpL31ZCO90xScwVSXRd7coqtgtB1Ng== dependencies: "@babel/runtime" "^7.10.1" "@rc-component/context" "^1.4.0" @@ -10441,7 +10609,7 @@ rc-tooltip@~6.2.1: "@rc-component/trigger" "^2.0.0" classnames "^2.3.1" -rc-tree-select@~5.24.4: +rc-tree-select@~5.24.5: version "5.24.5" resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-5.24.5.tgz#a1bf85c7d5e4979880cfb0748bb6bab937ed3483" integrity sha512-PnyR8LZJWaiEFw0SHRqo4MNQWyyZsyMs8eNmo68uXZWjxc7QqeWcjPPoONN0rc90c3HZqGF9z+Roz+GLzY5GXA== @@ -10472,18 +10640,18 @@ rc-upload@~4.8.1: classnames "^2.2.5" rc-util "^5.2.0" -rc-util@^5.0.1, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.2.0, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.24.4, rc-util@^5.25.2, rc-util@^5.27.0, rc-util@^5.30.0, rc-util@^5.31.1, rc-util@^5.32.2, rc-util@^5.34.1, rc-util@^5.35.0, rc-util@^5.36.0, rc-util@^5.37.0, rc-util@^5.38.0, rc-util@^5.38.1, rc-util@^5.40.1, rc-util@^5.41.0, rc-util@^5.43.0: - version "5.43.0" - resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.43.0.tgz#bba91fbef2c3e30ea2c236893746f3e9b05ecc4c" - integrity sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw== +rc-util@^5.0.1, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.2.0, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.24.4, rc-util@^5.25.2, rc-util@^5.27.0, rc-util@^5.30.0, rc-util@^5.31.1, rc-util@^5.32.2, rc-util@^5.34.1, rc-util@^5.35.0, rc-util@^5.36.0, rc-util@^5.37.0, rc-util@^5.38.0, rc-util@^5.38.1, rc-util@^5.40.1, rc-util@^5.41.0, rc-util@^5.43.0, rc-util@^5.44.0, rc-util@^5.44.1, rc-util@^5.44.2: + version "5.44.2" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.44.2.tgz#6bc5db0e96ebdb515eb5977a7371887e5413a6f8" + integrity sha512-uGSk3hpPBLa3/0QAcKhCjgl4SFnhQCJDLvvpoLdbR6KgDuXrujG+dQaUeUvBJr2ZWak1O/9n+cYbJiWmmk95EQ== dependencies: "@babel/runtime" "^7.18.3" react-is "^18.2.0" rc-virtual-list@^3.14.2, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2: - version "3.15.0" - resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.15.0.tgz#45c5b1ef1363287214e0a3c17af29ee0c3764764" - integrity sha512-dF2YQztqrU3ijAeWOqscTshCEr7vpimzSqAVjO1AyAmaqcHulaXpnGR0ptK5PXfxTUy48VkJOiglMIxlkYGs0w== + version "3.16.1" + resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.16.1.tgz#073d75cc0295497cdd9a35d6f5d1b71b4f35233e" + integrity sha512-algM5UsB7vrlPNr9lsZEH8s9KHkP8XbT/Y0qylyPkiM8mIOlSJLjBNADcmbYPEQCm4zW82mZRJuVHNzqqN0EAQ== dependencies: "@babel/runtime" "^7.20.0" classnames "^2.2.6" @@ -10583,6 +10751,18 @@ react-server-dom-webpack@0.0.0-experimental-c8b778b7f-20220825: loose-envify "^1.1.0" neo-async "^2.6.1" +react-syntax-highlighter@^15.6.1: + version "15.6.1" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz#fa567cb0a9f96be7bbccf2c13a3c4b5657d9543e" + integrity sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg== + dependencies: + "@babel/runtime" "^7.3.1" + highlight.js "^10.4.1" + highlightjs-vue "^1.0.0" + lowlight "^1.17.0" + prismjs "^1.27.0" + refractor "^3.6.0" + react@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -10659,18 +10839,28 @@ redux@4.2.1: dependencies: "@babel/runtime" "^7.9.2" -reflect.getprototypeof@^1.0.4, reflect.getprototypeof@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" - integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.8: + version "1.0.9" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz#c905f3386008de95a62315f3ea8630404be19e2f" + integrity sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" define-properties "^1.2.1" - es-abstract "^1.23.1" + dunder-proto "^1.0.1" + es-abstract "^1.23.6" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - globalthis "^1.0.3" - which-builtin-type "^1.1.3" + get-intrinsic "^1.2.6" + gopd "^1.2.0" + which-builtin-type "^1.2.1" + +refractor@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" + integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== + dependencies: + hastscript "^6.0.0" + parse-entities "^2.0.0" + prismjs "~1.27.0" regenerate-unicode-properties@^10.2.0: version "10.2.0" @@ -10716,7 +10906,7 @@ regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -regexpu-core@^6.1.1: +regexpu-core@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== @@ -10729,9 +10919,9 @@ regexpu-core@^6.1.1: unicode-match-property-value-ecmascript "^2.1.0" registry-auth-token@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" - integrity sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ== + version "5.0.3" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.3.tgz#417d758c8164569de8cf5cabff16cc937902dcc6" + integrity sha512-1bpc9IyC+e+CNFRaWyn77tk4xGG4PPUyfakSmA6F6cvUDjrm58dfyJ3II+9yb10EDkHoy1LaPSmHaWLOH3m6HA== dependencies: "@pnpm/npm-conf" "^2.1.0" @@ -10898,11 +11088,11 @@ resolve-from@^5.0.0: integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.4, resolve@^1.22.8: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + version "1.22.9" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.9.tgz#6da76e4cdc57181fa4471231400e8851d0a924f3" + integrity sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A== dependencies: - is-core-module "^2.13.0" + is-core-module "^2.16.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -10987,14 +11177,15 @@ sade@^1.7.3: dependencies: mri "^1.1.0" -safe-array-concat@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" - integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== +safe-array-concat@^1.1.2, safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" - has-symbols "^1.0.3" + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" isarray "^2.0.5" safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: @@ -11007,14 +11198,14 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-regex-test@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" - integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== +safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== dependencies: - call-bind "^1.0.6" + call-bound "^1.0.2" es-errors "^1.3.0" - is-regex "^1.1.4" + is-regex "^1.2.1" "safer-buffer@>= 2.1.2 < 3": version "2.1.2" @@ -11060,10 +11251,10 @@ schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: ajv "^6.12.5" ajv-keywords "^3.5.2" -schema-utils@^4.0.0, schema-utils@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" - integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== +schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0" + integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== dependencies: "@types/json-schema" "^7.0.9" ajv "^8.9.0" @@ -11135,7 +11326,7 @@ serialize-javascript@^5.0.1: dependencies: randombytes "^2.1.0" -serialize-javascript@^6.0.1: +serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== @@ -11157,7 +11348,7 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -set-function-length@^1.2.1: +set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -11169,7 +11360,7 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" -set-function-name@^2.0.1, set-function-name@^2.0.2: +set-function-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== @@ -11228,19 +11419,49 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.7.3: - version "1.8.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" - integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + version "1.8.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a" + integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== -side-channel@^1.0.4, side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: - call-bind "^1.0.7" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6, side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" @@ -11387,6 +11608,11 @@ source-map@^0.7.0, source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +space-separated-tokens@^1.0.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" + integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== + space-separated-tokens@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" @@ -11446,10 +11672,10 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -streamx@^2.15.0, streamx@^2.20.0: - version "2.20.2" - resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.2.tgz#6a8911959d6f307c19781a1d19ecd94b5f042d78" - integrity sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA== +streamx@^2.15.0, streamx@^2.21.0: + version "2.21.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.21.1.tgz#f02979d8395b6b637d08a589fb514498bed55845" + integrity sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw== dependencies: fast-fifo "^1.3.2" queue-tick "^1.0.1" @@ -11545,22 +11771,26 @@ string.prototype.repeat@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" -string.prototype.trim@^1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" - integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" define-properties "^1.2.1" - es-abstract "^1.23.0" + es-abstract "^1.23.5" es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" -string.prototype.trimend@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" - integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== +string.prototype.trimend@^1.0.8, string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.2" define-properties "^1.2.1" es-object-atoms "^1.0.0" @@ -11785,9 +12015,9 @@ tabbable@^6.0.0: integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== table@^6.0.9: - version "6.8.2" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58" - integrity sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA== + version "6.9.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.9.0.tgz#50040afa6264141c7566b3b81d4d82c47a8668f5" + integrity sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A== dependencies: ajv "^8.0.1" lodash.truncate "^4.4.2" @@ -11796,9 +12026,9 @@ table@^6.0.9: strip-ansi "^6.0.1" tailwindcss@^3.4.14: - version "3.4.15" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.15.tgz#04808bf4bf1424b105047d19e7d4bfab368044a9" - integrity sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw== + version "3.4.17" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63" + integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og== dependencies: "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" @@ -11809,7 +12039,7 @@ tailwindcss@^3.4.14: glob-parent "^6.0.2" is-glob "^4.0.3" jiti "^1.21.6" - lilconfig "^2.1.0" + lilconfig "^3.1.3" micromatch "^4.0.8" normalize-path "^3.0.0" object-hash "^3.0.0" @@ -11875,20 +12105,20 @@ tar-stream@^3.1.5: streamx "^2.15.0" terser-webpack-plugin@^5.3.10, terser-webpack-plugin@^5.3.9: - version "5.3.10" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" - integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + version "5.3.11" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz#93c21f44ca86634257cac176f884f942b7ba3832" + integrity sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ== dependencies: - "@jridgewell/trace-mapping" "^0.3.20" + "@jridgewell/trace-mapping" "^0.3.25" jest-worker "^27.4.5" - schema-utils "^3.1.1" - serialize-javascript "^6.0.1" - terser "^5.26.0" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" -terser@^5.2.0, terser@^5.26.0: - version "5.36.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e" - integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w== +terser@^5.2.0, terser@^5.31.1: + version "5.37.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.37.0.tgz#38aa66d1cfc43d0638fab54e43ff8a4f72a21ba3" + integrity sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -11896,9 +12126,11 @@ terser@^5.2.0, terser@^5.26.0: source-map-support "~0.5.20" text-decoder@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.1.tgz#e173f5121d97bfa3ff8723429ad5ba92e1ead67e" - integrity sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ== + version "1.2.3" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65" + integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA== + dependencies: + b4a "^1.6.4" text-table@^0.2.0: version "0.2.0" @@ -12093,26 +12325,26 @@ type@^2.7.2: integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== typed-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" - integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" es-errors "^1.3.0" - is-typed-array "^1.1.13" + is-typed-array "^1.1.14" typed-array-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" - integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" -typed-array-byte-offset@^1.0.2: +typed-array-byte-offset@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.3.tgz#3fa9f22567700cc86aaf86a1e7176f74b59600f2" integrity sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw== @@ -12125,17 +12357,17 @@ typed-array-byte-offset@^1.0.2: is-typed-array "^1.1.13" reflect.getprototypeof "^1.0.6" -typed-array-length@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" - integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== dependencies: call-bind "^1.0.7" for-each "^0.3.3" gopd "^1.0.1" - has-proto "^1.0.3" is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" typedarray-to-buffer@^3.1.5: version "3.1.5" @@ -12160,24 +12392,24 @@ ua-parser-js@^1.0.35: integrity sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw== unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.3" has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== -undici-types@~6.19.8: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" @@ -12587,15 +12819,15 @@ webpack-virtual-modules@^0.5.0: integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== webpack@^5.88.1: - version "5.96.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.96.1.tgz#3676d1626d8312b6b10d0c18cc049fba7ac01f0c" - integrity sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA== + version "5.97.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.97.1.tgz#972a8320a438b56ff0f1d94ade9e82eac155fa58" + integrity sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg== dependencies: "@types/eslint-scope" "^3.7.7" "@types/estree" "^1.0.6" - "@webassemblyjs/ast" "^1.12.1" - "@webassemblyjs/wasm-edit" "^1.12.1" - "@webassemblyjs/wasm-parser" "^1.12.1" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" acorn "^8.14.0" browserslist "^4.24.0" chrome-trace-event "^1.0.2" @@ -12623,34 +12855,35 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" -which-builtin-type@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" - integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== dependencies: + call-bound "^1.0.2" function.prototype.name "^1.1.6" has-tostringtag "^1.0.2" is-async-function "^2.0.0" - is-date-object "^1.0.5" - is-finalizationregistry "^1.0.2" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" is-generator-function "^1.0.10" - is-regex "^1.1.4" + is-regex "^1.2.1" is-weakref "^1.0.2" isarray "^2.0.5" - which-boxed-primitive "^1.0.2" + which-boxed-primitive "^1.1.0" which-collection "^1.0.2" - which-typed-array "^1.1.15" + which-typed-array "^1.1.16" which-collection@^1.0.2: version "1.0.2" @@ -12667,15 +12900,16 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== -which-typed-array@^1.1.14, which-typed-array@^1.1.15: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== +which-typed-array@^1.1.16: + version "1.1.18" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.18.tgz#df2389ebf3fbb246a71390e90730a9edb6ce17ad" + integrity sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA== dependencies: available-typed-arrays "^1.0.7" - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" for-each "^0.3.3" - gopd "^1.0.1" + gopd "^1.2.0" has-tostringtag "^1.0.2" which@^1.3.1: @@ -12890,9 +13124,9 @@ zustand@^4.4.0: use-sync-external-store "1.2.2" zustand@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.1.tgz#2bdca5e4be172539558ce3974fe783174a48fdcf" - integrity sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w== + version "5.0.2" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.2.tgz#f7595ada55a565f1fd6464f002a91e701ee0cfca" + integrity sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw== zwitch@^2.0.0: version "2.0.4" diff --git a/python/packages/autogen-studio/notebooks/tutorial.ipynb b/python/packages/autogen-studio/notebooks/tutorial.ipynb index 47dc0220d889..8a267b52e34e 100644 --- a/python/packages/autogen-studio/notebooks/tutorial.ipynb +++ b/python/packages/autogen-studio/notebooks/tutorial.ipynb @@ -1,320 +1,342 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## AutoGen Studio Agent Workflow API Example\n", - "\n", - "This notebook focuses on demonstrating capabilities of the autogen studio workflow python api. \n", - "\n", - "- Declarative Specification of an Agent Team\n", - "- Loading the specification and running the resulting agent\n", - "\n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from autogenstudio.teammanager import TeamManager\n", - "\n", - "wm = TeamManager()\n", - "result = await wm.run(task=\"What is the weather in New York?\", team_config=\"team.json\")\n", - "print(result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "result_stream = wm.run_stream(task=\"What is the weather in New York?\", team_config=\"team.json\")\n", - "async for response in result_stream:\n", - " print(response)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## AutoGen Studio Database API\n", - "\n", - "Api for creating objects and serializing to a database." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Response(message='Database is ready', status=True, data=None)" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from autogenstudio.database import DatabaseManager\n", - "import os\n", - "# delete database\n", - "# if os.path.exists(\"test.db\"):\n", - "# os.remove(\"test.db\")\n", - "\n", - "os.makedirs(\"test\", exist_ok=True)\n", - "# create a database\n", - "dbmanager = DatabaseManager(engine_uri=\"sqlite:///test.db\", base_dir=\"test\")\n", - "dbmanager.initialize_database()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "from sqlmodel import Session, text, select\n", - "from autogenstudio.datamodel import Model, ModelConfig, ModelTypes, Team, TeamConfig, TeamTypes, Agent, AgentConfig, AgentTypes, Tool, ToolConfig, LinkTypes,ToolTypes\n", - "\n", - "user_id = \"guestuser@gmail.com\"\n", - "from autogenstudio.datamodel import ModelConfig, Model, TeamConfig, Team, Tool, Agent, AgentConfig, TerminationConfig, TerminationTypes, ModelTypes, TeamTypes, AgentTypes, ToolConfig, LinkTypes, TerminationTypes\n", - "\n", - "gpt4_model = Model(user_id=user_id, config= ModelConfig(model=\"gpt-4o-2024-08-06\", model_type=ModelTypes.OPENAI).model_dump() )\n", - "\n", - "weather_tool = Tool(user_id=user_id, config=ToolConfig(name=\"get_weather\", description=\"Get the weather for a city\", content=\"async def get_weather(city: str) -> str:\\n return f\\\"The weather in {city} is 73 degrees and Sunny.\\\"\",tool_type=ToolTypes.PYTHON_FUNCTION).model_dump() )\n", - "\n", - "adding_tool = Tool(user_id=user_id, config=ToolConfig(name=\"add\", description=\"Add two numbers\", content=\"async def add(a: int, b: int) -> int:\\n return a + b\", tool_type=ToolTypes.PYTHON_FUNCTION).model_dump() )\n", - "\n", - "writing_agent = Agent(user_id=user_id,\n", - " config=AgentConfig(\n", - " name=\"writing_agent\",\n", - " tools=[weather_tool.config],\n", - " agent_type=AgentTypes.ASSISTANT,\n", - " model_client=gpt4_model.config\n", - " ).model_dump()\n", - " )\n", - "\n", - "team = Team(user_id=user_id, config=TeamConfig(\n", - " name=\"weather_team\",\n", - " participants=[writing_agent.config],\n", - " termination_condition=TerminationConfig(termination_type=TerminationTypes.MAX_MESSAGES, max_messages=5).model_dump(),\n", - " team_type=TeamTypes.ROUND_ROBIN\n", - " ).model_dump()\n", - ")\n", - "\n", - "with Session(dbmanager.engine) as session:\n", - " session.add(gpt4_model)\n", - " session.add(weather_tool)\n", - " session.add(adding_tool)\n", - " session.add(writing_agent)\n", - " session.add(team)\n", - " session.commit()\n", - "\n", - " dbmanager.link(LinkTypes.AGENT_MODEL, writing_agent.id, gpt4_model.id)\n", - " dbmanager.link(LinkTypes.AGENT_TOOL, writing_agent.id, weather_tool.id)\n", - " dbmanager.link(LinkTypes.AGENT_TOOL, writing_agent.id, adding_tool.id)\n", - " dbmanager.link(LinkTypes.TEAM_AGENT, team.id, writing_agent.id)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2 teams in database\n" - ] - } - ], - "source": [ - "all_teams = dbmanager.get(Team)\n", - "print(len(all_teams.data), \"teams in database\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Configuration Manager\n", - "\n", - "Helper class to mostly import teams/agents/models/tools etc into a database." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from autogenstudio.database import ConfigurationManager\n", - "\n", - "config_manager = ConfigurationManager(dbmanager)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "result = await config_manager.import_component(\"team.json\", user_id=\"user_id\", check_exists=True)\n", - "print(result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "result = await config_manager.import_directory(\".\", user_id=\"user_id\", check_exists=False)\n", - "print(result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "all_teams = dbmanager.get(Team)\n", - "print(len(all_teams.data), \"teams in database\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Sample AgentChat Example (Python)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_agentchat.agents import AssistantAgent\n", - "from autogen_agentchat.conditions import TextMentionTermination\n", - "from autogen_agentchat.teams import RoundRobinGroupChat, SelectorGroupChat\n", - "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", - "\n", - "planner_agent = AssistantAgent(\n", - " \"planner_agent\",\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4\"),\n", - " description=\"A helpful assistant that can plan trips.\",\n", - " system_message=\"You are a helpful assistant that can suggest a travel plan for a user based on their request. Respond with a single sentence\",\n", - ")\n", - "\n", - "local_agent = AssistantAgent(\n", - " \"local_agent\",\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4\"),\n", - " description=\"A local assistant that can suggest local activities or places to visit.\",\n", - " system_message=\"You are a helpful assistant that can suggest authentic and interesting local activities or places to visit for a user and can utilize any context information provided. Respond with a single sentence\",\n", - ")\n", - "\n", - "language_agent = AssistantAgent(\n", - " \"language_agent\",\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4\"),\n", - " description=\"A helpful assistant that can provide language tips for a given destination.\",\n", - " system_message=\"You are a helpful assistant that can review travel plans, providing feedback on important/critical tips about how best to address language or communication challenges for the given destination. If the plan already includes language tips, you can mention that the plan is satisfactory, with rationale.Respond with a single sentence\",\n", - ")\n", - "\n", - "travel_summary_agent = AssistantAgent(\n", - " \"travel_summary_agent\",\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4\"),\n", - " description=\"A helpful assistant that can summarize the travel plan.\",\n", - " system_message=\"You are a helpful assistant that can take in all of the suggestions and advice from the other agents and provide a detailed tfinal travel plan. You must ensure th b at the final plan is integrated and complete. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN. When the plan is complete and all perspectives are integrated, you can respond with TERMINATE.Respond with a single sentence\",\n", - ")\n", - "\n", - "termination = TextMentionTermination(\"TERMINATE\")\n", - "group_chat = RoundRobinGroupChat(\n", - " [planner_agent, local_agent, language_agent, travel_summary_agent], termination_condition=termination\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "result = group_chat.run_stream(task=\"Plan a 3 day trip to Nepal.\")\n", - "async for response in result:\n", - " print(response)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Human in the Loop with a UserProxy Agent\n", - "\n", - "AutoGen studio provides a custom agent allows a human interact as part of the agent team.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from autogenstudio.components import UserProxyAgent\n", - "\n", - "def input_func(prompt: str) -> str:\n", - " return \"Hello World there\" + str(prompt)\n", - "user_agent = UserProxyAgent(name=\"user_agent\", description=\"a human user\", input_func=input_func)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_core import CancellationToken\n", - "cancellation_token = CancellationToken()\n", - "stream = user_agent.run_stream(task=\"hello there\", cancellation_token=cancellation_token)\n", - "\n", - "async for response in stream:\n", - " print(response)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "agnext", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## AutoGen Studio Agent Workflow API Example\n", + "\n", + "This notebook focuses on demonstrating capabilities of the autogen studio workflow python api. \n", + "\n", + "- Declarative Specification of an Agent Team\n", + "- Loading the specification and running the resulting agent\n", + "\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "task_result=TaskResult(messages=[TextMessage(source='user', models_usage=None, content='What is the weather in New York?', type='TextMessage'), ToolCallRequestEvent(source='writing_agent', models_usage=RequestUsage(prompt_tokens=65, completion_tokens=15), content=[FunctionCall(id='call_jcgtAVlBvTFzVpPxKX88Xsa4', arguments='{\"city\":\"New York\"}', name='get_weather')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='writing_agent', models_usage=None, content=[FunctionExecutionResult(content='The weather in New York is 73 degrees and Sunny.', call_id='call_jcgtAVlBvTFzVpPxKX88Xsa4')], type='ToolCallExecutionEvent'), TextMessage(source='writing_agent', models_usage=None, content='The weather in New York is 73 degrees and Sunny.', type='TextMessage'), TextMessage(source='writing_agent', models_usage=RequestUsage(prompt_tokens=103, completion_tokens=14), content='The current weather in New York is 73 degrees and sunny.', type='TextMessage')], stop_reason='Maximum number of messages 5 reached, current message count: 5') usage='' duration=5.103050947189331\n" + ] + } + ], + "source": [ + "from autogenstudio.teammanager import TeamManager\n", + "\n", + "wm = TeamManager()\n", + "result = await wm.run(task=\"What is the weather in New York?\", team_config=\"team.json\")\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "source='user' models_usage=None content='What is the weather in New York?' type='TextMessage'\n", + "source='writing_agent' models_usage=RequestUsage(prompt_tokens=65, completion_tokens=15) content=[FunctionCall(id='call_EwdwWogp5jDKdB7t9WGCNjZW', arguments='{\"city\":\"New York\"}', name='get_weather')] type='ToolCallRequestEvent'\n", + "source='writing_agent' models_usage=None content=[FunctionExecutionResult(content='The weather in New York is 73 degrees and Sunny.', call_id='call_EwdwWogp5jDKdB7t9WGCNjZW')] type='ToolCallExecutionEvent'\n", + "source='writing_agent' models_usage=None content='The weather in New York is 73 degrees and Sunny.' type='TextMessage'\n", + "source='writing_agent' models_usage=RequestUsage(prompt_tokens=103, completion_tokens=14) content='The weather in New York is currently 73 degrees and sunny.' type='TextMessage'\n", + "task_result=TaskResult(messages=[TextMessage(source='user', models_usage=None, content='What is the weather in New York?', type='TextMessage'), ToolCallRequestEvent(source='writing_agent', models_usage=RequestUsage(prompt_tokens=65, completion_tokens=15), content=[FunctionCall(id='call_EwdwWogp5jDKdB7t9WGCNjZW', arguments='{\"city\":\"New York\"}', name='get_weather')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='writing_agent', models_usage=None, content=[FunctionExecutionResult(content='The weather in New York is 73 degrees and Sunny.', call_id='call_EwdwWogp5jDKdB7t9WGCNjZW')], type='ToolCallExecutionEvent'), TextMessage(source='writing_agent', models_usage=None, content='The weather in New York is 73 degrees and Sunny.', type='TextMessage'), TextMessage(source='writing_agent', models_usage=RequestUsage(prompt_tokens=103, completion_tokens=14), content='The weather in New York is currently 73 degrees and sunny.', type='TextMessage')], stop_reason='Maximum number of messages 5 reached, current message count: 5') usage='' duration=1.284574270248413\n" + ] + } + ], + "source": [ + "result_stream = wm.run_stream(task=\"What is the weather in New York?\", team_config=\"team.json\")\n", + "async for response in result_stream:\n", + " print(response)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## AutoGen Studio Database API\n", + "\n", + "Api for creating objects and serializing to a database." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Response(message='Database is ready', status=True, data=None)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from autogenstudio.database import DatabaseManager\n", + "import os\n", + "# delete database\n", + "# if os.path.exists(\"test.db\"):\n", + "# os.remove(\"test.db\")\n", + "\n", + "os.makedirs(\"test\", exist_ok=True)\n", + "# create a database\n", + "dbmanager = DatabaseManager(engine_uri=\"sqlite:///test.db\", base_dir=\"test\")\n", + "dbmanager.initialize_database()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from sqlmodel import Session, text, select\n", + "from autogenstudio.datamodel.types import ModelTypes, TeamTypes, AgentTypes, ToolConfig, ToolTypes, OpenAIModelConfig, RoundRobinTeamConfig, MaxMessageTerminationConfig, AssistantAgentConfig, TerminationTypes\n", + "\n", + "from autogenstudio.datamodel.db import Model, Team, Agent, Tool,LinkTypes\n", + "\n", + "user_id = \"guestuser@gmail.com\" \n", + "\n", + "gpt4_model = Model(user_id=user_id, config= OpenAIModelConfig(model=\"gpt-4o-2024-08-06\", model_type=ModelTypes.OPENAI).model_dump() )\n", + "\n", + "weather_tool = Tool(user_id=user_id, config=ToolConfig(name=\"get_weather\", description=\"Get the weather for a city\", content=\"async def get_weather(city: str) -> str:\\n return f\\\"The weather in {city} is 73 degrees and Sunny.\\\"\",tool_type=ToolTypes.PYTHON_FUNCTION).model_dump() )\n", + "\n", + "adding_tool = Tool(user_id=user_id, config=ToolConfig(name=\"add\", description=\"Add two numbers\", content=\"async def add(a: int, b: int) -> int:\\n return a + b\", tool_type=ToolTypes.PYTHON_FUNCTION).model_dump() )\n", + "\n", + "writing_agent = Agent(user_id=user_id,\n", + " config=AssistantAgentConfig(\n", + " name=\"writing_agent\",\n", + " tools=[weather_tool.config],\n", + " agent_type=AgentTypes.ASSISTANT,\n", + " model_client=gpt4_model.config\n", + " ).model_dump()\n", + " )\n", + "\n", + "team = Team(user_id=user_id, config=RoundRobinTeamConfig(\n", + " name=\"weather_team\",\n", + " participants=[writing_agent.config],\n", + " termination_condition=MaxMessageTerminationConfig(termination_type=TerminationTypes.MAX_MESSAGES, max_messages=5).model_dump(),\n", + " team_type=TeamTypes.ROUND_ROBIN\n", + " ).model_dump()\n", + ")\n", + "\n", + "with Session(dbmanager.engine) as session:\n", + " session.add(gpt4_model)\n", + " session.add(weather_tool)\n", + " session.add(adding_tool)\n", + " session.add(writing_agent)\n", + " session.add(team)\n", + " session.commit()\n", + "\n", + " dbmanager.link(LinkTypes.AGENT_MODEL, writing_agent.id, gpt4_model.id)\n", + " dbmanager.link(LinkTypes.AGENT_TOOL, writing_agent.id, weather_tool.id)\n", + " dbmanager.link(LinkTypes.AGENT_TOOL, writing_agent.id, adding_tool.id)\n", + " dbmanager.link(LinkTypes.TEAM_AGENT, team.id, writing_agent.id)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2 teams in database\n" + ] + } + ], + "source": [ + "all_teams = dbmanager.get(Team)\n", + "print(len(all_teams.data), \"teams in database\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration Manager\n", + "\n", + "Helper class to mostly import teams/agents/models/tools etc into a database." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from autogenstudio.database import ConfigurationManager\n", + "\n", + "config_manager = ConfigurationManager(dbmanager)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "message='Team Created Successfully' status=True data={'id': 4, 'updated_at': datetime.datetime(2024, 12, 15, 15, 52, 21, 674916), 'version': '0.0.1', 'created_at': datetime.datetime(2024, 12, 15, 15, 52, 21, 674910), 'user_id': 'user_id', 'config': {'version': '1.0.0', 'component_type': 'team', 'name': 'weather_team', 'participants': [{'version': '1.0.0', 'component_type': 'agent', 'name': 'writing_agent', 'agent_type': 'AssistantAgent', 'description': None, 'model_client': {'version': '1.0.0', 'component_type': 'model', 'model': 'gpt-4o-2024-08-06', 'model_type': 'OpenAIChatCompletionClient', 'api_key': None, 'base_url': None}, 'tools': [{'version': '1.0.0', 'component_type': 'tool', 'name': 'get_weather', 'description': 'Get the weather for a city', 'content': 'async def get_weather(city: str) -> str:\\n return f\"The weather in {city} is 73 degrees and Sunny.\"', 'tool_type': 'PythonFunction'}], 'system_message': None}], 'team_type': 'RoundRobinGroupChat', 'termination_condition': {'version': '1.0.0', 'component_type': 'termination', 'termination_type': 'MaxMessageTermination', 'max_messages': 5}, 'max_turns': None}}\n" + ] + } + ], + "source": [ + "result = await config_manager.import_component(\"team.json\", user_id=\"user_id\", check_exists=True)\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "message='Directory import complete' status=True data=[{'component': 'team', 'status': True, 'message': 'Team Created Successfully', 'id': 5}]\n" + ] + } + ], + "source": [ + "result = await config_manager.import_directory(\".\", user_id=\"user_id\", check_exists=False)\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5 teams in database\n" + ] + } + ], + "source": [ + "all_teams = dbmanager.get(Team)\n", + "print(len(all_teams.data), \"teams in database\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sample AgentChat Example (Python)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.conditions import TextMentionTermination\n", + "from autogen_agentchat.teams import RoundRobinGroupChat, SelectorGroupChat\n", + "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", + "\n", + "planner_agent = AssistantAgent(\n", + " \"planner_agent\",\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4\"),\n", + " description=\"A helpful assistant that can plan trips.\",\n", + " system_message=\"You are a helpful assistant that can suggest a travel plan for a user based on their request. Respond with a single sentence\",\n", + ")\n", + "\n", + "local_agent = AssistantAgent(\n", + " \"local_agent\",\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4\"),\n", + " description=\"A local assistant that can suggest local activities or places to visit.\",\n", + " system_message=\"You are a helpful assistant that can suggest authentic and interesting local activities or places to visit for a user and can utilize any context information provided. Respond with a single sentence\",\n", + ")\n", + "\n", + "language_agent = AssistantAgent(\n", + " \"language_agent\",\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4\"),\n", + " description=\"A helpful assistant that can provide language tips for a given destination.\",\n", + " system_message=\"You are a helpful assistant that can review travel plans, providing feedback on important/critical tips about how best to address language or communication challenges for the given destination. If the plan already includes language tips, you can mention that the plan is satisfactory, with rationale.Respond with a single sentence\",\n", + ")\n", + "\n", + "travel_summary_agent = AssistantAgent(\n", + " \"travel_summary_agent\",\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4\"),\n", + " description=\"A helpful assistant that can summarize the travel plan.\",\n", + " system_message=\"You are a helpful assistant that can take in all of the suggestions and advice from the other agents and provide a detailed tfinal travel plan. You must ensure th b at the final plan is integrated and complete. YOUR FINAL RESPONSE MUST BE THE COMPLETE PLAN. When the plan is complete and all perspectives are integrated, you can respond with TERMINATE.Respond with a single sentence\",\n", + ")\n", + "\n", + "termination = TextMentionTermination(\"TERMINATE\")\n", + "group_chat = RoundRobinGroupChat(\n", + " [planner_agent, local_agent, language_agent, travel_summary_agent], termination_condition=termination\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "source='user' models_usage=None content='Plan a 3 day trip to Nepal.' type='TextMessage'\n", + "source='planner_agent' models_usage=RequestUsage(prompt_tokens=45, completion_tokens=53) content='I recommend starting your trip in Kathmandu, where you can explore the historic Durbar Square and Pashupatinath Temple, then take a scenic flight over the Everest range, and finish your journey with a stunning hike in the Annapurna region.' type='TextMessage'\n", + "source='local_agent' models_usage=RequestUsage(prompt_tokens=115, completion_tokens=53) content='I recommend starting your trip in Kathmandu, where you can explore the historic Durbar Square and Pashupatinath Temple, then take a scenic flight over the Everest range, and finish your journey with a stunning hike in the Annapurna region.' type='TextMessage'\n", + "source='language_agent' models_usage=RequestUsage(prompt_tokens=199, completion_tokens=42) content=\"For your trip to Nepal, it's crucial to learn some phrases in Nepali since English is not widely spoken outside of major cities and tourist areas; even a simple phrasebook or translation app would be beneficial.\" type='TextMessage'\n", + "source='travel_summary_agent' models_usage=RequestUsage(prompt_tokens=265, completion_tokens=298) content=\"Day 1: Begin your journey in Kathmandu, where you can visit the historic Durbar Square, a UNESCO World Heritage site that showcases intricate woodcarving and houses the iconic Kasthamandap Temple. From there, proceed to the sacred Pashupatinath Temple, a significant Hindu pilgrimage site on the banks of the holy Bagmati River.\\n\\nDay 2: Embark on an early morning scenic flight over the Everest range. This one-hour flight provides a breathtaking view of the world's highest peak along with other neighboring peaks. Standard flights depart from Tribhuvan International Airport between 6:30 AM to 7:30 AM depending on the weather. Spend the remainder of the day exploring the local markets in Kathmandu, sampling a variety of Nepalese cuisines and shopping for unique souvenirs.\\n\\nDay 3: Finally, take a short flight or drive to Pokhara, the gateway to the Annapurna region. Embark on a guided hike enjoying the stunning backdrop of the Annapurna ranges and the serene Phewa lake.\\n\\nRemember to bring along a phrasebook or translation app, as English is not widely spoken in Nepal, particularly outside of major cities and tourist hotspots. \\n\\nPack comfortable trekking gear, adequate water, medical and emergency supplies. It's also advisable to check on the weather updates, as conditions can change rapidly, particularly in mountainous areas. Enjoy your Nepal expedition!TERMINATE\" type='TextMessage'\n", + "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Plan a 3 day trip to Nepal.', type='TextMessage'), TextMessage(source='planner_agent', models_usage=RequestUsage(prompt_tokens=45, completion_tokens=53), content='I recommend starting your trip in Kathmandu, where you can explore the historic Durbar Square and Pashupatinath Temple, then take a scenic flight over the Everest range, and finish your journey with a stunning hike in the Annapurna region.', type='TextMessage'), TextMessage(source='local_agent', models_usage=RequestUsage(prompt_tokens=115, completion_tokens=53), content='I recommend starting your trip in Kathmandu, where you can explore the historic Durbar Square and Pashupatinath Temple, then take a scenic flight over the Everest range, and finish your journey with a stunning hike in the Annapurna region.', type='TextMessage'), TextMessage(source='language_agent', models_usage=RequestUsage(prompt_tokens=199, completion_tokens=42), content=\"For your trip to Nepal, it's crucial to learn some phrases in Nepali since English is not widely spoken outside of major cities and tourist areas; even a simple phrasebook or translation app would be beneficial.\", type='TextMessage'), TextMessage(source='travel_summary_agent', models_usage=RequestUsage(prompt_tokens=265, completion_tokens=298), content=\"Day 1: Begin your journey in Kathmandu, where you can visit the historic Durbar Square, a UNESCO World Heritage site that showcases intricate woodcarving and houses the iconic Kasthamandap Temple. From there, proceed to the sacred Pashupatinath Temple, a significant Hindu pilgrimage site on the banks of the holy Bagmati River.\\n\\nDay 2: Embark on an early morning scenic flight over the Everest range. This one-hour flight provides a breathtaking view of the world's highest peak along with other neighboring peaks. Standard flights depart from Tribhuvan International Airport between 6:30 AM to 7:30 AM depending on the weather. Spend the remainder of the day exploring the local markets in Kathmandu, sampling a variety of Nepalese cuisines and shopping for unique souvenirs.\\n\\nDay 3: Finally, take a short flight or drive to Pokhara, the gateway to the Annapurna region. Embark on a guided hike enjoying the stunning backdrop of the Annapurna ranges and the serene Phewa lake.\\n\\nRemember to bring along a phrasebook or translation app, as English is not widely spoken in Nepal, particularly outside of major cities and tourist hotspots. \\n\\nPack comfortable trekking gear, adequate water, medical and emergency supplies. It's also advisable to check on the weather updates, as conditions can change rapidly, particularly in mountainous areas. Enjoy your Nepal expedition!TERMINATE\", type='TextMessage')], stop_reason=\"Text 'TERMINATE' mentioned\")\n" + ] + } + ], + "source": [ + "\n", + "result = group_chat.run_stream(task=\"Plan a 3 day trip to Nepal.\")\n", + "async for response in result:\n", + " print(response)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "agnext", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-studio/tests/test_component_factory.py b/python/packages/autogen-studio/tests/test_component_factory.py index 57c0e38192c0..c5339cc70721 100644 --- a/python/packages/autogen-studio/tests/test_component_factory.py +++ b/python/packages/autogen-studio/tests/test_component_factory.py @@ -7,11 +7,16 @@ from autogen_core.tools import FunctionTool from autogenstudio.datamodel.types import ( - AgentConfig, - ModelConfig, - TeamConfig, + AssistantAgentConfig, + OpenAIModelConfig, + RoundRobinTeamConfig, + SelectorTeamConfig, + MagenticOneTeamConfig, ToolConfig, - TerminationConfig, + MaxMessageTerminationConfig, + StopMessageTerminationConfig, + TextMentionTerminationConfig, + CombinationTerminationConfig, ModelTypes, AgentTypes, TeamTypes, @@ -56,7 +61,7 @@ def calculator(a: int, b: int, operation: str = '+') -> int: @pytest.fixture def sample_model_config(): - return ModelConfig( + return OpenAIModelConfig( model_type=ModelTypes.OPENAI, model="gpt-4", api_key="test-key", @@ -66,8 +71,8 @@ def sample_model_config(): @pytest.fixture -def sample_agent_config(sample_model_config: ModelConfig, sample_tool_config: ToolConfig): - return AgentConfig( +def sample_agent_config(sample_model_config: OpenAIModelConfig, sample_tool_config: ToolConfig): + return AssistantAgentConfig( name="test_agent", agent_type=AgentTypes.ASSISTANT, system_message="You are a helpful assistant", @@ -80,7 +85,7 @@ def sample_agent_config(sample_model_config: ModelConfig, sample_tool_config: To @pytest.fixture def sample_termination_config(): - return TerminationConfig( + return MaxMessageTerminationConfig( termination_type=TerminationTypes.MAX_MESSAGES, max_messages=10, component_type=ComponentTypes.TERMINATION, @@ -90,9 +95,9 @@ def sample_termination_config(): @pytest.fixture def sample_team_config( - sample_agent_config: AgentConfig, sample_termination_config: TerminationConfig, sample_model_config: ModelConfig + sample_agent_config: AssistantAgentConfig, sample_termination_config: MaxMessageTerminationConfig, sample_model_config: OpenAIModelConfig ): - return TeamConfig( + return RoundRobinTeamConfig( name="test_team", team_type=TeamTypes.ROUND_ROBIN, participants=[sample_agent_config], @@ -146,14 +151,14 @@ async def test_load_tool_invalid_config(component_factory: ComponentFactory): @pytest.mark.asyncio -async def test_load_model(component_factory: ComponentFactory, sample_model_config: ModelConfig): +async def test_load_model(component_factory: ComponentFactory, sample_model_config: OpenAIModelConfig): # Test loading model from ModelConfig model = await component_factory.load_model(sample_model_config) assert model is not None @pytest.mark.asyncio -async def test_load_agent(component_factory: ComponentFactory, sample_agent_config: AgentConfig): +async def test_load_agent(component_factory: ComponentFactory, sample_agent_config: AssistantAgentConfig): # Test loading agent from AgentConfig agent = await component_factory.load_agent(sample_agent_config) assert isinstance(agent, AssistantAgent) @@ -163,8 +168,8 @@ async def test_load_agent(component_factory: ComponentFactory, sample_agent_conf @pytest.mark.asyncio async def test_load_termination(component_factory: ComponentFactory): - # Test MaxMessageTermination - max_msg_config = TerminationConfig( + + max_msg_config = MaxMessageTerminationConfig( termination_type=TerminationTypes.MAX_MESSAGES, max_messages=5, component_type=ComponentTypes.TERMINATION, @@ -175,14 +180,14 @@ async def test_load_termination(component_factory: ComponentFactory): assert termination._max_messages == 5 # Test StopMessageTermination - stop_msg_config = TerminationConfig( + stop_msg_config = StopMessageTerminationConfig( termination_type=TerminationTypes.STOP_MESSAGE, component_type=ComponentTypes.TERMINATION, version="1.0.0" ) termination = await component_factory.load_termination(stop_msg_config) assert isinstance(termination, StopMessageTermination) # Test TextMentionTermination - text_mention_config = TerminationConfig( + text_mention_config = TextMentionTerminationConfig( termination_type=TerminationTypes.TEXT_MENTION, text="DONE", component_type=ComponentTypes.TERMINATION, @@ -193,17 +198,17 @@ async def test_load_termination(component_factory: ComponentFactory): assert termination._text == "DONE" # Test AND combination - and_combo_config = TerminationConfig( + and_combo_config = CombinationTerminationConfig( termination_type=TerminationTypes.COMBINATION, operator="and", conditions=[ - TerminationConfig( + MaxMessageTerminationConfig( termination_type=TerminationTypes.MAX_MESSAGES, max_messages=5, component_type=ComponentTypes.TERMINATION, version="1.0.0", ), - TerminationConfig( + TextMentionTerminationConfig( termination_type=TerminationTypes.TEXT_MENTION, text="DONE", component_type=ComponentTypes.TERMINATION, @@ -217,17 +222,17 @@ async def test_load_termination(component_factory: ComponentFactory): assert termination is not None # Test OR combination - or_combo_config = TerminationConfig( + or_combo_config = CombinationTerminationConfig( termination_type=TerminationTypes.COMBINATION, operator="or", conditions=[ - TerminationConfig( + MaxMessageTerminationConfig( termination_type=TerminationTypes.MAX_MESSAGES, max_messages=5, component_type=ComponentTypes.TERMINATION, version="1.0.0", ), - TerminationConfig( + TextMentionTerminationConfig( termination_type=TerminationTypes.TEXT_MENTION, text="DONE", component_type=ComponentTypes.TERMINATION, @@ -243,7 +248,7 @@ async def test_load_termination(component_factory: ComponentFactory): # Test invalid combinations with pytest.raises(ValueError): await component_factory.load_termination( - TerminationConfig( + CombinationTerminationConfig( termination_type=TerminationTypes.COMBINATION, conditions=[], # Empty conditions component_type=ComponentTypes.TERMINATION, @@ -253,11 +258,11 @@ async def test_load_termination(component_factory: ComponentFactory): with pytest.raises(ValueError): await component_factory.load_termination( - TerminationConfig( + CombinationTerminationConfig( termination_type=TerminationTypes.COMBINATION, operator="invalid", # type: ignore conditions=[ - TerminationConfig( + MaxMessageTerminationConfig( termination_type=TerminationTypes.MAX_MESSAGES, max_messages=5, component_type=ComponentTypes.TERMINATION, @@ -272,16 +277,16 @@ async def test_load_termination(component_factory: ComponentFactory): # Test missing operator with pytest.raises(ValueError): await component_factory.load_termination( - TerminationConfig( + CombinationTerminationConfig( termination_type=TerminationTypes.COMBINATION, conditions=[ - TerminationConfig( + MaxMessageTerminationConfig( termination_type=TerminationTypes.MAX_MESSAGES, max_messages=5, component_type=ComponentTypes.TERMINATION, version="1.0.0", ), - TerminationConfig( + TextMentionTerminationConfig( termination_type=TerminationTypes.TEXT_MENTION, text="DONE", component_type=ComponentTypes.TERMINATION, @@ -296,7 +301,7 @@ async def test_load_termination(component_factory: ComponentFactory): @pytest.mark.asyncio async def test_load_team( - component_factory: ComponentFactory, sample_team_config: TeamConfig, sample_model_config: ModelConfig + component_factory: ComponentFactory, sample_team_config: RoundRobinTeamConfig, sample_model_config: OpenAIModelConfig ): # Test loading RoundRobinGroupChat team team = await component_factory.load_team(sample_team_config) @@ -304,12 +309,12 @@ async def test_load_team( assert len(team._participants) == 1 # Test loading SelectorGroupChat team with multiple participants - selector_team_config = TeamConfig( + selector_team_config = SelectorTeamConfig( name="selector_team", team_type=TeamTypes.SELECTOR, participants=[ # Add two participants sample_team_config.participants[0], # First agent - AgentConfig( # Second agent + AssistantAgentConfig( # Second agent name="test_agent_2", agent_type=AgentTypes.ASSISTANT, system_message="You are another helpful assistant", @@ -329,12 +334,12 @@ async def test_load_team( assert len(team._participants) == 2 # Test loading MagenticOneGroupChat team - magentic_one_config = TeamConfig( + magentic_one_config = MagenticOneTeamConfig( name="magentic_one_team", team_type=TeamTypes.MAGENTIC_ONE, participants=[ # Add two participants sample_team_config.participants[0], # First agent - AgentConfig( # Second agent + AssistantAgentConfig( # Second agent name="test_agent_2", agent_type=AgentTypes.ASSISTANT, system_message="You are another helpful assistant", @@ -360,7 +365,7 @@ async def test_invalid_configs(component_factory: ComponentFactory): # Test invalid agent type with pytest.raises(ValueError): await component_factory.load_agent( - AgentConfig( + AssistantAgentConfig( name="test", agent_type="InvalidAgent", # type: ignore system_message="test", @@ -372,7 +377,7 @@ async def test_invalid_configs(component_factory: ComponentFactory): # Test invalid team type with pytest.raises(ValueError): await component_factory.load_team( - TeamConfig( + RoundRobinTeamConfig( name="test", team_type="InvalidTeam", # type: ignore participants=[], @@ -384,7 +389,7 @@ async def test_invalid_configs(component_factory: ComponentFactory): # Test invalid termination type with pytest.raises(ValueError): await component_factory.load_termination( - TerminationConfig( + MaxMessageTerminationConfig( termination_type="InvalidTermination", # type: ignore component_type=ComponentTypes.TERMINATION, version="1.0.0", diff --git a/python/packages/autogen-studio/tests/test_db_manager.py b/python/packages/autogen-studio/tests/test_db_manager.py index b77a891a7979..a72dfc4842ec 100644 --- a/python/packages/autogen-studio/tests/test_db_manager.py +++ b/python/packages/autogen-studio/tests/test_db_manager.py @@ -6,9 +6,13 @@ from autogenstudio.database import DatabaseManager from autogenstudio.datamodel.types import ( - ModelConfig, AgentConfig, ToolConfig, - TeamConfig, ModelTypes, AgentTypes, TeamTypes, ComponentTypes, - TerminationConfig, TerminationTypes, ToolTypes + ToolConfig, + OpenAIModelConfig, + RoundRobinTeamConfig, + StopMessageTerminationConfig, + AssistantAgentConfig, + ModelTypes, AgentTypes, TeamTypes, ComponentTypes, + TerminationTypes, ToolTypes ) from autogenstudio.datamodel.db import Model, Tool, Agent, Team, LinkTypes @@ -42,7 +46,7 @@ def sample_model(test_user: str) -> Model: """Create a sample model with proper config""" return Model( user_id=test_user, - config=ModelConfig( + config=OpenAIModelConfig( model="gpt-4", model_type=ModelTypes.OPENAI, component_type=ComponentTypes.MODEL, @@ -72,10 +76,10 @@ def sample_agent(test_user: str, sample_model: Model, sample_tool: Tool) -> Agen """Create a sample agent with proper config and relationships""" return Agent( user_id=test_user, - config=AgentConfig( + config=AssistantAgentConfig( name="test_agent", agent_type=AgentTypes.ASSISTANT, - model_client=ModelConfig.model_validate(sample_model.config), + model_client=OpenAIModelConfig.model_validate(sample_model.config), tools=[ToolConfig.model_validate(sample_tool.config)], component_type=ComponentTypes.AGENT, version="1.0.0" @@ -88,10 +92,11 @@ def sample_team(test_user: str, sample_agent: Agent) -> Team: """Create a sample team with proper config""" return Team( user_id=test_user, - config=TeamConfig( + config=RoundRobinTeamConfig( name="test_team", - participants=[AgentConfig.model_validate(sample_agent.config)], - termination_condition=TerminationConfig( + participants=[AssistantAgentConfig.model_validate( + sample_agent.config)], + termination_condition=StopMessageTerminationConfig( termination_type=TerminationTypes.STOP_MESSAGE, component_type=ComponentTypes.TERMINATION, version="1.0.0" @@ -142,7 +147,7 @@ def test_multiple_links(self, test_db: DatabaseManager, sample_agent: Agent): # Create two models with updated configs model1 = Model( user_id="test_user", - config=ModelConfig( + config=OpenAIModelConfig( model="gpt-4", model_type=ModelTypes.OPENAI, component_type=ComponentTypes.MODEL, @@ -151,7 +156,7 @@ def test_multiple_links(self, test_db: DatabaseManager, sample_agent: Agent): ) model2 = Model( user_id="test_user", - config=ModelConfig( + config=OpenAIModelConfig( model="gpt-3.5", model_type=ModelTypes.OPENAI, component_type=ComponentTypes.MODEL, diff --git a/python/samples/agentchat_chainlit/.gitignore b/python/samples/agentchat_chainlit/.gitignore new file mode 100644 index 000000000000..2a906c6b150a --- /dev/null +++ b/python/samples/agentchat_chainlit/.gitignore @@ -0,0 +1,63 @@ +build +dist + +*.egg-info + +.env + +*.files + +venv +.venv +.DS_Store + +.chainlit +!cypress/e2e/**/*/.chainlit/* +chainlit.md + +cypress/screenshots +cypress/videos +cypress/downloads + +__pycache__ + +.ipynb_checkpoints + +*.db + +.mypy_cache + +chat_files + +.chroma + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.aider* +.coverage + +backend/README.md +backend/.dmypy.json \ No newline at end of file diff --git a/python/samples/agentchat_chainlit/README.md b/python/samples/agentchat_chainlit/README.md new file mode 100644 index 000000000000..3f297158598c --- /dev/null +++ b/python/samples/agentchat_chainlit/README.md @@ -0,0 +1,108 @@ +# Building a Multi-Agent Application with AutoGen and Chainlit + +In this sample, we will build a simple chat interface that interacts with a `RoundRobinGroupChat` team built using the [AutoGen AgentChat](https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/index.html) api. + +![AgentChat](docs/chainlit_autogen.png). + +## High-Level Description + +The `app.py` script sets up a Chainlit chat interface that communicates with the AutoGen team. When a chat starts, it + +- Initializes an AgentChat team. + +```python + +async def get_weather(city: str) -> str: + return f"The weather in {city} is 73 degrees and Sunny." + +assistant_agent = AssistantAgent( + name="assistant_agent", + tools=[get_weather], + model_client=OpenAIChatCompletionClient( + model="gpt-4o-2024-08-06")) + + +termination = TextMentionTermination("TERMINATE") | MaxMessageTermination(10) +team = RoundRobinGroupChat( + participants=[assistant_agent], termination_condition=termination) + +``` + +- As users interact with the chat, their queries are sent to the team which responds. +- As agents respond/act, their responses are streamed back to the chat interface. + +## Quickstart + +To get started, ensure you have setup an API Key. We will be using the OpenAI API for this example. + +1. Ensure you have an OPENAPI API key. Set this key in your environment variables as `OPENAI_API_KEY`. + +2. Install the required Python packages by running: + +```shell +pip install -r requirements.txt +``` + +3. Run the `app.py` script to start the Chainlit server. + +```shell +chainlit run app.py -h +``` + +4. Interact with the Agent Team Chainlit interface. The chat interface will be available at `http://localhost:8000` by default. + +### Function Definitions + +- `start_chat`: Initializes the chat session +- `run_team`: Sends the user's query to the team streams the agent responses back to the chat interface. +- `chat`: Receives messages from the user and passes them to the `run_team` function. + + +## Adding a UserProxyAgent + +We can add a `UserProxyAgent` to the team so that the user can interact with the team directly with the input box in the chat interface. This requires defining a function for input that uses the Chainlit input box instead of the terminal. + +```python +from typing import Optional +from autogen_core import CancellationToken +from autogen_agentchat.agents import AssistantAgent, UserProxyAgent +from autogen_agentchat.conditions import TextMentionTermination, MaxMessageTermination +from autogen_agentchat.teams import RoundRobinGroupChat +from autogen_ext.models.openai import OpenAIChatCompletionClient + +async def chainlit_input_func(prompt: str, cancellation_token: Optional[CancellationToken] = None) -> str: + try: + response = await cl.AskUserMessage( + content=prompt, + author="System", + ).send() + return response["output"] + + except Exception as e: + raise RuntimeError(f"Failed to get user input: {str(e)}") from e + +user_proxy_agent = UserProxyAgent( + name="user_proxy_agent", + input_func=chainlit_input_func, +) +assistant_agent = AssistantAgent( + name="assistant_agent", + model_client=OpenAIChatCompletionClient( + model="gpt-4o-2024-08-06")) + +termination = TextMentionTermination("TERMINATE") | MaxMessageTermination(10) + +team = RoundRobinGroupChat( + participants=[user_proxy_agent, assistant_agent], + termination_condition=termination) +``` + + + +## Next Steps (Extra Credit) + +In this example, we created a basic AutoGen team with a single agent in a RoundRobinGroupChat team. There are a few ways you can extend this example: + +- Add more [agents](https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/tutorial/agents.html) to the team. +- Explor custom agents that sent multimodal messages +- Explore more [team](https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/tutorial/teams.html) types beyond the `RoundRobinGroupChat`. diff --git a/python/samples/agentchat_chainlit/app.py b/python/samples/agentchat_chainlit/app.py new file mode 100644 index 000000000000..370b990b2adb --- /dev/null +++ b/python/samples/agentchat_chainlit/app.py @@ -0,0 +1,41 @@ +import chainlit as cl +from autogen_agentchat.agents import AssistantAgent +from autogen_agentchat.conditions import TextMentionTermination, MaxMessageTermination +from autogen_agentchat.teams import RoundRobinGroupChat +from autogen_ext.models.openai import OpenAIChatCompletionClient +from autogen_agentchat.base import TaskResult + + +async def get_weather(city: str) -> str: + return f"The weather in {city} is 73 degrees and Sunny." + + +@cl.on_chat_start +async def start_chat(): + cl.user_session.set( + "prompt_history", + "", + ) + + +async def run_team(query: str): + assistant_agent = AssistantAgent( + name="assistant_agent", tools=[get_weather], model_client=OpenAIChatCompletionClient(model="gpt-4o-2024-08-06") + ) + + termination = TextMentionTermination("TERMINATE") | MaxMessageTermination(10) + team = RoundRobinGroupChat(participants=[assistant_agent], termination_condition=termination) + + response_stream = team.run_stream(task=query) + async for msg in response_stream: + if hasattr(msg, "content"): + msg = cl.Message(content=msg.content, author="Agent Team") + await msg.send() + if isinstance(msg, TaskResult): + msg = cl.Message(content="Termination condition met. Team and Agents are reset.", author="Agent Team") + await msg.send() + + +@cl.on_message +async def chat(message: cl.Message): + await run_team(message.content) diff --git a/python/samples/agentchat_chainlit/docs/chainlit_autogen.png b/python/samples/agentchat_chainlit/docs/chainlit_autogen.png new file mode 100644 index 000000000000..433f7e1117e9 --- /dev/null +++ b/python/samples/agentchat_chainlit/docs/chainlit_autogen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6fe23b5fc5d9cde6f75db6a2b5716e4460e00739bbe3788707846faf7e31130 +size 63730 diff --git a/python/samples/agentchat_chainlit/requirements.txt b/python/samples/agentchat_chainlit/requirements.txt new file mode 100644 index 000000000000..70a9e9a94389 --- /dev/null +++ b/python/samples/agentchat_chainlit/requirements.txt @@ -0,0 +1,2 @@ +chainlit +autogen-agentchat==0.4.0.dev11 \ No newline at end of file