mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into python-middleware
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
name: Reusable Setup UV
|
||||
description: Reusable workflow to setup uv environment
|
||||
|
||||
inputs:
|
||||
python-version:
|
||||
description: The Python version to set up
|
||||
required: true
|
||||
os:
|
||||
description: The operating system to set up
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ inputs.os }}-${{ inputs.python-version }}
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
- name: Install the project
|
||||
shell: bash
|
||||
run: |
|
||||
cd python && uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit
|
||||
@@ -28,15 +28,15 @@ jobs:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Python - Create Docs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
|
||||
jobs:
|
||||
python-build-docs:
|
||||
if: github.event_name == 'release' && startsWith(github.event.release.tag_name, 'python-')
|
||||
name: Python Build Docs
|
||||
runs-on: ubuntu-latest
|
||||
environment: "integration"
|
||||
env:
|
||||
UV_PYTHON: "3.11"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ runner.os }}-${{ env.UV_PYTHON }}
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-packages --dev --docs
|
||||
- name: Build the docs
|
||||
run: uv run poe docs-full
|
||||
# Upload docs to learn gh
|
||||
@@ -67,16 +67,15 @@ jobs:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
- name: Install the project
|
||||
run: |
|
||||
uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Test with pytest
|
||||
timeout-minutes: 10
|
||||
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test -n logical --dist loadfile --dist worksteal --junitxml=coverage.xml
|
||||
@@ -128,16 +127,15 @@ jobs:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
- name: Install the project
|
||||
run: |
|
||||
uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: azure/login@v2
|
||||
@@ -195,16 +193,15 @@ jobs:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
- name: Install the project
|
||||
run: |
|
||||
uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Azure CLI Login
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: azure/login@v2
|
||||
|
||||
@@ -24,13 +24,15 @@ jobs:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ runner.os }}-${{ env.UV_PYTHON }}
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
# Extract package name from tag (format: python-<package>-<version>)
|
||||
|
||||
@@ -26,15 +26,15 @@ jobs:
|
||||
- name: Save PR number
|
||||
run: |
|
||||
echo ${{ github.event.number }} > ./pr_number
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ runner.os }}-${{ env.UV_PYTHON }}
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run all tests with coverage report
|
||||
run: uv run poe all-tests -n logical --dist loadfile --dist worksteal --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
|
||||
- name: Upload coverage report
|
||||
|
||||
@@ -27,17 +27,15 @@ jobs:
|
||||
working-directory: python
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
version-file: "python/pyproject.toml"
|
||||
enable-cache: true
|
||||
cache-suffix: ${{ runner.os }}-${{ matrix.python-version }}
|
||||
cache-dependency-glob: "**/uv.lock"
|
||||
- name: Install the project
|
||||
run: |
|
||||
uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit
|
||||
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
# Main package tests
|
||||
- name: Set environment variables - main - win
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
@@ -104,28 +102,6 @@ jobs:
|
||||
name: coverage-${{ matrix.OS }}-${{ matrix.python-version }}-${{ env.PACKAGE_NAME }}
|
||||
path: ./python/coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml
|
||||
|
||||
# Workflow package tests
|
||||
- name: Set environment variables - workflow - win
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
run: |
|
||||
echo "PACKAGE_NAME=workflow" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
- name: Set environment variables - workflow
|
||||
if: ${{ matrix.os != 'windows-latest' }}
|
||||
run: |
|
||||
echo "PACKAGE_NAME=workflow" >> $GITHUB_ENV
|
||||
- name: Test with pytest - workflow
|
||||
run: uv run poe --directory ./packages/${{ env.PACKAGE_NAME }} test -n logical --dist loadfile --dist worksteal --junitxml=coverage.xml
|
||||
working-directory: ./python
|
||||
- name: Move coverage file - workflow
|
||||
run: |
|
||||
mv ./packages/${{ env.PACKAGE_NAME }}/coverage.xml coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml
|
||||
working-directory: ./python
|
||||
- name: Upload coverage artifact - workflow
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-${{ matrix.OS }}-${{ matrix.python-version }}-${{ env.PACKAGE_NAME }}
|
||||
path: ./python/coverage_${{ matrix.OS }}_${{ matrix.python-version }}_${{ env.PACKAGE_NAME }}.xml
|
||||
|
||||
# Surface failing tests
|
||||
- name: Surface failing tests
|
||||
if: always()
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ instance/
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
docs/build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step10_AsMcpTool/Agent_Step10_AsMcpTool.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step11_UsingImages/Agent_Step11_UsingImages.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step12_AsFunctionTool/Agent_Step12_AsFunctionTool.csproj" />
|
||||
<Project Path="samples/GettingStarted/Agents/Agent_Step13_Memory/Agent_Step13_Memory.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/GettingStarted/AgentWithOpenAI/">
|
||||
<File Path="samples/GettingStarted/AgentWithOpenAI/README.md" />
|
||||
@@ -190,6 +191,10 @@
|
||||
<File Path="src/LegacySupport/CallerAttributes/CallerArgumentExpressionAttribute.cs" />
|
||||
<File Path="src/LegacySupport/CallerAttributes/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/LegacySupport/CompilerFeatureRequiredAttribute/">
|
||||
<File Path="src/LegacySupport/CompilerFeatureRequiredAttribute/CompilerFeatureRequiredAttribute.cs" />
|
||||
<File Path="src/LegacySupport/CompilerFeatureRequiredAttribute/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/LegacySupport/DiagnosticAttributes/">
|
||||
<File Path="src/LegacySupport/DiagnosticAttributes/NullableAttributes.cs" />
|
||||
<File Path="src/LegacySupport/DiagnosticAttributes/README.md" />
|
||||
@@ -206,6 +211,10 @@
|
||||
<File Path="src/LegacySupport/IsExternalInit/IsExternalInit.cs" />
|
||||
<File Path="src/LegacySupport/IsExternalInit/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/LegacySupport/RequiredMemberAttribute/">
|
||||
<File Path="src/LegacySupport/RequiredMemberAttribute/README.md" />
|
||||
<File Path="src/LegacySupport/RequiredMemberAttribute/RequiredMemberAttribute.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/LegacySupport/TrimAttributes/">
|
||||
<File Path="src/LegacySupport/TrimAttributes/DynamicallyAccessedMembersAttribute.cs" />
|
||||
<File Path="src/LegacySupport/TrimAttributes/DynamicallyAccessedMemberTypes.cs" />
|
||||
|
||||
@@ -22,4 +22,12 @@
|
||||
<ItemGroup Condition="'$(InjectTrimAttributesOnLegacy)' == 'true' AND !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\TrimAttributes\*.cs" LinkBase="LegacySupport\TrimAttributes" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(InjectRequiredMemberOnLegacy)' == 'true' AND !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\RequiredMemberAttribute\*.cs" LinkBase="LegacySupport\RequiredMemberAttribute" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(InjectCompilerFeatureRequiredOnLegacy)' == 'true' AND !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\CompilerFeatureRequiredAttribute\*.cs" LinkBase="LegacySupport\CompilerFeatureRequiredAttribute" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-4
@@ -39,8 +39,7 @@ namespace SampleApp
|
||||
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
|
||||
|
||||
// Notify the thread of the input and output messages.
|
||||
await NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken);
|
||||
await NotifyThreadOfNewMessagesAsync(thread, responseMessages, cancellationToken);
|
||||
await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken);
|
||||
|
||||
return new AgentRunResponse
|
||||
{
|
||||
@@ -59,8 +58,7 @@ namespace SampleApp
|
||||
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
|
||||
|
||||
// Notify the thread of the input and output messages.
|
||||
await NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken);
|
||||
await NotifyThreadOfNewMessagesAsync(thread, responseMessages, cancellationToken);
|
||||
await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken);
|
||||
|
||||
foreach (var message in responseMessages)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Extensions.AI.Agents\Microsoft.Extensions.AI.Agents.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample shows how to add a basic custom memory component to an agent.
|
||||
// The memory component subscribes to all messages added to the conversation and
|
||||
// extracts the user's name and age if provided.
|
||||
// The component adds a prompt to ask for this information if it is not already known
|
||||
// and provides it to the model before each invocation if known.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using OpenAI;
|
||||
using OpenAI.Chat;
|
||||
using SampleApp;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
ChatClient chatClient = new AzureOpenAIClient(
|
||||
new Uri(endpoint),
|
||||
new AzureCliCredential())
|
||||
.GetChatClient(deploymentName);
|
||||
|
||||
// Create the agent and provide a factory to add our custom memory component to
|
||||
// all threads created by the agent. Here each new memory component will have its own
|
||||
// user info object, so each thread will have its own memory.
|
||||
// In real world applications/services, where the user info would be persisted in a database,
|
||||
// and preferably shared between multiple threads used by the same user, ensure that the
|
||||
// factory reads the user id from the current context and scopes the memory component
|
||||
// and its storage to that user id.
|
||||
AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions()
|
||||
{
|
||||
Instructions = "You are a friendly assistant. Always address the user by their name.",
|
||||
AIContextProviderFactory = () => new SampleApp.UserInfoMemory(chatClient.AsIChatClient())
|
||||
});
|
||||
|
||||
// Create a new thread for the conversation.
|
||||
AgentThread thread = agent.GetNewThread();
|
||||
|
||||
Console.WriteLine(">> Use thread with blank memory\n");
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
Console.WriteLine(await agent.RunAsync("Hello, what is the square root of 9?", thread));
|
||||
Console.WriteLine(await agent.RunAsync("My name is Ruaidhrí", thread));
|
||||
Console.WriteLine(await agent.RunAsync("I am 20 years old", thread));
|
||||
|
||||
// We can serialize the thread. The serialized state will include the state of the memory component.
|
||||
var threadElement = await thread.SerializeAsync();
|
||||
|
||||
Console.WriteLine("\n>> Use deserialized thread with previously created memories\n");
|
||||
|
||||
// Later we can deserialize the thread and continue the conversation with the previous memory component state.
|
||||
var deserializedThread = await agent.DeserializeThreadAsync(threadElement);
|
||||
Console.WriteLine(await agent.RunAsync("What is my name and age?", deserializedThread));
|
||||
|
||||
Console.WriteLine("\n>> Read memories from memory component\n");
|
||||
|
||||
// It's possible to access the memory component via the thread's AIContextProvider property.
|
||||
var userInfo = ((UserInfoMemory)deserializedThread.AIContextProvider!).UserInfo;
|
||||
|
||||
// Output the user info that was captured by the memory component.
|
||||
Console.WriteLine($"MEMORY - User Name: {userInfo.UserName}");
|
||||
Console.WriteLine($"MEMORY - User Age: {userInfo.UserAge}");
|
||||
|
||||
Console.WriteLine("\n>> Use new thread with previously created memories\n");
|
||||
|
||||
// Create a new thread.
|
||||
thread = agent.GetNewThread();
|
||||
|
||||
// It is also possible to add the memory component to an individual thread only instead of all
|
||||
// threads via the factory above.
|
||||
// In this case we will also use the same user info object, so this thread will share the same
|
||||
// memories as the previous thread.
|
||||
thread.AIContextProvider = new UserInfoMemory(chatClient.AsIChatClient(), userInfo);
|
||||
|
||||
// Invoke the agent and output the text result.
|
||||
// This time the agent should remember the user's name and use it in the response.
|
||||
Console.WriteLine(await agent.RunAsync("What is my name and age?", thread));
|
||||
|
||||
namespace SampleApp
|
||||
{
|
||||
/// <summary>
|
||||
/// Sample memory component that can remember a user's name and age.
|
||||
/// </summary>
|
||||
internal sealed class UserInfoMemory(IChatClient chatClient, UserInfo? userInfo = null) : AIContextProvider
|
||||
{
|
||||
public UserInfo UserInfo { get; set; } = userInfo ?? new();
|
||||
|
||||
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Try and extract the user name and age from the message if we don't have it already and it's a user message.
|
||||
if ((this.UserInfo.UserName == null || this.UserInfo.UserAge == null) && context.RequestMessages.Any(x => x.Role == ChatRole.User))
|
||||
{
|
||||
var result = await chatClient.GetResponseAsync<UserInfo>(
|
||||
context.RequestMessages,
|
||||
new ChatOptions()
|
||||
{
|
||||
Instructions = "Extract the user's name and age from the message if present. If not present return nulls."
|
||||
},
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
this.UserInfo.UserName ??= result.Result.UserName;
|
||||
this.UserInfo.UserAge ??= result.Result.UserAge;
|
||||
}
|
||||
}
|
||||
|
||||
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
StringBuilder instructions = new();
|
||||
|
||||
// If we don't already know the user's name and age, add instructions to ask for them, otherwise just provide what we have to the context.
|
||||
instructions.AppendLine(
|
||||
this.UserInfo.UserName == null ?
|
||||
"Ask the user for their name and politely decline to answer any questions until they provide it." :
|
||||
$"The user's name is {this.UserInfo.UserName}.");
|
||||
|
||||
instructions.AppendLine(
|
||||
this.UserInfo.UserAge == null ?
|
||||
"Ask the user for their age and politely decline to answer any questions until they provide it." :
|
||||
$"The user's age is {this.UserInfo.UserAge}.");
|
||||
|
||||
return new ValueTask<AIContext>(new AIContext
|
||||
{
|
||||
Instructions = instructions.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
public override ValueTask<JsonElement?> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ValueTask<JsonElement?>(JsonSerializer.SerializeToElement(this.UserInfo, jsonSerializerOptions));
|
||||
}
|
||||
|
||||
public override ValueTask DeserializeAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.UserInfo = JsonSerializer.Deserialize<UserInfo>(serializedState, jsonSerializerOptions) ?? new UserInfo();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UserInfo
|
||||
{
|
||||
public string? UserName { get; set; }
|
||||
public int? UserAge { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,8 @@ Before you begin, ensure you have the following prerequisites:
|
||||
|[Dependency injection with a simple agent](./Agent_Step09_DependencyInjection/)|This sample demonstrates how to add and resolve an agent with a dependency injection container|
|
||||
|[Exposing a simple agent as MCP tool](./Agent_Step10_AsMcpTool/)|This sample demonstrates how to expose an agent as an MCP tool|
|
||||
|[Using images with a simple agent](./Agent_Step11_UsingImages/)|This sample demonstrates how to use image multi-modality with an AI agent|
|
||||
|[Exposing a simple agent a function tool](./Agent_Step12_AsFunctionTool/)|This sample demonstrates how to expose an agent as a function tool|
|
||||
|[Exposing a simple agent as a function tool](./Agent_Step12_AsFunctionTool/)|This sample demonstrates how to expose an agent as a function tool|
|
||||
|[Using memory with an agent](./Agent_Step12_Memory/)|This sample demonstrates how to create a simple memory component and use it with an agent|
|
||||
|
||||
## Running the samples from the console
|
||||
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma warning disable SA1623 // Property summary documentation should match accessors
|
||||
|
||||
namespace System.Runtime.CompilerServices;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that compiler support for a particular feature is required for the location where this attribute is applied.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
|
||||
internal sealed class CompilerFeatureRequiredAttribute : Attribute
|
||||
{
|
||||
public CompilerFeatureRequiredAttribute(string featureName)
|
||||
{
|
||||
FeatureName = featureName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the compiler feature.
|
||||
/// </summary>
|
||||
public string FeatureName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand <see cref="FeatureName"/>.
|
||||
/// </summary>
|
||||
public bool IsOptional { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="FeatureName"/> used for the ref structs C# feature.
|
||||
/// </summary>
|
||||
public const string RefStructs = nameof(RefStructs);
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="FeatureName"/> used for the required members C# feature.
|
||||
/// </summary>
|
||||
public const string RequiredMembers = nameof(RequiredMembers);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
Enables use of C# required members on older frameworks.
|
||||
|
||||
To use this source in your project, add the following to your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectCompilerFeatureRequiredOnLegacy>true</InjectCompilerFeatureRequiredOnLegacy>
|
||||
</PropertyGroup>
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
Enables use of C# required members on older frameworks.
|
||||
|
||||
To use this source in your project, add the following to your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectRequiredMemberOnLegacy>true</InjectRequiredMemberOnLegacy>
|
||||
</PropertyGroup>
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace System.Runtime.CompilerServices;
|
||||
|
||||
/// <summary>Specifies that a type has required members or that a member is required.</summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal sealed class RequiredMemberAttribute : Attribute;
|
||||
@@ -290,6 +290,6 @@ public abstract class AIAgent
|
||||
_ = Throw.IfNull(thread);
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
await thread.OnNewMessagesAsync(messages, cancellationToken).ConfigureAwait(false);
|
||||
await thread.MessagesReceivedAsync(messages, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// A class containing any context that should be provided to the AI model
|
||||
/// as supplied by an <see cref="AIContextProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Each <see cref="AIContextProvider"/> has the ability to provide its own context for each invocation.
|
||||
/// The <see cref="AIContext"/> class contains the additional context supplied by the <see cref="AIContextProvider"/>.
|
||||
/// This context will be combined with context supplied by other providers before being passed to the AI model.
|
||||
/// </remarks>
|
||||
public sealed class AIContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets any instructions to pass to the AI model in addition to any other prompts
|
||||
/// that it may already have (in the case of an agent), or chat history that may
|
||||
/// already exist.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These instructions will be transient and only apply to the current invocation.
|
||||
/// </remarks>
|
||||
public string? Instructions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of messages to add to the chat history.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These messages will permanently be added to the chat history.
|
||||
/// </remarks>
|
||||
public IList<ChatMessage>? Messages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of functions/tools to make available to the AI model for the current invocation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These functions/tools will be transient and only apply to the current invocation.
|
||||
/// </remarks>
|
||||
public IList<AITool>? Tools { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all AI context providers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An AI context provider is a component that can be used to enhance the AI's context management.
|
||||
/// It can listen to changes in the conversation, provide additional context to
|
||||
/// the Model/Agent/etc. just before invocation and supply additional function tools.
|
||||
/// </remarks>
|
||||
public abstract class AIContextProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Called just before the Model/Agent/etc. is invoked
|
||||
/// Implementers can load any additional context required at this time,
|
||||
/// and they should return any context that should be passed to the Model/Agent/etc.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the event context.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that completes when the context has been rendered and returned.</returns>
|
||||
public abstract ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Called just before the Model/Agent/etc. is invoked
|
||||
/// Implementers can load any additional context required at this time,
|
||||
/// and they should return any context that should be passed to the Model/Agent/etc.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the event context.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that completes when the context has been rendered and returned.</returns>
|
||||
public virtual ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
|
||||
/// </summary>
|
||||
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
|
||||
public virtual ValueTask<JsonElement?> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the state contained in the provided <see cref="JsonElement"/> into the properties on this object.
|
||||
/// </summary>
|
||||
/// <param name="serializedState">A <see cref="JsonElement"/> representing the state of the object.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> that completes when the state has been deserialized.</returns>
|
||||
public virtual ValueTask DeserializeAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the event context provided to <see cref="AIContextProvider.InvokingAsync(InvokingContext, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
public class InvokingContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokingContext"/> class.
|
||||
/// </summary>
|
||||
/// <param name="requestMessages">The messages to be sent to the Model/Agent/etc. for this invocation.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="requestMessages"/> is <see langword="null"/>.</exception>
|
||||
public InvokingContext(IEnumerable<ChatMessage> requestMessages)
|
||||
{
|
||||
RequestMessages = requestMessages ?? throw new ArgumentNullException(nameof(requestMessages));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the messages that will be sent to the Model/Agent/etc. for this invocation.
|
||||
/// </summary>
|
||||
public IEnumerable<ChatMessage> RequestMessages { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the event conext provided to <see cref="AIContextProvider.InvokedAsync(InvokedContext, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
public class InvokedContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvokedContext"/> class with the specified request messages.
|
||||
/// </summary>
|
||||
/// <param name="requestMessages">The messages that were sent to the Model/Agent/etc. for this invocation.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="requestMessages"/> is <see langword="null"/>.</exception>
|
||||
public InvokedContext(IEnumerable<ChatMessage> requestMessages)
|
||||
{
|
||||
RequestMessages = requestMessages ?? throw new ArgumentNullException(nameof(requestMessages));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the messages that were sent to the Model/Agent/etc. for this invocation.
|
||||
/// </summary>
|
||||
public IEnumerable<ChatMessage> RequestMessages { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of response messages generated by Model/Agent/etc. if the invocation succeeded.
|
||||
/// </summary>
|
||||
public IEnumerable<ChatMessage>? ResponseMessages { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Exception"/> that was thrown during the invocation, if the invocation failed.
|
||||
/// </summary>
|
||||
public Exception? InvokeException { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ public class AgentThread
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id of the current thread to support cases where the thread is owned by the agent service.
|
||||
/// Gets or sets the ID of the underlying service thread to support cases where the chat history is stored by the agent service.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -108,6 +108,11 @@ public class AgentThread
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="AIContextProvider"/> used by this thread to provide additional context to the AI model before each invocation.
|
||||
/// </summary>
|
||||
public AIContextProvider? AIContextProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
|
||||
/// </summary>
|
||||
@@ -120,10 +125,15 @@ public class AgentThread
|
||||
null :
|
||||
await this._messageStore.SerializeStateAsync(jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var aiContextProviderState = this.AIContextProvider is null ?
|
||||
null :
|
||||
await this.AIContextProvider.SerializeAsync(jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var state = new ThreadState
|
||||
{
|
||||
ConversationId = this.ConversationId,
|
||||
StoreState = storeState
|
||||
StoreState = storeState,
|
||||
AIContextProviderState = aiContextProviderState
|
||||
};
|
||||
|
||||
return JsonSerializer.SerializeToElement(state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ThreadState)));
|
||||
@@ -139,7 +149,7 @@ public class AgentThread
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that completes when the context has been updated.</returns>
|
||||
/// <exception cref="InvalidOperationException">The thread has been deleted.</exception>
|
||||
protected internal virtual async Task OnNewMessagesAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken = default)
|
||||
protected internal virtual async Task MessagesReceivedAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken = default)
|
||||
{
|
||||
switch (this)
|
||||
{
|
||||
@@ -186,6 +196,11 @@ public class AgentThread
|
||||
return;
|
||||
}
|
||||
|
||||
if (state?.AIContextProviderState.HasValue is true && this.AIContextProvider is not null)
|
||||
{
|
||||
await this.AIContextProvider.DeserializeAsync(state.AIContextProviderState.Value, jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// If we don't have any IChatMessageStore state return here.
|
||||
if (state?.StoreState is null || state?.StoreState.Value.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
|
||||
{
|
||||
@@ -206,5 +221,7 @@ public class AgentThread
|
||||
public string? ConversationId { get; set; }
|
||||
|
||||
public JsonElement? StoreState { get; set; }
|
||||
|
||||
public JsonElement? AIContextProviderState { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -12,6 +12,9 @@
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
|
||||
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectRequiredMemberOnLegacy>true</InjectRequiredMemberOnLegacy>
|
||||
<InjectCompilerFeatureRequiredOnLegacy>true</InjectCompilerFeatureRequiredOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ClientModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace OpenAI.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for working with <see cref="ClientResult{Assistant}"/> where T is <see cref="Assistant"/>.
|
||||
/// </summary>
|
||||
public static class AssistantExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="ClientResult{Assistant}"/> to a <see cref="ChatClientAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="assistantClientResult">The client result containing the assistant.</param>
|
||||
/// <param name="assistantClient">The assistant client.</param>
|
||||
/// <param name="chatOptions">Optional chat options.</param>
|
||||
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the assistant.</returns>
|
||||
public static ChatClientAgent AsAIAgent(this ClientResult<Assistant> assistantClientResult, AssistantClient assistantClient, ChatOptions? chatOptions = null)
|
||||
{
|
||||
if (assistantClientResult is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assistantClientResult));
|
||||
}
|
||||
|
||||
return AssistantExtensions.AsAIAgent(assistantClientResult, assistantClient, chatOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an <see cref="Assistant"/> to a <see cref="ChatClientAgent"/>.
|
||||
/// </summary>
|
||||
/// <param name="assistantMetadata">The assistant metadata.</param>
|
||||
/// <param name="assistantClient">The assistant client.</param>
|
||||
/// <param name="chatOptions">Optional chat options.</param>
|
||||
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the assistant.</returns>
|
||||
public static ChatClientAgent AsAIAgent(this Assistant assistantMetadata, AssistantClient assistantClient, ChatOptions? chatOptions = null)
|
||||
{
|
||||
if (assistantMetadata is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assistantMetadata));
|
||||
}
|
||||
if (assistantClient is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assistantClient));
|
||||
}
|
||||
|
||||
#pragma warning disable CA2000 // Dispose objects before losing scope
|
||||
var chatClient = assistantClient.AsIChatClient(assistantMetadata.Id);
|
||||
#pragma warning restore CA2000 // Dispose objects before losing scope
|
||||
|
||||
return new ChatClientAgent(chatClient, options: new()
|
||||
{
|
||||
Id = assistantMetadata.Id,
|
||||
Name = assistantMetadata.Name,
|
||||
Description = assistantMetadata.Description,
|
||||
Instructions = assistantMetadata.Instructions,
|
||||
ChatOptions = chatOptions
|
||||
});
|
||||
}
|
||||
}
|
||||
+57
@@ -20,6 +20,63 @@ namespace OpenAI;
|
||||
/// </remarks>
|
||||
public static class OpenAIAssistantClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AssistantClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="assistantClient">The <see cref="AssistantClient"/> to create the <see cref="ChatClientAgent"/> with.</param>
|
||||
/// <param name="agentId">The ID of the server side agent to create a <see cref="ChatClientAgent"/> for.</param>
|
||||
/// <param name="chatOptions">Options that should apply to all runs of the agent.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the assistant agent.</returns>
|
||||
public static ChatClientAgent GetAIAgent(
|
||||
this AssistantClient assistantClient,
|
||||
string agentId,
|
||||
ChatOptions? chatOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (assistantClient is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assistantClient));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(agentId))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId));
|
||||
}
|
||||
|
||||
var assistant = assistantClient.GetAssistant(agentId, cancellationToken);
|
||||
return assistant.AsAIAgent(assistantClient, chatOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AssistantClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="assistantClient">The <see cref="AssistantClient"/> to create the <see cref="ChatClientAgent"/> with.</param>
|
||||
/// <param name="agentId"> The ID of the server side agent to create a <see cref="ChatClientAgent"/> for.</param>
|
||||
/// <param name="chatOptions">Options that should apply to all runs of the agent.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the assistant agent.</returns>
|
||||
public static async Task<ChatClientAgent> GetAIAgentAsync(
|
||||
this AssistantClient assistantClient,
|
||||
string agentId,
|
||||
ChatOptions? chatOptions = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (assistantClient is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(assistantClient));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(agentId))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId));
|
||||
}
|
||||
|
||||
var assistanceResponse = await assistantClient.GetAssistantAsync(agentId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return assistanceResponse.AsAIAgent(assistantClient, chatOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an AI agent from an <see cref="AssistantClient"/> using the OpenAI Assistant API.
|
||||
/// </summary>
|
||||
|
||||
@@ -114,7 +114,17 @@ public sealed class ChatClientAgent : AIAgent
|
||||
|
||||
this._logger.LogAgentChatClientInvokingAgent(nameof(RunAsync), this.Id, agentName, this._chatClientType);
|
||||
|
||||
ChatResponse chatResponse = await this.ChatClient.GetResponseAsync(threadMessages, chatOptions, cancellationToken).ConfigureAwait(false);
|
||||
// Call the IChatClient and notify the AIContextProvider of any failures.
|
||||
ChatResponse chatResponse;
|
||||
try
|
||||
{
|
||||
chatResponse = await this.ChatClient.GetResponseAsync(threadMessages, chatOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, agentName, this._chatClientType, inputMessages.Count);
|
||||
|
||||
@@ -122,19 +132,17 @@ public sealed class ChatClientAgent : AIAgent
|
||||
// so let's update it and set the conversation id for the service thread case.
|
||||
this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId);
|
||||
|
||||
// Only notify the thread of new messages if the chatResponse was successful to avoid inconsistent messages state in the thread.
|
||||
await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Ensure that the author name is set for each message in the response.
|
||||
foreach (ChatMessage chatResponseMessage in chatResponse.Messages)
|
||||
{
|
||||
chatResponseMessage.AuthorName ??= agentName;
|
||||
}
|
||||
|
||||
// Convert the chat response messages to a valid IReadOnlyCollection for notification signatures below.
|
||||
var chatResponseMessages = chatResponse.Messages as IReadOnlyCollection<ChatMessage> ?? [.. chatResponse.Messages];
|
||||
// Only notify the thread of new messages if the chatResponse was successful to avoid inconsistent message state in the thread.
|
||||
await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages.Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await NotifyThreadOfNewMessagesAsync(safeThread, chatResponseMessages, cancellationToken).ConfigureAwait(false);
|
||||
// Notify the AIContextProvider of all new messages.
|
||||
await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new(chatResponse) { AgentId = this.Id };
|
||||
}
|
||||
@@ -156,15 +164,34 @@ public sealed class ChatClientAgent : AIAgent
|
||||
|
||||
this._logger.LogAgentChatClientInvokingAgent(nameof(RunStreamingAsync), this.Id, loggingAgentName, this._chatClientType);
|
||||
|
||||
// Using the enumerator to ensure we consider the case where no updates are returned for notification.
|
||||
var responseUpdatesEnumerator = this.ChatClient.GetStreamingResponseAsync(threadMessages, chatOptions, cancellationToken).GetAsyncEnumerator(cancellationToken);
|
||||
List<ChatResponseUpdate> responseUpdates = [];
|
||||
|
||||
IAsyncEnumerator<ChatResponseUpdate> responseUpdatesEnumerator;
|
||||
|
||||
try
|
||||
{
|
||||
// Using the enumerator to ensure we consider the case where no updates are returned for notification.
|
||||
responseUpdatesEnumerator = this.ChatClient.GetStreamingResponseAsync(threadMessages, chatOptions, cancellationToken).GetAsyncEnumerator(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
this._logger.LogAgentChatClientInvokedStreamingAgent(nameof(RunStreamingAsync), this.Id, loggingAgentName, this._chatClientType);
|
||||
|
||||
List<ChatResponseUpdate> responseUpdates = [];
|
||||
|
||||
// Ensure we start the streaming request
|
||||
var hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false);
|
||||
bool hasUpdates;
|
||||
try
|
||||
{
|
||||
// Ensure we start the streaming request
|
||||
hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
while (hasUpdates)
|
||||
{
|
||||
@@ -176,20 +203,28 @@ public sealed class ChatClientAgent : AIAgent
|
||||
yield return new(update) { AgentId = this.Id };
|
||||
}
|
||||
|
||||
hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
hasUpdates = await responseUpdatesEnumerator.MoveNextAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await NotifyAIContextProviderOfFailureAsync(safeThread, ex, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
var chatResponse = responseUpdates.ToChatResponse();
|
||||
var chatResponseMessages = chatResponse.Messages as IReadOnlyCollection<ChatMessage> ?? [.. chatResponse.Messages];
|
||||
|
||||
// We can derive the type of supported thread from whether we have a conversation id,
|
||||
// so let's update it and set the conversation id for the service thread case.
|
||||
this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId);
|
||||
|
||||
// To avoid inconsistent state we only notify the thread of the input messages if no error occurs after the initial request.
|
||||
await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages, cancellationToken).ConfigureAwait(false);
|
||||
await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages.Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await NotifyThreadOfNewMessagesAsync(safeThread, chatResponseMessages, cancellationToken).ConfigureAwait(false);
|
||||
// Notify the AIContextProvider of all new messages.
|
||||
await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -204,12 +239,40 @@ public sealed class ChatClientAgent : AIAgent
|
||||
/// <inheritdoc/>
|
||||
public override AgentThread GetNewThread()
|
||||
{
|
||||
var thread = new AgentThread { MessageStore = this._agentOptions?.ChatMessageStoreFactory?.Invoke() };
|
||||
var thread = new AgentThread
|
||||
{
|
||||
MessageStore = this._agentOptions?.ChatMessageStoreFactory?.Invoke(),
|
||||
AIContextProvider = this._agentOptions?.AIContextProviderFactory?.Invoke()
|
||||
};
|
||||
return thread;
|
||||
}
|
||||
|
||||
#region Private
|
||||
|
||||
/// <summary>
|
||||
/// Notify the <see cref="AIContextProvider"/> when an agent run succeeded, if there is an <see cref="AIContextProvider"/>.
|
||||
/// </summary>
|
||||
private static async Task NotifyAIContextProviderOfSuccessAsync(AgentThread thread, IEnumerable<ChatMessage> inputMessages, IEnumerable<ChatMessage> responseMessages, CancellationToken cancellationToken)
|
||||
{
|
||||
if (thread.AIContextProvider is not null)
|
||||
{
|
||||
await thread.AIContextProvider.InvokedAsync(new(inputMessages) { ResponseMessages = responseMessages },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notify the <see cref="AIContextProvider"/> of any failure during an agent run, if there is an <see cref="AIContextProvider"/>.
|
||||
/// </summary>
|
||||
private static async Task NotifyAIContextProviderOfFailureAsync(AgentThread thread, Exception ex, IEnumerable<ChatMessage> inputMessages, CancellationToken cancellationToken)
|
||||
{
|
||||
if (thread.AIContextProvider is not null)
|
||||
{
|
||||
await thread.AIContextProvider.InvokedAsync(new(inputMessages) { InvokeException = ex },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures and returns chat options by merging the provided run options with the agent's default chat options.
|
||||
/// </summary>
|
||||
@@ -350,6 +413,34 @@ public sealed class ChatClientAgent : AIAgent
|
||||
threadMessages.AddRange(await thread.MessageStore.GetMessagesAsync(cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
// If we have an AIContextProvider, we should get context from it, and update our
|
||||
// messages and options with the additional context.
|
||||
if (thread.AIContextProvider is not null)
|
||||
{
|
||||
var invokingContext = new AIContextProvider.InvokingContext(inputMessages);
|
||||
var aiContext = await thread.AIContextProvider.InvokingAsync(invokingContext, cancellationToken).ConfigureAwait(false);
|
||||
if (aiContext.Messages is { Count: > 0 })
|
||||
{
|
||||
threadMessages.AddRange(aiContext.Messages);
|
||||
}
|
||||
|
||||
if (aiContext.Tools is { Count: > 0 })
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.Tools ??= [];
|
||||
foreach (AITool tool in aiContext.Tools)
|
||||
{
|
||||
chatOptions.Tools.Add(tool);
|
||||
}
|
||||
}
|
||||
|
||||
if (aiContext.Instructions is not null)
|
||||
{
|
||||
chatOptions ??= new();
|
||||
chatOptions.Instructions = string.IsNullOrWhiteSpace(chatOptions.Instructions) ? aiContext.Instructions : $"{chatOptions.Instructions}\n{aiContext.Instructions}";
|
||||
}
|
||||
}
|
||||
|
||||
// Add the input messages to the end of thread messages.
|
||||
threadMessages.AddRange(inputMessages);
|
||||
|
||||
|
||||
@@ -80,6 +80,13 @@ public class ChatClientAgentOptions
|
||||
/// </summary>
|
||||
public Func<IChatMessageStore>? ChatMessageStoreFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a factory function to create an instance of <see cref="AIContextProvider"/>
|
||||
/// which will be used to create a context provider for each new thread, and can then
|
||||
/// provide additional context for each agent run.
|
||||
/// </summary>
|
||||
public Func<AIContextProvider>? AIContextProviderFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use the provided <see cref="IChatClient"/> instance as is,
|
||||
/// without applying any default decorators.
|
||||
@@ -105,6 +112,7 @@ public class ChatClientAgentOptions
|
||||
Instructions = this.Instructions,
|
||||
Description = this.Description,
|
||||
ChatOptions = this.ChatOptions?.Clone(),
|
||||
ChatMessageStoreFactory = this.ChatMessageStoreFactory
|
||||
ChatMessageStoreFactory = this.ChatMessageStoreFactory,
|
||||
AIContextProviderFactory = this.AIContextProviderFactory,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ public class AIAgentTests
|
||||
|
||||
await MockAgent.NotifyThreadOfNewMessagesAsync(threadMock.Object, messages, cancellationToken);
|
||||
|
||||
threadMock.Protected().Verify("OnNewMessagesAsync", Times.Once(), messages, cancellationToken);
|
||||
threadMock.Protected().Verify("MessagesReceivedAsync", Times.Once(), messages, cancellationToken);
|
||||
}
|
||||
|
||||
#region GetService Method Tests
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests;
|
||||
|
||||
public class AIContextProviderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task InvokedAsync_ReturnsCompletedTaskAsync()
|
||||
{
|
||||
var provider = new TestAIContextProvider();
|
||||
var messages = new ReadOnlyCollection<ChatMessage>(new List<ChatMessage>());
|
||||
var task = provider.InvokedAsync(new(messages));
|
||||
Assert.Equal(default, task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SerializeAsync_ReturnsEmptyElementAsync()
|
||||
{
|
||||
var provider = new TestAIContextProvider();
|
||||
var actual = await provider.SerializeAsync();
|
||||
Assert.Equal(default, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeAsync_ReturnsCompletedTaskAsync()
|
||||
{
|
||||
var provider = new TestAIContextProvider();
|
||||
var element = default(JsonElement);
|
||||
var actual = provider.DeserializeAsync(element);
|
||||
Assert.Equal(default, actual);
|
||||
}
|
||||
|
||||
private sealed class TestAIContextProvider : AIContextProvider
|
||||
{
|
||||
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
public override async ValueTask<JsonElement?> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await base.SerializeAsync(jsonSerializerOptions, cancellationToken);
|
||||
}
|
||||
|
||||
public override async ValueTask DeserializeAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await base.DeserializeAsync(serializedState, jsonSerializerOptions, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AIContext"/>.
|
||||
/// </summary>
|
||||
public class AIContextTests
|
||||
{
|
||||
[Fact]
|
||||
public void SetInstructionsRoundtrips()
|
||||
{
|
||||
var context = new AIContext
|
||||
{
|
||||
Instructions = "Test Instructions"
|
||||
};
|
||||
|
||||
Assert.Equal("Test Instructions", context.Instructions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetMessagesRoundtrips()
|
||||
{
|
||||
var context = new AIContext
|
||||
{
|
||||
Messages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
}
|
||||
};
|
||||
|
||||
Assert.NotNull(context.Messages);
|
||||
Assert.Equal(2, context.Messages.Count);
|
||||
Assert.Equal("Hello", context.Messages[0].Text);
|
||||
Assert.Equal("Hi there!", context.Messages[1].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetAIFunctionsRoundtrips()
|
||||
{
|
||||
var context = new AIContext
|
||||
{
|
||||
Tools = new List<AITool>
|
||||
{
|
||||
AIFunctionFactory.Create(() => "Function1", "Function1", "Description1"),
|
||||
AIFunctionFactory.Create(() => "Function2", "Function2", "Description2"),
|
||||
}
|
||||
};
|
||||
|
||||
Assert.NotNull(context.Tools);
|
||||
Assert.Equal(2, context.Tools.Count);
|
||||
Assert.Equal("Function1", context.Tools[0].Name);
|
||||
Assert.Equal("Function2", context.Tools[1].Name);
|
||||
}
|
||||
}
|
||||
+49
-2
@@ -8,6 +8,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
|
||||
#pragma warning disable CA1861 // Avoid constant arrays as arguments
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests;
|
||||
|
||||
public class AgentThreadTests
|
||||
@@ -102,7 +104,7 @@ public class AgentThreadTests
|
||||
};
|
||||
|
||||
// Act
|
||||
await thread.OnNewMessagesAsync(messages, CancellationToken.None);
|
||||
await thread.MessagesReceivedAsync(messages, CancellationToken.None);
|
||||
Assert.Equal("thread-123", thread.ConversationId);
|
||||
Assert.Null(thread.MessageStore);
|
||||
}
|
||||
@@ -120,7 +122,7 @@ public class AgentThreadTests
|
||||
};
|
||||
|
||||
// Act
|
||||
await thread.OnNewMessagesAsync(messages, CancellationToken.None);
|
||||
await thread.MessagesReceivedAsync(messages, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, store.Count);
|
||||
@@ -173,6 +175,26 @@ public class AgentThreadTests
|
||||
Assert.Null(thread.MessageStore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyDeserializeWithAIContextProviderAsync()
|
||||
{
|
||||
// Arrange
|
||||
var json = JsonSerializer.Deserialize("""
|
||||
{
|
||||
"aiContextProviderState": ["CP1"]
|
||||
}
|
||||
""", TestJsonSerializerContext.Default.JsonElement);
|
||||
Mock<AIContextProvider> mockProvider = new();
|
||||
var thread = new AgentThread() { AIContextProvider = mockProvider.Object };
|
||||
|
||||
// Act
|
||||
await thread.DeserializeAsync(json);
|
||||
|
||||
// Assert
|
||||
Assert.Null(thread.MessageStore);
|
||||
mockProvider.Verify(m => m.DeserializeAsync(It.Is<JsonElement>(e => e.ValueKind == JsonValueKind.Array && e.GetArrayLength() == 1), It.IsAny<JsonSerializerOptions?>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeWithInvalidJsonThrowsAsync()
|
||||
{
|
||||
@@ -245,6 +267,31 @@ public class AgentThreadTests
|
||||
Assert.Equal("TestContent", textContent.GetProperty("text").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyThreadSerializationWithWithAIContextProviderAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<AIContextProvider> mockProvider = new();
|
||||
var providerStateElement = JsonSerializer.SerializeToElement(new[] { "CP1" }, TestJsonSerializerContext.Default.StringArray);
|
||||
mockProvider
|
||||
.Setup(m => m.SerializeAsync(It.IsAny<JsonSerializerOptions?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(providerStateElement);
|
||||
|
||||
var thread = new AgentThread();
|
||||
thread.AIContextProvider = mockProvider.Object;
|
||||
|
||||
// Act
|
||||
var json = await thread.SerializeAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(JsonValueKind.Object, json.ValueKind);
|
||||
Assert.True(json.TryGetProperty("aiContextProviderState", out var providerStateProperty));
|
||||
Assert.Equal(JsonValueKind.Array, providerStateProperty.ValueKind);
|
||||
Assert.Single(providerStateProperty.EnumerateArray());
|
||||
Assert.Equal("CP1", providerStateProperty.EnumerateArray().First().GetString());
|
||||
mockProvider.Verify(m => m.SerializeAsync(It.IsAny<JsonSerializerOptions?>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify thread serialization to JSON with custom options.
|
||||
/// </summary>
|
||||
|
||||
+1
@@ -17,4 +17,5 @@ namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests;
|
||||
[JsonSerializable(typeof(Animal))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object?>))]
|
||||
[JsonSerializable(typeof(string[]))]
|
||||
internal sealed partial class TestJsonSerializerContext : JsonSerializerContext;
|
||||
|
||||
+8
-1
@@ -22,6 +22,7 @@ public class ChatClientAgentOptionsTests
|
||||
Assert.Null(options.Description);
|
||||
Assert.Null(options.ChatOptions);
|
||||
Assert.Null(options.ChatMessageStoreFactory);
|
||||
Assert.Null(options.AIContextProviderFactory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -39,6 +40,7 @@ public class ChatClientAgentOptionsTests
|
||||
Assert.Null(options.Instructions);
|
||||
Assert.Null(options.Description);
|
||||
Assert.Null(options.ChatOptions);
|
||||
Assert.Null(options.AIContextProviderFactory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -163,11 +165,13 @@ public class ChatClientAgentOptionsTests
|
||||
const string Description = "Test description";
|
||||
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
|
||||
static IChatMessageStore ChatMessageStoreFactory() => new Mock<IChatMessageStore>().Object;
|
||||
static AIContextProvider AIContextProviderFactory() => new Mock<AIContextProvider>().Object;
|
||||
|
||||
var original = new ChatClientAgentOptions(Instructions, Name, Description, tools)
|
||||
{
|
||||
Id = "test-id",
|
||||
ChatMessageStoreFactory = ChatMessageStoreFactory
|
||||
ChatMessageStoreFactory = ChatMessageStoreFactory,
|
||||
AIContextProviderFactory = AIContextProviderFactory
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -180,6 +184,7 @@ public class ChatClientAgentOptionsTests
|
||||
Assert.Equal(original.Instructions, clone.Instructions);
|
||||
Assert.Equal(original.Description, clone.Description);
|
||||
Assert.Same(original.ChatMessageStoreFactory, clone.ChatMessageStoreFactory);
|
||||
Assert.Same(original.AIContextProviderFactory, clone.AIContextProviderFactory);
|
||||
|
||||
// ChatOptions should be cloned, not the same reference
|
||||
Assert.NotSame(original.ChatOptions, clone.ChatOptions);
|
||||
@@ -209,5 +214,7 @@ public class ChatClientAgentOptionsTests
|
||||
Assert.Equal(original.Instructions, clone.Instructions);
|
||||
Assert.Equal(original.Description, clone.Description);
|
||||
Assert.Null(clone.ChatOptions);
|
||||
Assert.Null(clone.ChatMessageStoreFactory);
|
||||
Assert.Null(clone.AIContextProviderFactory);
|
||||
}
|
||||
}
|
||||
|
||||
+231
@@ -10,6 +10,8 @@ namespace Microsoft.Extensions.AI.Agents.UnitTests.ChatCompletion;
|
||||
|
||||
public class ChatClientAgentTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verify the invocation and response of <see cref="ChatClientAgent"/>.
|
||||
/// </summary>
|
||||
@@ -38,6 +40,10 @@ public class ChatClientAgentTests
|
||||
Assert.Equal("AgentInvokedChatClient", agent.ChatClient.GetType().Name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RunAsync Tests
|
||||
|
||||
/// <summary>
|
||||
/// Verify the invocation and response of <see cref="ChatClientAgent"/> using <see cref="IChatClient"/>.
|
||||
/// </summary>
|
||||
@@ -390,6 +396,176 @@ public class ChatClientAgentTests
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], thread));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that RunAsync sets the ConversationId on the thread when the service returns one.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsyncSetsConversationIdOnThreadWhenReturnedByChatClientAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
mockService.Setup(
|
||||
s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
|
||||
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "test instructions" });
|
||||
AgentThread thread = new();
|
||||
|
||||
// Act
|
||||
await agent.RunAsync([new(ChatRole.User, "test")], thread);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("ConvId", thread.ConversationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that RunAsync invokes any provided AIContextProvider and uses the result.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsyncInvokesAIContextProviderAndUsesResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessage[] requestMessages = [new(ChatRole.User, "user message")];
|
||||
ChatMessage[] responseMessages = [new(ChatRole.Assistant, "response")];
|
||||
Mock<IChatClient> mockService = new();
|
||||
List<ChatMessage> capturedMessages = [];
|
||||
string capturedInstructions = string.Empty;
|
||||
List<AITool> capturedTools = [];
|
||||
mockService
|
||||
.Setup(s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
{
|
||||
capturedMessages.AddRange(msgs);
|
||||
capturedInstructions = opts.Instructions ?? string.Empty;
|
||||
if (opts.Tools != null)
|
||||
{
|
||||
capturedTools.AddRange(opts.Tools);
|
||||
}
|
||||
})
|
||||
.ReturnsAsync(new ChatResponse(responseMessages));
|
||||
|
||||
var mockProvider = new Mock<AIContextProvider>();
|
||||
mockProvider
|
||||
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AIContext
|
||||
{
|
||||
Messages = [new(ChatRole.System, "context provider message")],
|
||||
Instructions = "context provider instructions",
|
||||
Tools = [AIFunctionFactory.Create(() => { }, "context provider function")]
|
||||
});
|
||||
mockProvider
|
||||
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(new ValueTask());
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = () => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
|
||||
|
||||
// Act
|
||||
await agent.RunAsync(requestMessages);
|
||||
|
||||
// Assert
|
||||
// Should contain: base instructions, context message, user message, base function, context function
|
||||
Assert.Equal(2, capturedMessages.Count);
|
||||
Assert.Equal("base instructions\ncontext provider instructions", capturedInstructions);
|
||||
Assert.Equal("context provider message", capturedMessages[0].Text);
|
||||
Assert.Equal(ChatRole.System, capturedMessages[0].Role);
|
||||
Assert.Equal("user message", capturedMessages[1].Text);
|
||||
Assert.Equal(ChatRole.User, capturedMessages[1].Role);
|
||||
Assert.Equal(2, capturedTools.Count);
|
||||
Assert.Contains(capturedTools, t => t.Name == "base function");
|
||||
Assert.Contains(capturedTools, t => t.Name == "context provider function");
|
||||
mockProvider.Verify(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
mockProvider.Verify(p => p.InvokedAsync(It.Is<AIContextProvider.InvokedContext>(x => x.RequestMessages == requestMessages && x.ResponseMessages == responseMessages && x.InvokeException == null), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that RunAsync invokes any provided AIContextProvider when the downstream GetResponse call fails.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsyncInvokesAIContextProviderWhenGetResponseFailsAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessage[] requestMessages = [new(ChatRole.User, "user message")];
|
||||
ChatMessage[] responseMessages = [new(ChatRole.Assistant, "response")];
|
||||
Mock<IChatClient> mockService = new();
|
||||
mockService
|
||||
.Setup(s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Throws(new InvalidOperationException("downstream failure"));
|
||||
|
||||
var mockProvider = new Mock<AIContextProvider>();
|
||||
mockProvider
|
||||
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AIContext());
|
||||
mockProvider
|
||||
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(new ValueTask());
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = () => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(requestMessages));
|
||||
|
||||
// Assert
|
||||
mockProvider.Verify(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
mockProvider.Verify(p => p.InvokedAsync(It.Is<AIContextProvider.InvokedContext>(x => x.RequestMessages == requestMessages && x.ResponseMessages == null && x.InvokeException is InvalidOperationException), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that RunAsync invokes any provided AIContextProvider and succeeds even when the AIContext is empty.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunAsyncInvokesAIContextProviderAndSucceedsWithEmptyAIContextAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockService = new();
|
||||
List<ChatMessage> capturedMessages = [];
|
||||
string capturedInstructions = string.Empty;
|
||||
List<AITool> capturedTools = [];
|
||||
mockService
|
||||
.Setup(s => s.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
|
||||
{
|
||||
capturedMessages.AddRange(msgs);
|
||||
capturedInstructions = opts.Instructions ?? string.Empty;
|
||||
if (opts.Tools != null)
|
||||
{
|
||||
capturedTools.AddRange(opts.Tools);
|
||||
}
|
||||
})
|
||||
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
|
||||
|
||||
var mockProvider = new Mock<AIContextProvider>();
|
||||
mockProvider
|
||||
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AIContext());
|
||||
|
||||
ChatClientAgent agent = new(mockService.Object, options: new() { Instructions = "base instructions", AIContextProviderFactory = () => mockProvider.Object, ChatOptions = new() { Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
|
||||
|
||||
// Act
|
||||
await agent.RunAsync([new(ChatRole.User, "user message")]);
|
||||
|
||||
// Assert
|
||||
// Should contain: base instructions, user message, base function
|
||||
Assert.Single(capturedMessages);
|
||||
Assert.Equal("base instructions", capturedInstructions);
|
||||
Assert.Equal("user message", capturedMessages[0].Text);
|
||||
Assert.Equal(ChatRole.User, capturedMessages[0].Role);
|
||||
Assert.Single(capturedTools);
|
||||
Assert.Contains(capturedTools, t => t.Name == "base function");
|
||||
mockProvider.Verify(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Override Tests
|
||||
|
||||
/// <summary>
|
||||
@@ -1524,6 +1700,61 @@ public class ChatClientAgentTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetNewThread Tests
|
||||
|
||||
[Fact]
|
||||
public void GetNewThreadUsesChatMessageStoreFactoryIfProvided()
|
||||
{
|
||||
// Arrange
|
||||
var mockChatClient = new Mock<IChatClient>();
|
||||
var mockStore = new Mock<IChatMessageStore>();
|
||||
var factoryCalled = false;
|
||||
|
||||
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
|
||||
{
|
||||
Instructions = "Test instructions",
|
||||
ChatMessageStoreFactory = () =>
|
||||
{
|
||||
factoryCalled = true;
|
||||
return mockStore.Object;
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var thread = agent.GetNewThread();
|
||||
|
||||
// Assert
|
||||
Assert.True(factoryCalled, "ChatMessageStoreFactory was not called.");
|
||||
Assert.Same(mockStore.Object, thread.MessageStore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNewThreadUsesAIContextProviderFactoryIfProvided()
|
||||
{
|
||||
// Arrange
|
||||
var mockChatClient = new Mock<IChatClient>();
|
||||
var mockContextProvider = new Mock<AIContextProvider>();
|
||||
var factoryCalled = false;
|
||||
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
|
||||
{
|
||||
Instructions = "Test instructions",
|
||||
AIContextProviderFactory = () =>
|
||||
{
|
||||
factoryCalled = true;
|
||||
return mockContextProvider.Object;
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var thread = agent.GetNewThread();
|
||||
|
||||
// Assert
|
||||
Assert.True(factoryCalled, "AIContextProviderFactory was not called.");
|
||||
Assert.Same(mockContextProvider.Object, thread.AIContextProvider);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static async IAsyncEnumerable<T> ToAsyncEnumerableAsync<T>(IEnumerable<T> values)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.UnitTests;
|
||||
|
||||
[JsonSourceGenerationOptions(
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
UseStringEnumConverter = true)]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
internal sealed partial class TestJsonSerializerContext : JsonSerializerContext;
|
||||
Vendored
+9
@@ -11,6 +11,15 @@
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
"name": "Python Attach",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "localhost",
|
||||
"port": 5678
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+27
-9
@@ -550,6 +550,12 @@ uv run poe test
|
||||
|
||||
### Documentation
|
||||
|
||||
#### `docs-install`
|
||||
Install including the documentation tools:
|
||||
```bash
|
||||
uv run poe docs-install
|
||||
```
|
||||
|
||||
#### `docs-clean`
|
||||
Remove the docs build directory:
|
||||
```bash
|
||||
@@ -562,22 +568,34 @@ Build the documentation:
|
||||
uv run poe docs-build
|
||||
```
|
||||
|
||||
#### `docs-serve`
|
||||
Serve documentation locally with auto-reload:
|
||||
#### `docs-full`
|
||||
Build the packages, clean and build the documentation:
|
||||
```bash
|
||||
uv run poe docs-serve
|
||||
uv run poe docs-full
|
||||
```
|
||||
|
||||
#### `docs-check`
|
||||
Build documentation and fail on warnings:
|
||||
#### `docs-rebuild`
|
||||
Clean and build the documentation:
|
||||
```bash
|
||||
uv run poe docs-check
|
||||
uv run poe docs-rebuild
|
||||
```
|
||||
|
||||
#### `docs-check-examples`
|
||||
Check documentation examples for code correctness:
|
||||
#### `docs-full-install`
|
||||
Install the docs dependencies, build the packages, clean and build the documentation:
|
||||
```bash
|
||||
uv run poe docs-check-examples
|
||||
uv run poe docs-full-install
|
||||
```
|
||||
|
||||
#### `docs-debug`
|
||||
Build the documentation with debug information:
|
||||
```bash
|
||||
uv run poe docs-debug
|
||||
```
|
||||
|
||||
#### `docs-rebuild-debug`
|
||||
Clean and build the documentation with debug information:
|
||||
```bash
|
||||
uv run poe docs-rebuild-debug
|
||||
```
|
||||
|
||||
### Code Validation
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## Quick Install
|
||||
|
||||
```bash
|
||||
# Base package including workflow support
|
||||
pip install agent-framework
|
||||
# Optional: Add Azure integration
|
||||
pip install agent-framework[azure]
|
||||
|
||||
@@ -3,19 +3,35 @@
|
||||
"""Check code blocks in Markdown files for syntax errors."""
|
||||
|
||||
import argparse
|
||||
from enum import Enum
|
||||
import logging
|
||||
import tempfile
|
||||
import subprocess # nosec
|
||||
|
||||
from pygments import highlight # type: ignore
|
||||
from pygments.formatters import TerminalFormatter
|
||||
from pygments.lexers import PythonLexer
|
||||
from sphinx.util.console import darkgreen, darkred, faint, red, teal # type: ignore[attr-defined]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class Colors(str, Enum):
|
||||
CEND = "\33[0m"
|
||||
CRED = "\33[31m"
|
||||
CREDBG = "\33[41m"
|
||||
CGREEN = "\33[32m"
|
||||
CGREENBG = "\33[42m"
|
||||
CVIOLET = "\33[35m"
|
||||
CGREY = "\33[90m"
|
||||
|
||||
|
||||
def with_color(text: str, color: Colors) -> str:
|
||||
"""Prints a string with the specified color."""
|
||||
return f"{color.value}{text}{Colors.CEND.value}"
|
||||
|
||||
|
||||
def extract_python_code_blocks(markdown_file_path: str) -> list[tuple[str, int]]:
|
||||
"""Extract Python code blocks from a Markdown file."""
|
||||
with open(markdown_file_path, encoding="utf-8") as file:
|
||||
@@ -40,7 +56,7 @@ def extract_python_code_blocks(markdown_file_path: str) -> list[tuple[str, int]]
|
||||
|
||||
def check_code_blocks(markdown_file_paths: list[str]) -> None:
|
||||
"""Check Python code blocks in a Markdown file for syntax errors."""
|
||||
files_with_errors = []
|
||||
files_with_errors: list[str] = []
|
||||
|
||||
for markdown_file_path in markdown_file_paths:
|
||||
code_blocks = extract_python_code_blocks(markdown_file_path)
|
||||
@@ -54,7 +70,7 @@ def check_code_blocks(markdown_file_paths: list[str]) -> None:
|
||||
all(import_code not in code_block for import_code in [f"import {module}", f"from {module}"])
|
||||
for module in ["agent_framework"]
|
||||
):
|
||||
logger.info(" " + darkgreen("OK[ignored]"))
|
||||
logger.info(f' {with_color("OK[ignored]", Colors.CGREENBG)}')
|
||||
continue
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as temp_file:
|
||||
@@ -62,17 +78,25 @@ def check_code_blocks(markdown_file_paths: list[str]) -> None:
|
||||
temp_file.flush()
|
||||
|
||||
# Run pyright on the temporary file using subprocess.run
|
||||
import subprocess # nosec
|
||||
|
||||
result = subprocess.run(["pyright", temp_file.name], capture_output=True, text=True) # nosec
|
||||
if result.returncode != 0:
|
||||
logger.info(" " + darkred("FAIL"))
|
||||
highlighted_code = highlight(code_block, PythonLexer(), TerminalFormatter()) # type: ignore
|
||||
output = f"{faint('========================================================')}\n{red('Error')}: Pyright found issues in {teal(markdown_file_path_with_line_no)}:\n{faint('--------------------------------------------------------')}\n{highlighted_code}\n{faint('--------------------------------------------------------')}\n\n{teal('pyright output:')}\n{red(result.stdout)}{faint('========================================================')}\n"
|
||||
logger.info(output)
|
||||
logger.info(
|
||||
f" {with_color('FAIL', Colors.CREDBG)}\n"
|
||||
f"{with_color('========================================================', Colors.CGREY)}\n"
|
||||
f"{with_color('Error', Colors.CRED)}: Pyright found issues in {with_color(markdown_file_path_with_line_no, Colors.CVIOLET)}:\n"
|
||||
f"{with_color('--------------------------------------------------------', Colors.CGREY)}\n"
|
||||
f"{highlighted_code}\n"
|
||||
f"{with_color('--------------------------------------------------------', Colors.CGREY)}\n"
|
||||
"\n"
|
||||
f"{with_color('pyright output:', Colors.CVIOLET)}\n"
|
||||
f"{with_color(result.stdout, Colors.CRED)}"
|
||||
f"{with_color('========================================================', Colors.CGREY)}\n"
|
||||
)
|
||||
had_errors = True
|
||||
else:
|
||||
logger.info(" " + darkgreen("OK"))
|
||||
logger.info(f" {with_color('OK', Colors.CGREENBG)}")
|
||||
|
||||
if had_errors:
|
||||
files_with_errors.append(markdown_file_path)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
## Building the Agent Framework Documentation
|
||||
|
||||
Agent Framework documentation is based on the sphinx documentation system and uses the myst-parser to render markdown files. It uses the [pydata-sphinx-theme](https://pydata-sphinx-theme.readthedocs.io/en/latest/) to style the documentation.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Ensure you have all of the dev dependencies for the `agent-framework` package installed. You can install them by running the following command from the root of the `python` directory:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
## Building Docs
|
||||
|
||||
To build the documentation, run the following command from the root of the python repository:
|
||||
|
||||
```bash
|
||||
poe docs-build
|
||||
```
|
||||
|
||||
To serve the documentation locally, run the following command from the root of the python repository:
|
||||
|
||||
```bash
|
||||
poe --directory ./packages/autogen-core/ docs-serve
|
||||
```
|
||||
|
||||
[!NOTE]
|
||||
Sphinx will only rebuild files that have changed since the last build. If you want to force a full rebuild, you can run `poe docs-clean` before running the `docs-build` command.
|
||||
|
||||
## Versioning the Documentation
|
||||
|
||||
The current theme - [pydata-sphinx-theme](https://pydata-sphinx-theme.readthedocs.io/en/latest/) - supports [switching between versions](https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/version-dropdown.html) of the documentation.
|
||||
|
||||
To version the documentation, you need to create a new version of the documentation by copying the existing documentation to a new directory with the version number. For example, to create a new version of the documentation for version `0.1.0`, you would run the following command:
|
||||
|
||||
How are various versions built? - TBD.
|
||||
@@ -1,8 +0,0 @@
|
||||
{%- if show_headings %}
|
||||
{{- basename | e | heading }}
|
||||
|
||||
{% endif -%}
|
||||
.. automodule:: {{ qualname }}
|
||||
{%- for option in automodule_options %}
|
||||
:{{ option }}:
|
||||
{%- endfor %}
|
||||
@@ -1,53 +0,0 @@
|
||||
{%- macro automodule(modname, options) -%}
|
||||
.. automodule:: {{ modname }}
|
||||
{%- for option in options %}
|
||||
:{{ option }}:
|
||||
{%- endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{%- macro toctree(docnames) -%}
|
||||
.. toctree::
|
||||
:maxdepth: {{ maxdepth }}
|
||||
:hidden:
|
||||
{% for docname in docnames %}
|
||||
{{ docname }}
|
||||
{%- endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{%- if is_namespace %}
|
||||
{{- [pkgname, "namespace"] | join(" ") | e | heading }}
|
||||
{% else %}
|
||||
{{- pkgname | e | heading }}
|
||||
{% endif %}
|
||||
|
||||
{%- if is_namespace %}
|
||||
.. py:module:: {{ pkgname }}
|
||||
{% endif %}
|
||||
|
||||
{%- if modulefirst and not is_namespace %}
|
||||
{{ automodule(pkgname, automodule_options) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if subpackages %}
|
||||
|
||||
{{ toctree(subpackages) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if submodules %}
|
||||
|
||||
{% if separatemodules %}
|
||||
{{ toctree(submodules) }}
|
||||
{% else %}
|
||||
{%- for submodule in submodules %}
|
||||
{% if show_headings %}
|
||||
{{- [submodule, "module"] | join(" ") | e | heading(2) }}
|
||||
{% endif %}
|
||||
{{ automodule(submodule, automodule_options) }}
|
||||
{% endfor %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if not modulefirst and not is_namespace %}
|
||||
|
||||
{{ automodule(pkgname, automodule_options) }}
|
||||
{% endif %}
|
||||
@@ -1,98 +0,0 @@
|
||||
# Modified from: https://github.com/kai687/sphinxawesome-codelinter
|
||||
|
||||
import tempfile
|
||||
from collections.abc import Iterable
|
||||
from typing import AbstractSet, Any
|
||||
|
||||
from docutils import nodes
|
||||
from pygments import highlight # type: ignore
|
||||
from pygments.formatters import TerminalFormatter
|
||||
from pygments.lexers import PythonLexer
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.builders import Builder
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.console import darkgreen, darkred, faint, red, teal # type: ignore[attr-defined]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
|
||||
class CodeLinter(Builder):
|
||||
"""Iterate over all ``literal_block`` nodes.
|
||||
|
||||
pipe them into any command line tool that
|
||||
can read from standard input.
|
||||
"""
|
||||
|
||||
name = "code_lint"
|
||||
allow_parallel = True
|
||||
|
||||
def init(self) -> None:
|
||||
"""Initialize."""
|
||||
self._had_errors = False
|
||||
pass
|
||||
|
||||
def get_outdated_docs(self) -> str | Iterable[str]:
|
||||
"""Check for outdated files.
|
||||
|
||||
Return an iterable of outdated output files, or a string describing what an
|
||||
update will build.
|
||||
"""
|
||||
return self.env.found_docs
|
||||
|
||||
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
|
||||
"""Return Target URI for a document name."""
|
||||
return ""
|
||||
|
||||
def prepare_writing(self, docnames: AbstractSet[str]) -> None:
|
||||
"""Run these steps before documents are written."""
|
||||
return
|
||||
|
||||
def write_doc(self, docname: str, doctree: nodes.Node) -> None:
|
||||
path_prefix: str = self.app.config.code_lint_path_prefix
|
||||
supported_languages = {"python", "default"}
|
||||
|
||||
if not docname.startswith(path_prefix):
|
||||
return
|
||||
|
||||
for code in doctree.findall(nodes.literal_block):
|
||||
if code["language"] in supported_languages:
|
||||
logger.info("Checking a code block in %s...", docname, nonl=True)
|
||||
if "ignore" in code["classes"]:
|
||||
logger.info(" " + darkgreen("OK[ignored]"))
|
||||
continue
|
||||
|
||||
# Create a temporary file to store the code block
|
||||
with tempfile.NamedTemporaryFile(mode="wb", suffix=".py") as temp_file:
|
||||
temp_file.write(code.astext().encode())
|
||||
temp_file.flush()
|
||||
|
||||
# Run pyright on the temporary file using subprocess.run
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(["pyright", temp_file.name], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
logger.info(" " + darkred("FAIL"))
|
||||
highlighted_code = highlight(code.astext(), PythonLexer(), TerminalFormatter()) # type: ignore
|
||||
output = f"{faint('========================================================')}\n{red('Error')}: Pyright found issues in {teal(docname)}:\n{faint('--------------------------------------------------------')}\n{highlighted_code}\n{faint('--------------------------------------------------------')}\n\n{teal('pyright output:')}\n{red(result.stdout)}{faint('========================================================')}\n"
|
||||
logger.info(output)
|
||||
self._had_errors = True
|
||||
else:
|
||||
logger.info(" " + darkgreen("OK"))
|
||||
|
||||
def finish(self) -> None:
|
||||
"""Finish the build process."""
|
||||
if self._had_errors:
|
||||
raise RuntimeError("Code linting failed - see earlier output")
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> dict[str, Any]:
|
||||
app.add_builder(CodeLinter)
|
||||
app.add_config_value("code_lint_path_prefix", "", "env")
|
||||
|
||||
return {
|
||||
"version": __version__,
|
||||
"parallel_read_safe": True,
|
||||
"parallel_write_safe": True,
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
"""A directive to generate a gallery of images from structured data.
|
||||
|
||||
Generating a gallery of images that are all the same size is a common
|
||||
pattern in documentation, and this can be cumbersome if the gallery is
|
||||
generated programmatically. This directive wraps this particular use-case
|
||||
in a helper-directive to generate it with a single YAML configuration file.
|
||||
|
||||
It currently exists for maintainers of the pydata-sphinx-theme,
|
||||
but might be abstracted into a standalone package if it proves useful.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst import directives
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.docutils import SphinxDirective
|
||||
from yaml import safe_load
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
TEMPLATE_GRID = """
|
||||
`````{{grid}} {columns}
|
||||
{options}
|
||||
|
||||
{content}
|
||||
|
||||
`````
|
||||
"""
|
||||
|
||||
GRID_CARD = """
|
||||
````{{grid-item-card}} {title}
|
||||
{options}
|
||||
|
||||
{content}
|
||||
````
|
||||
"""
|
||||
|
||||
|
||||
class GalleryGridDirective(SphinxDirective):
|
||||
"""A directive to show a gallery of images and links in a Bootstrap grid.
|
||||
|
||||
The grid can be generated from a YAML file that contains a list of items, or
|
||||
from the content of the directive (also formatted in YAML). Use the parameter
|
||||
"class-card" to add an additional CSS class to all cards. When specifying the grid
|
||||
items, you can use all parameters from "grid-item-card" directive to customize
|
||||
individual cards + ["image", "header", "content", "title"].
|
||||
|
||||
Danger:
|
||||
This directive can only be used in the context of a Myst documentation page as
|
||||
the templates use Markdown flavored formatting.
|
||||
"""
|
||||
|
||||
name = "gallery-grid"
|
||||
has_content = True
|
||||
required_arguments = 0
|
||||
optional_arguments = 1
|
||||
final_argument_whitespace = True
|
||||
option_spec: ClassVar[dict[str, Any]] = {
|
||||
# A class to be added to the resulting container
|
||||
"grid-columns": directives.unchanged,
|
||||
"class-container": directives.unchanged,
|
||||
"class-card": directives.unchanged,
|
||||
}
|
||||
|
||||
def run(self) -> list[nodes.Node]:
|
||||
"""Create the gallery grid."""
|
||||
if self.arguments:
|
||||
# If an argument is given, assume it's a path to a YAML file
|
||||
# Parse it and load it into the directive content
|
||||
path_data_rel = Path(self.arguments[0])
|
||||
path_doc, _ = self.get_source_info()
|
||||
path_doc = Path(path_doc).parent
|
||||
path_data = (path_doc / path_data_rel).resolve()
|
||||
if not path_data.exists():
|
||||
logger.info(f"Could not find grid data at {path_data}.")
|
||||
nodes.text("No grid data found at {path_data}.")
|
||||
return None
|
||||
yaml_string = path_data.read_text()
|
||||
else:
|
||||
yaml_string = "\n".join(self.content)
|
||||
|
||||
# Use all the element with an img-bottom key as sites to show
|
||||
# and generate a card item for each of them
|
||||
grid_items = []
|
||||
for item in safe_load(yaml_string):
|
||||
# remove parameters that are not needed for the card options
|
||||
title = item.pop("title", "")
|
||||
|
||||
# build the content of the card using some extra parameters
|
||||
header = f"{item.pop('header')} \n^^^ \n" if "header" in item else ""
|
||||
image = f"}) \n" if "image" in item else ""
|
||||
content = f"{item.pop('content')} \n" if "content" in item else ""
|
||||
|
||||
# optional parameter that influence all cards
|
||||
if "class-card" in self.options:
|
||||
item["class-card"] = self.options["class-card"]
|
||||
|
||||
loc_options_str = "\n".join(f":{k}: {v}" for k, v in item.items()) + " \n"
|
||||
|
||||
card = GRID_CARD.format(
|
||||
options=loc_options_str, content=header + image + content, title=title
|
||||
)
|
||||
grid_items.append(card)
|
||||
|
||||
# Parse the template with Sphinx Design to create an output container
|
||||
# Prep the options for the template grid
|
||||
class_ = "gallery-directive" + f' {self.options.get("class-container", "")}'
|
||||
options = {"gutter": 2, "class-container": class_}
|
||||
options_str = "\n".join(f":{k}: {v}" for k, v in options.items())
|
||||
|
||||
# Create the directive string for the grid
|
||||
grid_directive = TEMPLATE_GRID.format(
|
||||
columns=self.options.get("grid-columns", "1 2 3 4"),
|
||||
options=options_str,
|
||||
content="\n".join(grid_items),
|
||||
)
|
||||
|
||||
# Parse content as a directive so Sphinx Design processes it
|
||||
container = nodes.container()
|
||||
self.state.nested_parse([grid_directive], 0, container)
|
||||
|
||||
# Sphinx Design outputs a container too, so just use that
|
||||
return [container.children[0]]
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> dict[str, Any]:
|
||||
"""Add custom configuration to sphinx app.
|
||||
|
||||
Args:
|
||||
app: the Sphinx application
|
||||
|
||||
Returns:
|
||||
the 2 parallel parameters set to ``True``.
|
||||
"""
|
||||
app.add_directive("gallery-grid", GalleryGridDirective)
|
||||
|
||||
return {
|
||||
"parallel_read_safe": True,
|
||||
"parallel_write_safe": True,
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
var version = DOCUMENTATION_OPTIONS.VERSION;
|
||||
if (version === "stable") {
|
||||
var styles = `
|
||||
#bd-header-version-warning {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
var styleSheet = document.createElement("style")
|
||||
styleSheet.textContent = styles
|
||||
document.head.appendChild(styleSheet)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// File from: https://github.com/pydata/pydata-sphinx-theme/blob/main/docs/_static/custom-icon.js
|
||||
|
||||
/*******************************************************************************
|
||||
* Set a custom icon for pypi as it's not available in the fa built-in brands
|
||||
*/
|
||||
FontAwesome.library.add(
|
||||
(faListOldStyle = {
|
||||
prefix: "fa-custom",
|
||||
iconName: "pypi",
|
||||
icon: [
|
||||
17.313, // viewBox width
|
||||
19.807, // viewBox height
|
||||
[], // ligature
|
||||
"e001", // unicode codepoint - private use area
|
||||
"m10.383 0.2-3.239 1.1769 3.1883 1.1614 3.239-1.1798zm-3.4152 1.2411-3.2362 1.1769 3.1855 1.1614 3.2369-1.1769zm6.7177 0.00281-3.2947 1.2009v3.8254l3.2947-1.1988zm-3.4145 1.2439-3.2926 1.1981v3.8254l0.17548-0.064132 3.1171-1.1347zm-6.6564 0.018325v3.8247l3.244 1.1805v-3.8254zm10.191 0.20931v2.3137l3.1777-1.1558zm3.2947 1.2425-3.2947 1.1988v3.8254l3.2947-1.1988zm-8.7058 0.45739c0.00929-1.931e-4 0.018327-2.977e-4 0.027485 0 0.25633 0.00851 0.4263 0.20713 0.42638 0.49826 1.953e-4 0.38532-0.29327 0.80469-0.65542 0.93662-0.36226 0.13215-0.65608-0.073306-0.65613-0.4588-6.28e-5 -0.38556 0.2938-0.80504 0.65613-0.93662 0.068422-0.024919 0.13655-0.038114 0.20156-0.039466zm5.2913 0.78369-3.2947 1.1988v3.8247l3.2947-1.1981zm-10.132 1.239-3.2362 1.1769 3.1883 1.1614 3.2362-1.1769zm6.7177 0.00213-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4124 1.2439-3.2947 1.1988v3.8254l3.2947-1.1988zm-6.6585 0.016195v3.8275l3.244 1.1805v-3.8254zm16.9 0.21143-3.2947 1.1988v3.8247l3.2947-1.1981zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4124 1.2432-3.2947 1.1988v3.8254l3.2947-1.1988zm-6.6585 0.019027v3.8247l3.244 1.1805v-3.8254zm13.485 1.4497-3.2947 1.1988v3.8247l3.2947-1.1981zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm2.4018 0.38127c0.0093-1.83e-4 0.01833-3.16e-4 0.02749 0 0.25633 0.0085 0.4263 0.20713 0.42638 0.49826 1.97e-4 0.38532-0.29327 0.80469-0.65542 0.93662-0.36188 0.1316-0.65525-0.07375-0.65542-0.4588-1.95e-4 -0.38532 0.29328-0.80469 0.65542-0.93662 0.06842-0.02494 0.13655-0.03819 0.20156-0.03947zm-5.8142 0.86403-3.244 1.1805v1.4201l3.244 1.1805z", // svg path (https://simpleicons.org/icons/pypi.svg)
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -1,147 +0,0 @@
|
||||
.bd-footer {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
--pst-color-primary: hsl(222.2 47.4% 11.2%);
|
||||
--pst-color-secondary: #1774E5;
|
||||
--pst-color-secondary-bg: #1774E5;
|
||||
--pst-color-accent: #1774E5;
|
||||
--sd-color-secondary-highlight: #0062cc;
|
||||
--pst-color-shadow: rgba(0, 0, 0, 0.0);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--pst-color-primary: hsl(213 31% 91%);
|
||||
--pst-color-secondary: #017FFF;
|
||||
--pst-color-secondary-bg: #017FFF;
|
||||
--pst-color-accent: #017FFF;
|
||||
--sd-color-secondary-highlight: #0062cc;
|
||||
--pst-color-shadow: rgba(0, 0, 0, 0.0);
|
||||
}
|
||||
|
||||
.bd-header-announcement {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bd-header-announcement a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bd-header-announcement a:hover {
|
||||
color: white;
|
||||
text-shadow: 0.5px 0 0 currentColor;
|
||||
}
|
||||
|
||||
/* Adding header icon hover and focus effects */
|
||||
.bd-header a:focus-visible {
|
||||
color: var(--pst-color-secondary) !important;
|
||||
text-decoration: underline !important;
|
||||
text-shadow: 0.5px 0 0 currentColor;
|
||||
transform: scale(1.05);
|
||||
transition: all 0.2s ease-in-out;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
nav.bd-links .current>a {
|
||||
box-shadow: inset 1px 0 0 var(--pst-color-primary);
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
/* Top breadcrumbs navigation (ie: Home > Core > ...) */
|
||||
.bd-breadcrumbs .breadcrumb-item > a:focus-visible{
|
||||
border: 2px solid var(--pst-color-primary);
|
||||
}
|
||||
|
||||
/* Left sidebar */
|
||||
nav.bd-links .navbar-nav .toctree-l1>a:focus-visible {
|
||||
border: 2px solid var(--pst-color-primary);
|
||||
}
|
||||
nav.bd-links .current>a {
|
||||
box-shadow: none;
|
||||
border-left: 4px solid var(--pst-color-primary) !important;
|
||||
}
|
||||
|
||||
/* Right sidebar */
|
||||
.bd-sidebar-secondary .sidebar-secondary-items .nav-item .active {
|
||||
box-shadow: none;
|
||||
border-left: 5px solid var(--pst-color-primary) !important;
|
||||
}
|
||||
.bd-sidebar-secondary .sidebar-secondary-items .nav-item>a:focus-visible {
|
||||
border: 2px solid var(--pst-color-primary);
|
||||
}
|
||||
}
|
||||
html[data-theme="light"] .bd-header {
|
||||
border-bottom: 1px solid var(--pst-color-border);
|
||||
}
|
||||
|
||||
.admonition, div.admonition {
|
||||
border: 1px solid var(--pst-color-border);
|
||||
}
|
||||
|
||||
.api-card {
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.api-card svg {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.search-button-field {
|
||||
border-radius: var(--bs-btn-border-radius);
|
||||
}
|
||||
|
||||
.bd-content .sd-tab-set .sd-tab-content {
|
||||
border: none;
|
||||
border-top: 3px solid var(--pst-color-border);
|
||||
|
||||
}
|
||||
.bd-content .sd-tab-set>input:checked+label {
|
||||
border: none;
|
||||
transform: translateY(0);
|
||||
font-weight: 700;
|
||||
border-bottom: 4px solid var(--pst-color-secondary);
|
||||
}
|
||||
.bd-content .sd-tab-set>input:focus-visible+label {
|
||||
border: 2px outset var(--pst-color-secondary);
|
||||
transform: translateY(0);
|
||||
}
|
||||
.bd-content .sd-tab-set>label {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-title svg {
|
||||
font-size: 2rem;
|
||||
vertical-align: bottom;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* This is gross, but necessary to meet accessibility requirements */
|
||||
.headerlink {
|
||||
visibility: visible !important;
|
||||
}
|
||||
/* jupyter notebook output cells */
|
||||
.bd-article .docutils .cell_output .output .highlight > pre:focus-visible{
|
||||
border: 2px outset var(--pst-color-secondary);
|
||||
}
|
||||
|
||||
/* Copy button */
|
||||
.bd-article .docutils .docutils .copybtn:focus-visible:after {
|
||||
/* border: 10px outset var(--pst-color-primary); */
|
||||
display: block;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Long autodoc module names wrap on prev/next links */
|
||||
/* TODO: Should we extend this to the entire site? */
|
||||
.prev-next-title {
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
let liveRegion = createLiveRegion();
|
||||
|
||||
document.querySelectorAll('.copybtn').forEach(button => {
|
||||
// Return focus to copy button after activation
|
||||
button.addEventListener('click', async function (event) {
|
||||
// Save the current focus
|
||||
const focusedElement = document.activeElement;
|
||||
|
||||
// Perform the copy action
|
||||
await copyToClipboard(this);
|
||||
announceMessage(liveRegion, 'Copied to clipboard');
|
||||
|
||||
// Restore the focus
|
||||
focusedElement.focus();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.search-button-field').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
// Save the element that had focus before opening the search
|
||||
const previousFocus = document.activeElement;
|
||||
|
||||
// Add an event listener to handle closing the search
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
// Restore focus to the previous element
|
||||
previousFocus.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Set active TOCtree elements with aria-current=page
|
||||
document.querySelectorAll('.bd-sidenav .active').forEach(function (element) {
|
||||
element.setAttribute('aria-current', 'page');
|
||||
});
|
||||
|
||||
// Set secondary navbar (in-page nagivation) active element with aria-current=page
|
||||
document.addEventListener("activate.bs.scrollspy", function () {
|
||||
const navLinks = document.querySelectorAll(".bd-toc-nav a");
|
||||
|
||||
navLinks.forEach((navLink) => {
|
||||
navLink.parentElement.removeAttribute('aria-current');
|
||||
});
|
||||
|
||||
const activeNavLinks = document.querySelectorAll(".bd-toc-nav a.active");
|
||||
activeNavLinks.forEach((navLink) => {
|
||||
navLink.parentElement.setAttribute('aria-current', 'page');
|
||||
});
|
||||
});
|
||||
|
||||
const themeButton = document.querySelector('.theme-switch-button');
|
||||
if (themeButton) {
|
||||
themeButton.addEventListener('click', function () {
|
||||
const mode = document.documentElement.getAttribute('data-mode');
|
||||
announceMessage(liveRegion, `Theme changed to ${mode}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Enhance TOC sections for accessibility
|
||||
document.querySelectorAll('.caption-text').forEach(caption => {
|
||||
const sectionTitle = caption.textContent.trim();
|
||||
const captionContainer = caption.closest('p.caption');
|
||||
if (!captionContainer) return;
|
||||
|
||||
// Find and process navigation lists that belong to this section
|
||||
findSectionNav(captionContainer, sectionTitle);
|
||||
});
|
||||
|
||||
// Version dropdown menu is dynamically generated after page load. Listen for changes to set aria-selected
|
||||
var observer = new MutationObserver(function () {
|
||||
document.querySelectorAll('.dropdown-item').forEach(function (element) {
|
||||
if (element.classList.contains('active')) {
|
||||
element.setAttribute('aria-selected', 'true');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Observe changes in the version-switcher__menu element
|
||||
var targetNode = document.querySelector('.version-switcher__menu');
|
||||
var config = { childList: true, subtree: true };
|
||||
|
||||
if (targetNode) {
|
||||
observer.observe(targetNode, config);
|
||||
}
|
||||
});
|
||||
|
||||
async function copyToClipboard(button) {
|
||||
const targetSelector = button.getAttribute('data-clipboard-target');
|
||||
const codeBlock = document.querySelector(targetSelector);
|
||||
try {
|
||||
await navigator.clipboard.writeText(codeBlock.textContent);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
function createLiveRegion() {
|
||||
const liveRegion = document.createElement('div');
|
||||
liveRegion.setAttribute('role', 'status');
|
||||
liveRegion.setAttribute('aria-live', 'assertive');
|
||||
liveRegion.style.position = 'absolute';
|
||||
liveRegion.style.width = '1px';
|
||||
liveRegion.style.height = '1px';
|
||||
liveRegion.style.padding = '0';
|
||||
liveRegion.style.margin = '-1px';
|
||||
liveRegion.style.overflow = 'hidden';
|
||||
liveRegion.style.clipPath = 'inset(50%)';
|
||||
liveRegion.style.whiteSpace = 'nowrap'; ` `
|
||||
liveRegion.style.border = '0';
|
||||
document.body.appendChild(liveRegion);
|
||||
|
||||
return liveRegion;
|
||||
}
|
||||
|
||||
function announceMessage(liveRegion, message) {
|
||||
liveRegion.textContent = '';
|
||||
setTimeout(() => {
|
||||
liveRegion.textContent = message;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find navigation lists belonging to a section and process them
|
||||
*/
|
||||
function findSectionNav(captionContainer, sectionTitle) {
|
||||
let nextElement = captionContainer.nextElementSibling;
|
||||
|
||||
while (nextElement) {
|
||||
if (nextElement.classList && nextElement.classList.contains('caption')) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (nextElement.matches('ul.bd-sidenav')) {
|
||||
enhanceNavList(nextElement, sectionTitle);
|
||||
}
|
||||
|
||||
nextElement = nextElement.nextElementSibling;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a navigation list by enhancing its links for accessibility
|
||||
*/
|
||||
function enhanceNavList(navList, sectionTitle) {
|
||||
const topLevelItems = navList.querySelectorAll(':scope > li');
|
||||
|
||||
topLevelItems.forEach(item => {
|
||||
const link = item.querySelector(':scope > a.reference.internal');
|
||||
if (!link) return;
|
||||
|
||||
const linkText = link.textContent.trim();
|
||||
link.setAttribute('aria-label', `${sectionTitle}: ${linkText}`);
|
||||
|
||||
enhanceExpandableSections(item, link, linkText, sectionTitle);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process expandable sections (details elements) within a navigation item
|
||||
*/
|
||||
function enhanceExpandableSections(item, parentLink, parentText, sectionTitle) {
|
||||
const detailsElements = item.querySelectorAll('details');
|
||||
|
||||
detailsElements.forEach(details => {
|
||||
enhanceToggleButton(details, parentText);
|
||||
enhanceNestedLinks(details, parentLink, parentText, sectionTitle);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make toggle buttons more accessible by adding appropriate aria labels
|
||||
*/
|
||||
function enhanceToggleButton(details, parentText) {
|
||||
const summary = details.querySelector('summary');
|
||||
if (!summary) return;
|
||||
|
||||
function updateToggleLabel() {
|
||||
const isExpanded = details.hasAttribute('open');
|
||||
const action = isExpanded ? 'Collapse' : 'Expand';
|
||||
summary.setAttribute('aria-label', `${action} ${parentText} section`);
|
||||
}
|
||||
|
||||
updateToggleLabel();
|
||||
|
||||
summary.addEventListener('click', () => {
|
||||
setTimeout(updateToggleLabel, 10);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance nested links with hierarchical aria-labels
|
||||
*/
|
||||
function enhanceNestedLinks(details, parentLink, parentText, sectionTitle) {
|
||||
const nestedLinks = details.querySelectorAll('a.reference.internal');
|
||||
|
||||
nestedLinks.forEach(link => {
|
||||
const linkText = link.textContent.trim();
|
||||
const parentLabel = parentLink.getAttribute('aria-label');
|
||||
|
||||
if (parentLabel) {
|
||||
link.setAttribute('aria-label', `${parentLabel}: ${linkText}`);
|
||||
} else {
|
||||
link.setAttribute('aria-label', `${sectionTitle}: ${parentText}: ${linkText}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="96" height="85" viewBox="0 0 96 85" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="96" height="85" rx="6" fill="#2D2D2F"/>
|
||||
<path d="M32.6484 28.7109L23.3672 57H15.8906L28.5703 22.875H33.3281L32.6484 28.7109ZM40.3594 57L31.0547 28.7109L30.3047 22.875H35.1094L47.8594 57H40.3594ZM39.9375 44.2969V49.8047H21.9141V44.2969H39.9375ZM77.6484 39.1641V52.6875C77.1172 53.3281 76.2969 54.0234 75.1875 54.7734C74.0781 55.5078 72.6484 56.1406 70.8984 56.6719C69.1484 57.2031 67.0312 57.4688 64.5469 57.4688C62.3438 57.4688 60.3359 57.1094 58.5234 56.3906C56.7109 55.6562 55.1484 54.5859 53.8359 53.1797C52.5391 51.7734 51.5391 50.0547 50.8359 48.0234C50.1328 45.9766 49.7812 43.6406 49.7812 41.0156V38.8828C49.7812 36.2578 50.1172 33.9219 50.7891 31.875C51.4766 29.8281 52.4531 28.1016 53.7188 26.6953C54.9844 25.2891 56.4922 24.2188 58.2422 23.4844C59.9922 22.75 61.9375 22.3828 64.0781 22.3828C67.0469 22.3828 69.4844 22.8672 71.3906 23.8359C73.2969 24.7891 74.75 26.1172 75.75 27.8203C76.7656 29.5078 77.3906 31.4453 77.625 33.6328H70.8047C70.6328 32.4766 70.3047 31.4688 69.8203 30.6094C69.3359 29.75 68.6406 29.0781 67.7344 28.5938C66.8438 28.1094 65.6875 27.8672 64.2656 27.8672C63.0938 27.8672 62.0469 28.1094 61.125 28.5938C60.2188 29.0625 59.4531 29.7578 58.8281 30.6797C58.2031 31.6016 57.7266 32.7422 57.3984 34.1016C57.0703 35.4609 56.9062 37.0391 56.9062 38.8359V41.0156C56.9062 42.7969 57.0781 44.375 57.4219 45.75C57.7656 47.1094 58.2734 48.2578 58.9453 49.1953C59.6328 50.1172 60.4766 50.8125 61.4766 51.2812C62.4766 51.75 63.6406 51.9844 64.9688 51.9844C66.0781 51.9844 67 51.8906 67.7344 51.7031C68.4844 51.5156 69.0859 51.2891 69.5391 51.0234C70.0078 50.7422 70.3672 50.4766 70.6172 50.2266V44.1797H64.1953V39.1641H77.6484Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,11 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "v0.2 (stable)",
|
||||
"version": "0.2",
|
||||
"url": "https://microsoft.github.io/autogen/0.2/"
|
||||
},
|
||||
{
|
||||
"version": "dev",
|
||||
"url": "https://microsoft.github.io/autogen/dev/"
|
||||
}
|
||||
]
|
||||
@@ -1,16 +0,0 @@
|
||||
{% if sourcename is defined and theme_use_edit_page_button and page_source_suffix %}
|
||||
{% set src = sourcename.split('.') %}
|
||||
<div class="tocsection editthispage">
|
||||
<a href="{{ to_main(get_edit_provider_and_url()[1]) }}">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
{% set provider = get_edit_provider_and_url()[0] %}
|
||||
{% block edit_this_page_text %}
|
||||
{% if provider %}
|
||||
{% trans provider=provider %}Edit on {{ provider }}{% endtrans %}
|
||||
{% else %}
|
||||
{% trans %}Edit{% endtrans %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1 +0,0 @@
|
||||
<p><a href="https://go.microsoft.com/fwlink/?LinkId=521839">Privacy Policy</a> | <a href="https://go.microsoft.com/fwlink/?linkid=2259814">Consumer Health Privacy</a> </p>
|
||||
@@ -1,39 +0,0 @@
|
||||
{# Displays the TOC-subtree for pages nested under the currently active top-level TOCtree element. #}
|
||||
|
||||
<nav class="bd-docs-nav bd-links" aria-label="{{ _('Section Navigation') }}">
|
||||
<div class="bd-toc-item navbar-nav">
|
||||
{{- generate_toctree_html(
|
||||
"sidebar",
|
||||
show_nav_level=theme_show_nav_level | int,
|
||||
maxdepth=theme_navigation_depth | int,
|
||||
collapse=theme_collapse_navigation | tobool,
|
||||
includehidden=theme_sidebar_includehidden | tobool,
|
||||
titles_only=True
|
||||
)
|
||||
-}}
|
||||
<ul class="nav bd-sidenav">
|
||||
<li class="toctree-l1">
|
||||
<a class="reference internal" href="{{pathto('reference/python/autogen_agentchat')}}">
|
||||
<i class="fa-solid fa-file-code"></i>
|
||||
API Reference
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a target="_blank" class="reference internal"
|
||||
href="https://pypi.org/project/autogen-agentchat/">
|
||||
<i class="fa-brands fa-python"></i>
|
||||
PyPi
|
||||
<i class="fa-solid fa-arrow-up-right-from-square fa-2xs"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a target="_blank" class="reference internal"
|
||||
href="https://github.com/microsoft/autogen/tree/main/python/packages/autogen-agentchat">
|
||||
<i class="fa-brands fa-github"></i>
|
||||
Source
|
||||
<i class="fa-solid fa-arrow-up-right-from-square fa-2xs"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,38 +0,0 @@
|
||||
{# Displays the TOC-subtree for pages nested under the currently active top-level TOCtree element. #}
|
||||
|
||||
<nav class="bd-docs-nav bd-links" aria-label="{{ _('Section Navigation') }}">
|
||||
<div class="bd-toc-item navbar-nav">
|
||||
{{- generate_toctree_html(
|
||||
"sidebar",
|
||||
show_nav_level=theme_show_nav_level | int,
|
||||
maxdepth=theme_navigation_depth | int,
|
||||
collapse=theme_collapse_navigation | tobool,
|
||||
includehidden=theme_sidebar_includehidden | tobool,
|
||||
titles_only=True
|
||||
)
|
||||
-}}
|
||||
<ul class="nav bd-sidenav">
|
||||
<li class="toctree-l1">
|
||||
<a class="reference internal" href="{{pathto('reference/python/autogen_core')}}">
|
||||
<i class="fa-solid fa-file-code"></i>
|
||||
API Reference
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a target="_blank" class="reference internal" href="https://pypi.org/project/autogen-core/">
|
||||
<i class="fa-brands fa-python"></i>
|
||||
PyPi
|
||||
<i class="fa-solid fa-arrow-up-right-from-square fa-2xs"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a target="_blank" class="reference internal"
|
||||
href="https://github.com/microsoft/autogen/tree/main/python/packages/autogen-core">
|
||||
<i class="fa-brands fa-github"></i>
|
||||
Source
|
||||
<i class="fa-solid fa-arrow-up-right-from-square fa-2xs"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,39 +0,0 @@
|
||||
{# Displays the TOC-subtree for pages nested under the currently active top-level TOCtree element. #}
|
||||
|
||||
<nav class="bd-docs-nav bd-links" aria-label="{{ _('Section Navigation') }}">
|
||||
<div class="bd-toc-item navbar-nav">
|
||||
{{- generate_toctree_html(
|
||||
"sidebar",
|
||||
show_nav_level=theme_show_nav_level | int,
|
||||
maxdepth=theme_navigation_depth | int,
|
||||
collapse=theme_collapse_navigation | tobool,
|
||||
includehidden=theme_sidebar_includehidden | tobool,
|
||||
titles_only=True
|
||||
)
|
||||
-}}
|
||||
<p aria-level="2" class="caption" role="heading"><span class="caption-text">More</span></p>
|
||||
<ul class="nav bd-sidenav">
|
||||
<li class="toctree-l1">
|
||||
<a class="reference internal" href="{{pathto('reference/python/autogen_ext.agents.magentic_one')}}">
|
||||
<i class="fa-solid fa-file-code"></i>
|
||||
API Reference
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a target="_blank" class="reference internal" href="https://pypi.org/project/autogen-ext/">
|
||||
<i class="fa-brands fa-python"></i>
|
||||
PyPi
|
||||
<i class="fa-solid fa-arrow-up-right-from-square fa-2xs"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a target="_blank" class="reference internal"
|
||||
href="https://github.com/microsoft/autogen/tree/main/python/packages/autogen-ext">
|
||||
<i class="fa-brands fa-github"></i>
|
||||
Source
|
||||
<i class="fa-solid fa-arrow-up-right-from-square fa-2xs"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,32 +0,0 @@
|
||||
{# Displays the TOC-subtree for pages nested under the currently active top-level TOCtree element. #}
|
||||
|
||||
<nav class="bd-docs-nav bd-links"
|
||||
aria-label="{{ _('Section Navigation') }}">
|
||||
<div class="bd-toc-item navbar-nav">
|
||||
{{- generate_toctree_html(
|
||||
"sidebar",
|
||||
show_nav_level=theme_show_nav_level | int,
|
||||
maxdepth=theme_navigation_depth | int,
|
||||
collapse=theme_collapse_navigation | tobool,
|
||||
includehidden=theme_sidebar_includehidden | tobool,
|
||||
titles_only=True
|
||||
)
|
||||
-}}
|
||||
<ul class="nav bd-sidenav">
|
||||
<li class="toctree-l1">
|
||||
<a target="_blank" class="reference internal" href="https://pypi.org/project/autogenstudio/">
|
||||
<i class="fa-brands fa-python"></i>
|
||||
PyPi
|
||||
<i class="fa-solid fa-arrow-up-right-from-square fa-2xs"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="toctree-l1">
|
||||
<a target="_blank" class="reference internal" href="https://github.com/microsoft/autogen/tree/main/python/packages/autogen-studio">
|
||||
<i class="fa-brands fa-github"></i>
|
||||
Source
|
||||
<i class="fa-solid fa-arrow-up-right-from-square fa-2xs"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,15 +0,0 @@
|
||||
{# Displays the TOC-subtree for pages nested under the currently active top-level TOCtree element. #}
|
||||
<nav class="bd-docs-nav bd-links"
|
||||
aria-label="{{ _('Section Navigation') }}">
|
||||
<div class="bd-toc-item navbar-nav">
|
||||
{{- generate_toctree_html(
|
||||
"sidebar",
|
||||
show_nav_level=theme_show_nav_level | int,
|
||||
maxdepth=theme_navigation_depth | int,
|
||||
collapse=theme_collapse_navigation | tobool,
|
||||
includehidden=theme_sidebar_includehidden | tobool,
|
||||
titles_only=True
|
||||
)
|
||||
-}}
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,7 +0,0 @@
|
||||
{# Displays an icon to switch between light mode, dark mode, and auto (use browser's setting). #}
|
||||
{# As the theme switcher will only work when JavaScript is enabled, we hide it with `pst-js-only`. #}
|
||||
<button class="btn btn-sm nav-link pst-navbar-icon theme-switch-button pst-js-only" aria-label="{{ _('Color mode') }}" data-bs-title="{{ _('Color mode') }}" data-bs-placement="bottom" data-bs-toggle="tooltip">
|
||||
<i class="theme-switch fa-solid fa-sun fa-lg" data-mode="light" title="{{ _('Light') }}"></i>
|
||||
<i class="theme-switch fa-solid fa-moon fa-lg" data-mode="dark" title="{{ _('Dark') }}"></i>
|
||||
<i class="theme-switch fa-solid fa-circle-half-stroke fa-lg" data-mode="auto" title="{{ _('System Settings') }}"></i>
|
||||
</button>
|
||||
@@ -1 +0,0 @@
|
||||
<script src="_static/banner-override.js"></script>
|
||||
@@ -1,210 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from sphinx.application import Sphinx
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
import agent_framework
|
||||
import agent_framework_foundry
|
||||
import agent_framework_azure
|
||||
|
||||
project = "agent_framework"
|
||||
copyright = "2025, Microsoft"
|
||||
author = "Microsoft"
|
||||
version = "0.1.0b1"
|
||||
|
||||
release_override = os.getenv("SPHINX_RELEASE_OVERRIDE")
|
||||
if release_override is None or release_override == "":
|
||||
release = agent_framework.__version__
|
||||
else:
|
||||
release = release_override
|
||||
|
||||
sys.path.append(str(Path(".").resolve()))
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = [
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.autosummary",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.graphviz",
|
||||
"sphinxext.rediraffe",
|
||||
"sphinx_design",
|
||||
"sphinx_copybutton",
|
||||
"_extension.gallery_directive",
|
||||
"myst_nb",
|
||||
"sphinxcontrib.autodoc_pydantic",
|
||||
"_extension.code_lint",
|
||||
]
|
||||
suppress_warnings = ["myst.header"]
|
||||
|
||||
# Napoleon settings
|
||||
napoleon_google_docstring = True
|
||||
napoleon_use_admonition_for_examples = False
|
||||
napoleon_include_init_with_doc = True
|
||||
napoleon_custom_sections = [("returns_style", "params_style")]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
|
||||
|
||||
# TODO: include all notebooks excluding those requiring remote API access.
|
||||
nb_execution_mode = "off"
|
||||
|
||||
# Guides and tutorials must succeed.
|
||||
nb_execution_raise_on_error = True
|
||||
nb_execution_timeout = 60
|
||||
|
||||
myst_heading_anchors = 5
|
||||
|
||||
myst_enable_extensions = [
|
||||
"colon_fence",
|
||||
"linkify",
|
||||
"strikethrough",
|
||||
]
|
||||
|
||||
if (path := os.getenv("PY_DOCS_DIR")) is None:
|
||||
path = "dev"
|
||||
|
||||
|
||||
if (switcher_version := os.getenv("PY_SWITCHER_VERSION")) is None:
|
||||
switcher_version = "dev"
|
||||
|
||||
html_baseurl = f"/agent-framework/{path}/"
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_title = "Agent Framework"
|
||||
|
||||
html_theme = "pydata_sphinx_theme"
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = ["custom.css"]
|
||||
|
||||
add_module_names = False
|
||||
|
||||
html_logo = "_static/images/logo/logo.svg"
|
||||
html_favicon = "_static/images/logo/favicon-512x512.png"
|
||||
|
||||
html_theme_options = {
|
||||
"header_links_before_dropdown": 6,
|
||||
"navbar_align": "left",
|
||||
"check_switcher": False,
|
||||
# "navbar_start": ["navbar-logo", "version-switcher"],
|
||||
# "switcher": {
|
||||
# "json_url": "/_static/switcher.json",
|
||||
# },
|
||||
"show_prev_next": True,
|
||||
"icon_links": [
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/microsoft/agent-framework",
|
||||
"icon": "fa-brands fa-github",
|
||||
},
|
||||
],
|
||||
"footer_start": ["copyright"],
|
||||
"footer_center": ["footer-middle-links"],
|
||||
"footer_end": ["theme-version", "version-banner-override"],
|
||||
"pygments_light_style": "xcode",
|
||||
"pygments_dark_style": "monokai",
|
||||
"navbar_start": ["navbar-logo", "version-switcher"],
|
||||
"switcher": {
|
||||
"json_url": "https://raw.githubusercontent.com/microsoft/agent-framework/refs/heads/main/docs/switcher.json",
|
||||
"version_match": switcher_version,
|
||||
},
|
||||
"show_version_warning_banner": True,
|
||||
}
|
||||
|
||||
html_js_files = ["custom-icon.js", "banner-override.js", "custom.js"]
|
||||
html_sidebars = {"packages/index": []}
|
||||
|
||||
html_context = {
|
||||
"display_github": True,
|
||||
"github_user": "microsoft",
|
||||
"github_repo": "agent-framework",
|
||||
"github_version": "main",
|
||||
"doc_path": "python/docs/agent-framework/",
|
||||
}
|
||||
|
||||
autoclass_content = "both"
|
||||
autodoc_default_options = {
|
||||
"members": True,
|
||||
"member-order": "alphabetical",
|
||||
"undoc-members": True,
|
||||
"show-inheritance": True,
|
||||
"imported-members": True,
|
||||
}
|
||||
autodoc_pydantic_model_show_json = False
|
||||
autodoc_pydantic_model_show_config_summary = False
|
||||
autodoc_pydantic_model_show_json_error_strategy = "coerce"
|
||||
python_use_unqualified_type_names = True
|
||||
autodoc_preserve_defaults = True
|
||||
|
||||
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
|
||||
|
||||
code_lint_path_prefix = "reference/python"
|
||||
|
||||
nb_mime_priority_overrides = [
|
||||
("code_lint", "image/jpeg", 100),
|
||||
("code_lint", "image/png", 100),
|
||||
("code_lint", "text/plain", 100),
|
||||
]
|
||||
|
||||
rediraffe_redirects = {}
|
||||
|
||||
|
||||
def setup_to_main(app: Sphinx, pagename: str, templatename: str, context, doctree) -> None:
|
||||
"""Add a function that jinja can access for returning an "edit this page" link pointing to `main`."""
|
||||
|
||||
def to_main(link: str) -> str:
|
||||
"""Transform "edit on github" links and make sure they always point to the main branch.
|
||||
|
||||
Args:
|
||||
link: the link to the github edit interface
|
||||
|
||||
Returns:
|
||||
the link to the tip of the main branch for the same file
|
||||
"""
|
||||
links = link.split("/")
|
||||
idx = links.index("edit")
|
||||
return "/".join(links[: idx + 1]) + "/main/" + "/".join(links[idx + 2 :])
|
||||
|
||||
context["to_main"] = to_main
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> dict[str, Any]:
|
||||
"""Add custom configuration to sphinx app.
|
||||
|
||||
Args:
|
||||
app: the Sphinx application
|
||||
Returns:
|
||||
the 2 parallel parameters set to ``True``.
|
||||
"""
|
||||
app.connect("html-page-context", setup_to_main)
|
||||
|
||||
# Adding here so it is inline and not in a separate file.
|
||||
clarity_analytics = """(function(c,l,a,r,i,t,y){
|
||||
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||
})(window, document, "clarity", "script", "lnxpe6skj1");"""
|
||||
app.add_js_file(None, body=clarity_analytics)
|
||||
|
||||
return {
|
||||
"parallel_read_safe": True,
|
||||
"parallel_write_safe": True,
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
myst:
|
||||
html_meta:
|
||||
"description lang=en": |
|
||||
Top-level documentation for Agent Framework, a framework for developing applications using AI agents
|
||||
html_theme.sidebar_secondary.remove: false
|
||||
sd_hide_title: true
|
||||
---
|
||||
|
||||
<style>
|
||||
.hero-title {
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
margin: 2rem auto 0;
|
||||
}
|
||||
|
||||
.wip-card {
|
||||
border: 1px solid var(--pst-color-success);
|
||||
background-color: var(--pst-color-success-bg);
|
||||
border-radius: .25rem;
|
||||
padding: 0.3rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
# Agent Framework
|
||||
|
||||
<div class="container">
|
||||
<div class="row text-center">
|
||||
<div class="col-sm-12">
|
||||
<h1 class="hero-title">
|
||||
Agent Framework
|
||||
</h1>
|
||||
<h3>
|
||||
A framework for building AI agents and applications
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2rem;">
|
||||
|
||||
::::{grid}
|
||||
:gutter: 2
|
||||
|
||||
:::{grid-item-card} {fas}`cube;pst-color-primary` Agent Framework [](https://pypi.org/project/agent-framework/)
|
||||
:shadow: none
|
||||
:margin: 2 0 0 0
|
||||
:columns: 12 12 12 12
|
||||
|
||||
Create and manage AI agents, workflows, and applications using the Agent Framework. It provides:
|
||||
|
||||
* Deterministic and dynamic agentic workflows for business processes.
|
||||
* Research on multi-agent collaboration.
|
||||
* Distributed agents for multi-language applications.
|
||||
|
||||
_Start here if you are getting serious about building multi-agent systems._
|
||||
|
||||
+++
|
||||
|
||||
```{button-ref} reference/index
|
||||
:color: secondary
|
||||
|
||||
Get Started
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::{grid-item-card} {fas}`puzzle-piece;pst-color-primary` Extensions [](https://pypi.org/search/?q=agent-framework-)
|
||||
:shadow: none
|
||||
:margin: 2 0 0 0
|
||||
:columns: 12 12 12 12
|
||||
|
||||
Implementations of connectors and other external components for the Agent Framework. These extensions allow you to connect to various AI models, services, and tools, enhancing the capabilities of your agents.
|
||||
|
||||
* {py:mod}`~agent-framework.azure` for using Azure services.
|
||||
* {py:mod}`~agent-framework.foundry` for using Foundry models.
|
||||
* {py:mod}`~agent-framework.openai` for using OpenAI models.
|
||||
+++
|
||||
|
||||
:::
|
||||
|
||||
::::
|
||||
|
||||
</div>
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 3
|
||||
:hidden:
|
||||
|
||||
reference/index
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
myst:
|
||||
html_meta:
|
||||
"description lang=en": |
|
||||
Agent Framework is a community-driven project. Learn how to get involved, contribute, and connect with the community.
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
||||
```{toctree}
|
||||
:caption: Agent Framework
|
||||
:maxdepth: 2
|
||||
|
||||
python/agent_framework
|
||||
python/agent_framework.exceptions
|
||||
python/agent_framework.openai
|
||||
python/agent_framework.azure
|
||||
python/agent_framework.foundry
|
||||
```
|
||||
@@ -1,4 +0,0 @@
|
||||
agent_framework.azure
|
||||
==========================
|
||||
|
||||
.. automodule:: agent_framework.azure
|
||||
@@ -1,5 +0,0 @@
|
||||
agent_framework.exceptions
|
||||
==========================
|
||||
|
||||
.. automodule:: agent_framework.exceptions
|
||||
:member-order: bysource
|
||||
@@ -1,4 +0,0 @@
|
||||
agent_framework.foundry
|
||||
==========================
|
||||
|
||||
.. automodule:: agent_framework.foundry
|
||||
@@ -1,4 +0,0 @@
|
||||
agent_framework.openai
|
||||
==========================
|
||||
|
||||
.. automodule:: agent_framework.openai
|
||||
@@ -1,4 +0,0 @@
|
||||
agent_framework
|
||||
===============
|
||||
|
||||
.. automodule:: agent_framework
|
||||
@@ -0,0 +1,107 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import debugpy
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from agent_framework import __version__ as agent_framework_version
|
||||
from py2docfx.__main__ import main as py2docfx_main
|
||||
|
||||
load_dotenv()
|
||||
|
||||
async def generate_af_docs(root_path: Path):
|
||||
"""Generate documentation for the Agent Framework using py2docfx.
|
||||
|
||||
This function runs the py2docfx command with the specified parameters.
|
||||
"""
|
||||
package = {
|
||||
"packages": [
|
||||
{
|
||||
"package_info": {
|
||||
"name": "agent-framework",
|
||||
"version": agent_framework_version,
|
||||
"install_type": "pypi",
|
||||
"extras": ["all"],
|
||||
},
|
||||
"sphinx_extensions": [
|
||||
"sphinxcontrib.autodoc_pydantic",
|
||||
"sphinx-pydantic",
|
||||
"sphinx.ext.autosummary"
|
||||
],
|
||||
"extension_config": {
|
||||
"napoleon_google_docstring": True,
|
||||
"napoleon_preprocess_types": True,
|
||||
"napoleon_use_param": True,
|
||||
"autodoc_pydantic_field_doc_policy": "both",
|
||||
"autodoc_pydantic_model_show_json": False,
|
||||
"autodoc_pydantic_model_show_config_summary": True,
|
||||
"autodoc_pydantic_model_show_field_summary": True,
|
||||
"autodoc_pydantic_model_hide_paramlist": False,
|
||||
"autodoc_pydantic_model_show_json_error_strategy": "coerce",
|
||||
"autodoc_pydantic_settings_show_config_summary": True,
|
||||
"autodoc_pydantic_settings_show_field_summary": True,
|
||||
"python_use_unqualified_type_names": True,
|
||||
"autodoc_preserve_defaults": True,
|
||||
"autodoc_class_signature": "separated",
|
||||
"autodoc_typehints": "both",
|
||||
"autodoc_typehints_format": "fully-qualified",
|
||||
"autodoc_default_options": {
|
||||
"members": True,
|
||||
"member-order": "alphabetical",
|
||||
"undoc-members": True,
|
||||
"show-inheritance": True,
|
||||
"imported-members": True,
|
||||
"inherited-members": 'AFBaseModel',
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
"required_packages": [
|
||||
{
|
||||
"install_type": "pypi",
|
||||
"name": "autodoc_pydantic",
|
||||
"version": ">=2.0.0",
|
||||
},
|
||||
{
|
||||
"install_type": "pypi",
|
||||
"name": "sphinx-pydantic",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
args = [
|
||||
"-o",
|
||||
str((root_path / "docs" / "build").absolute()),
|
||||
"-j",
|
||||
json.dumps(package),
|
||||
]
|
||||
try:
|
||||
await py2docfx_main(args)
|
||||
except Exception as e:
|
||||
print(f"Error generating documentation: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Ensure the script is run from the correct directory
|
||||
debug = False
|
||||
if debug:
|
||||
debugpy.listen(("localhost", 5678))
|
||||
debugpy.wait_for_client()
|
||||
debugpy.breakpoint()
|
||||
|
||||
current_path = Path(__file__).parent.parent.resolve()
|
||||
print(f"Current path: {current_path}")
|
||||
# ensure the dist folder exists
|
||||
dist_path = current_path / "dist"
|
||||
if not dist_path.exists():
|
||||
print(" Please run `poe build` to generate the dist folder.")
|
||||
exit(1)
|
||||
if os.getenv("PIP_FIND_LINKS") != str(dist_path.absolute()):
|
||||
print(f"Setting PIP_FIND_LINKS to {dist_path.absolute()}")
|
||||
os.environ["PIP_FIND_LINKS"] = str(dist_path.absolute())
|
||||
print(f"Generating documentation in: {current_path / 'docs' / 'build'}")
|
||||
# Generate the documentation
|
||||
asyncio.run(generate_af_docs(current_path))
|
||||
@@ -82,10 +82,6 @@ include = "../../shared_tasks.toml"
|
||||
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure"
|
||||
test = "pytest --cov=agent_framework_azure --cov-report=term-missing:skip-covered tests"
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-name = "agent_framework_azure"
|
||||
module-root = ""
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.8.2,<0.9.0"]
|
||||
build-backend = "uv_build"
|
||||
requires = ["flit-core >= 3.9,<4.0"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
|
||||
@@ -23,7 +23,7 @@ Before using the Copilot Studio agent, you need:
|
||||
The following environment variables are used for configuration:
|
||||
|
||||
- `COPILOTSTUDIOAGENT__ENVIRONMENTID` - Your Copilot Studio environment ID
|
||||
- `COPILOTSTUDIOAGENT__SCHEMANAME` - Your copilot's agent identifier/schema name
|
||||
- `COPILOTSTUDIOAGENT__SCHEMANAME` - Your copilot's agent identifier/schema name
|
||||
- `COPILOTSTUDIOAGENT__AGENTAPPID` - Your App Registration client ID
|
||||
- `COPILOTSTUDIOAGENT__TENANTID` - Your Azure AD tenant ID
|
||||
|
||||
@@ -36,7 +36,7 @@ from agent_framework.copilotstudio import CopilotStudioAgent
|
||||
async def main():
|
||||
# Create agent using environment variables
|
||||
agent = CopilotStudioAgent()
|
||||
|
||||
|
||||
# Run a simple query
|
||||
result = await agent.run("What is the capital of France?")
|
||||
print(result)
|
||||
@@ -58,19 +58,20 @@ async def main():
|
||||
client_id=os.environ["COPILOTSTUDIOAGENT__AGENTAPPID"],
|
||||
tenant_id=os.environ["COPILOTSTUDIOAGENT__TENANTID"]
|
||||
)
|
||||
|
||||
|
||||
# Create connection settings
|
||||
settings = ConnectionSettings(
|
||||
environment_id=os.environ["COPILOTSTUDIOAGENT__ENVIRONMENTID"],
|
||||
agent_identifier=os.environ["COPILOTSTUDIOAGENT__SCHEMANAME"],
|
||||
cloud=PowerPlatformCloud.PROD,
|
||||
copilot_agent_type=AgentType.PUBLISHED
|
||||
copilot_agent_type=AgentType.PUBLISHED,
|
||||
custom_power_platform_cloud=None
|
||||
)
|
||||
|
||||
|
||||
# Create client and agent
|
||||
client = CopilotClient(settings=settings, token=token)
|
||||
agent = CopilotStudioAgent(client=client)
|
||||
|
||||
|
||||
# Run a query
|
||||
result = await agent.run("What is the capital of Italy?")
|
||||
print(result)
|
||||
@@ -94,4 +95,3 @@ For more comprehensive examples, see the [Copilot Studio examples](https://githu
|
||||
- Explicit settings and manual token acquisition
|
||||
- Different authentication patterns
|
||||
- Error handling and troubleshooting
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from agent_framework import (
|
||||
)
|
||||
from agent_framework._pydantic import AFBaseSettings
|
||||
from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException
|
||||
from agent_framework.telemetry import prepend_agent_framework_to_user_agent, use_telemetry
|
||||
from agent_framework.telemetry import AGENT_FRAMEWORK_USER_AGENT, use_telemetry
|
||||
from azure.ai.agents.models import (
|
||||
AgentsNamedToolChoice,
|
||||
AgentsNamedToolChoiceType,
|
||||
@@ -95,8 +95,6 @@ class FoundrySettings(AFBaseSettings):
|
||||
|
||||
TFoundryChatClient = TypeVar("TFoundryChatClient", bound="FoundryChatClient")
|
||||
|
||||
HEADERS = prepend_agent_framework_to_user_agent()
|
||||
|
||||
|
||||
@use_function_invocation
|
||||
@use_telemetry
|
||||
@@ -174,7 +172,11 @@ class FoundryChatClient(BaseChatClient):
|
||||
# Use provided credential
|
||||
if not async_credential:
|
||||
raise ServiceInitializationError("Azure credential is required when client is not provided.")
|
||||
client = AIProjectClient(endpoint=foundry_settings.project_endpoint, credential=async_credential)
|
||||
client = AIProjectClient(
|
||||
endpoint=foundry_settings.project_endpoint,
|
||||
credential=async_credential,
|
||||
user_agent=AGENT_FRAMEWORK_USER_AGENT,
|
||||
)
|
||||
should_close_client = True
|
||||
|
||||
super().__init__(
|
||||
@@ -286,11 +288,7 @@ class FoundryChatClient(BaseChatClient):
|
||||
raise ServiceInitializationError("Model deployment name is required for agent creation.")
|
||||
|
||||
agent_name = self.agent_name
|
||||
args = {
|
||||
"model": self.ai_model_id,
|
||||
"name": agent_name,
|
||||
"headers": HEADERS,
|
||||
}
|
||||
args = {"model": self.ai_model_id, "name": agent_name}
|
||||
if run_options:
|
||||
if "tools" in run_options:
|
||||
args["tools"] = run_options["tools"]
|
||||
@@ -326,11 +324,7 @@ class FoundryChatClient(BaseChatClient):
|
||||
if thread_run is not None and tool_run_id is not None and tool_run_id == thread_run.id and tool_outputs:
|
||||
# There's an active run and we have tool results to submit, so submit the results.
|
||||
await self.client.agents.runs.submit_tool_outputs_stream( # type: ignore[reportUnknownMemberType]
|
||||
thread_run.thread_id,
|
||||
tool_run_id,
|
||||
tool_outputs=tool_outputs,
|
||||
event_handler=handler,
|
||||
headers=HEADERS,
|
||||
thread_run.thread_id, tool_run_id, tool_outputs=tool_outputs, event_handler=handler
|
||||
)
|
||||
# Pass the handler to the stream to continue processing
|
||||
stream = handler # type: ignore
|
||||
@@ -342,10 +336,7 @@ class FoundryChatClient(BaseChatClient):
|
||||
# Now create a new run and stream the results.
|
||||
run_options.pop("conversation_id", None)
|
||||
stream = await self.client.agents.runs.stream( # type: ignore[reportUnknownMemberType]
|
||||
final_thread_id,
|
||||
agent_id=agent_id,
|
||||
headers=HEADERS,
|
||||
**run_options,
|
||||
final_thread_id, agent_id=agent_id, **run_options
|
||||
)
|
||||
|
||||
return stream, final_thread_id
|
||||
@@ -355,9 +346,7 @@ class FoundryChatClient(BaseChatClient):
|
||||
if thread_id is None:
|
||||
return None
|
||||
|
||||
async for run in self.client.agents.runs.list(
|
||||
thread_id=thread_id, limit=1, order=ListSortOrder.DESCENDING, headers=HEADERS
|
||||
): # type: ignore[reportUnknownMemberType]
|
||||
async for run in self.client.agents.runs.list(thread_id=thread_id, limit=1, order=ListSortOrder.DESCENDING): # type: ignore[reportUnknownMemberType]
|
||||
if run.status not in [
|
||||
RunStatus.COMPLETED,
|
||||
RunStatus.CANCELLED,
|
||||
@@ -374,15 +363,13 @@ class FoundryChatClient(BaseChatClient):
|
||||
if thread_id is not None:
|
||||
if thread_run is not None:
|
||||
# There was an active run; we need to cancel it before starting a new run.
|
||||
await self.client.agents.runs.cancel(thread_id, thread_run.id, headers=HEADERS)
|
||||
await self.client.agents.runs.cancel(thread_id, thread_run.id)
|
||||
|
||||
return thread_id
|
||||
|
||||
# No thread ID was provided, so create a new thread.
|
||||
thread = await self.client.agents.threads.create(
|
||||
tool_resources=run_options.get("tool_resources"),
|
||||
metadata=run_options.get("metadata"),
|
||||
headers=HEADERS,
|
||||
tool_resources=run_options.get("tool_resources"), metadata=run_options.get("metadata")
|
||||
)
|
||||
thread_id = thread.id
|
||||
# workaround for: https://github.com/Azure/azure-sdk-for-python/issues/42805
|
||||
@@ -391,11 +378,7 @@ class FoundryChatClient(BaseChatClient):
|
||||
# `messages=run_options.pop("additional_messages")`
|
||||
for msg in run_options.pop("additional_messages", []):
|
||||
await self.client.agents.messages.create(
|
||||
thread_id=thread_id,
|
||||
role=msg.role,
|
||||
content=msg.content,
|
||||
metadata=msg.metadata,
|
||||
headers=HEADERS,
|
||||
thread_id=thread_id, role=msg.role, content=msg.content, metadata=msg.metadata
|
||||
)
|
||||
# and remove until here.
|
||||
return thread_id
|
||||
@@ -519,7 +502,7 @@ class FoundryChatClient(BaseChatClient):
|
||||
async def _cleanup_agent_if_needed(self) -> None:
|
||||
"""Clean up the agent if we created it."""
|
||||
if self._should_delete_agent and self.agent_id is not None:
|
||||
await self.client.agents.delete_agent(self.agent_id, headers=HEADERS)
|
||||
await self.client.agents.delete_agent(self.agent_id)
|
||||
self.agent_id = None
|
||||
self._should_delete_agent = False
|
||||
|
||||
|
||||
@@ -84,10 +84,6 @@ include = "../../shared_tasks.toml"
|
||||
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry"
|
||||
test = "pytest --cov=agent_framework_foundry --cov-report=term-missing:skip-covered tests"
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-name = "agent_framework_foundry"
|
||||
module-root = ""
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.8.2,<0.9.0"]
|
||||
build-backend = "uv_build"
|
||||
requires = ["flit-core >= 3.9,<4.0"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
|
||||
@@ -23,7 +23,6 @@ from agent_framework import (
|
||||
TextContent,
|
||||
UriContent,
|
||||
)
|
||||
from agent_framework import __version__ as AF_VERSION
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from agent_framework.foundry import FoundryChatClient, FoundrySettings
|
||||
from azure.ai.agents.models import (
|
||||
@@ -315,9 +314,7 @@ async def test_foundry_chat_client_cleanup_agent_if_needed_should_delete(
|
||||
|
||||
await chat_client._cleanup_agent_if_needed() # type: ignore
|
||||
# Verify agent deletion was called
|
||||
mock_ai_project_client.agents.delete_agent.assert_called_once_with(
|
||||
"agent-to-delete", headers={"User-Agent": f"agent-framework-python/{AF_VERSION}"}
|
||||
)
|
||||
mock_ai_project_client.agents.delete_agent.assert_called_once_with("agent-to-delete")
|
||||
assert not chat_client._should_delete_agent # type: ignore
|
||||
|
||||
|
||||
@@ -358,9 +355,7 @@ async def test_foundry_chat_client_aclose(mock_ai_project_client: MagicMock) ->
|
||||
await chat_client.close()
|
||||
|
||||
# Verify agent deletion was called
|
||||
mock_ai_project_client.agents.delete_agent.assert_called_once_with(
|
||||
"agent-to-delete", headers={"User-Agent": f"agent-framework-python/{AF_VERSION}"}
|
||||
)
|
||||
mock_ai_project_client.agents.delete_agent.assert_called_once_with("agent-to-delete")
|
||||
|
||||
|
||||
async def test_foundry_chat_client_async_context_manager(mock_ai_project_client: MagicMock) -> None:
|
||||
@@ -374,9 +369,7 @@ async def test_foundry_chat_client_async_context_manager(mock_ai_project_client:
|
||||
pass # Just test that we can enter and exit
|
||||
|
||||
# Verify cleanup was called on exit
|
||||
mock_ai_project_client.agents.delete_agent.assert_called_once_with(
|
||||
"agent-to-delete", headers={"User-Agent": f"agent-framework-python/{AF_VERSION}"}
|
||||
)
|
||||
mock_ai_project_client.agents.delete_agent.assert_called_once_with("agent-to-delete")
|
||||
|
||||
|
||||
def test_foundry_chat_client_create_run_options_basic(mock_ai_project_client: MagicMock) -> None:
|
||||
@@ -562,9 +555,7 @@ async def test_foundry_chat_client_prepare_thread_cancels_active_run(mock_ai_pro
|
||||
result = await chat_client._prepare_thread("test-thread", mock_thread_run, run_options) # type: ignore
|
||||
|
||||
assert result == "test-thread"
|
||||
mock_ai_project_client.agents.runs.cancel.assert_called_once_with(
|
||||
"test-thread", "run_123", headers={"User-Agent": f"agent-framework-python/{AF_VERSION}"}
|
||||
)
|
||||
mock_ai_project_client.agents.runs.cancel.assert_called_once_with("test-thread", "run_123")
|
||||
|
||||
|
||||
def test_foundry_chat_client_create_function_call_contents_basic(mock_ai_project_client: MagicMock) -> None:
|
||||
|
||||
@@ -50,10 +50,10 @@ You can also override environment variables by explicitly passing configuration
|
||||
from agent_framework.azure import AzureChatClient
|
||||
|
||||
chat_client = AzureChatClient(
|
||||
api_key=...,
|
||||
endpoint=...,
|
||||
deployment_name=...,
|
||||
api_version=...,
|
||||
api_key="",
|
||||
endpoint="",
|
||||
deployment_name="",
|
||||
api_version="",
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -17,3 +17,4 @@ from ._middleware import * # noqa: F403
|
||||
from ._threads import * # noqa: F403
|
||||
from ._tools import * # noqa: F403
|
||||
from ._types import * # noqa: F403
|
||||
from ._workflow import * # noqa: F403
|
||||
|
||||
@@ -17,6 +17,8 @@ else:
|
||||
|
||||
# region Context
|
||||
|
||||
__all__ = ["AggregateContextProvider", "Context", "ContextProvider"]
|
||||
|
||||
|
||||
class Context(AFBaseModel):
|
||||
"""A class containing any context that should be provided to the AI model as supplied by an ContextProvider.
|
||||
|
||||
@@ -73,6 +73,9 @@ DEFAULT_MAX_ITERATIONS: Final[int] = 10
|
||||
TChatClient = TypeVar("TChatClient", bound="ChatClientProtocol")
|
||||
# region Helpers
|
||||
|
||||
ArgsT = TypeVar("ArgsT", bound=BaseModel)
|
||||
ReturnT = TypeVar("ReturnT")
|
||||
|
||||
|
||||
def _parse_inputs(
|
||||
inputs: "Contents | dict[str, Any] | str | list[Contents | dict[str, Any] | str] | None",
|
||||
@@ -121,13 +124,10 @@ def _parse_inputs(
|
||||
class ToolProtocol(Protocol):
|
||||
"""Represents a generic tool that can be specified to an AI service.
|
||||
|
||||
Attributes:
|
||||
Parameters:
|
||||
name: The name of the tool.
|
||||
description: A description of the tool.
|
||||
additional_properties: Additional properties associated with the tool.
|
||||
|
||||
Methods:
|
||||
parameters: The parameters accepted by the tool, in a json schema format.
|
||||
"""
|
||||
|
||||
name: str
|
||||
@@ -142,10 +142,6 @@ class ToolProtocol(Protocol):
|
||||
...
|
||||
|
||||
|
||||
ArgsT = TypeVar("ArgsT", bound=BaseModel)
|
||||
ReturnT = TypeVar("ReturnT")
|
||||
|
||||
|
||||
class BaseTool(AFBaseModel):
|
||||
"""Base class for AI tools, providing common attributes and methods.
|
||||
|
||||
@@ -516,11 +512,20 @@ def ai_function(
|
||||
In order to add descriptions to parameters, in your function signature,
|
||||
use the `Annotated` type from `typing` and the `Field` class from `pydantic`:
|
||||
|
||||
from typing import Annotated
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from typing import Annotated
|
||||
from pydantic import Field
|
||||
|
||||
<field_name>: Annotated[<type>, Field(description="<description>")]
|
||||
|
||||
def ai_function_example(
|
||||
arg1: Annotated[str, Field(description="The first argument")],
|
||||
arg2: Annotated[int, Field(description="The second argument")],
|
||||
) -> str:
|
||||
# An example function that takes two arguments and returns a string.
|
||||
return f"arg1: {arg1}, arg2: {arg2}"
|
||||
|
||||
Args:
|
||||
func: The function to wrap. If None, returns a decorator.
|
||||
|
||||
@@ -1297,7 +1297,7 @@ FinishReason.TOOL_CALLS = FinishReason(value="tool_calls") # type: ignore[assig
|
||||
|
||||
|
||||
class ChatMessage(AFBaseModel):
|
||||
"""Represents a chat message used by a `ModelClient`.
|
||||
"""Represents a chat message.
|
||||
|
||||
Attributes:
|
||||
role: The role of the author of the message.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Get Started with Microsoft Agent Framework Workflow
|
||||
|
||||
Workflow capabilities now ship with the core `agent-framework` package.
|
||||
|
||||
```bash
|
||||
pip install agent-framework
|
||||
```
|
||||
|
||||
Optional visualization support is still available via the `viz` extra:
|
||||
|
||||
```bash
|
||||
pip install agent-framework[viz]
|
||||
```
|
||||
|
||||
See the [project README](https://github.com/microsoft/agent-framework/tree/main/python/README.md) for more information.
|
||||
+31
-12
@@ -1,6 +1,6 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib.metadata
|
||||
import contextlib
|
||||
|
||||
from ._agent import WorkflowAgent
|
||||
from ._checkpoint import (
|
||||
@@ -13,7 +13,18 @@ from ._concurrent import ConcurrentBuilder
|
||||
from ._const import (
|
||||
DEFAULT_MAX_ITERATIONS,
|
||||
)
|
||||
from ._edge import Case, Default
|
||||
from ._edge import (
|
||||
Case,
|
||||
Default,
|
||||
Edge,
|
||||
FanInEdgeGroup,
|
||||
FanOutEdgeGroup,
|
||||
SingleEdgeGroup,
|
||||
SwitchCaseEdgeGroup,
|
||||
SwitchCaseEdgeGroupCase,
|
||||
SwitchCaseEdgeGroupDefault,
|
||||
)
|
||||
from ._edge_runner import create_edge_runner
|
||||
from ._events import (
|
||||
AgentRunEvent,
|
||||
AgentRunUpdateEvent,
|
||||
@@ -67,15 +78,19 @@ from ._magentic import (
|
||||
MagenticStartMessage,
|
||||
StandardMagenticManager,
|
||||
)
|
||||
from ._runner import Runner
|
||||
from ._runner_context import (
|
||||
InProcRunnerContext,
|
||||
Message,
|
||||
RunnerContext,
|
||||
)
|
||||
from ._sequential import SequentialBuilder
|
||||
from ._shared_state import SharedState
|
||||
from ._telemetry import EdgeGroupDeliveryStatus, WorkflowTracer, workflow_tracer
|
||||
from ._validation import (
|
||||
EdgeDuplicationError,
|
||||
GraphConnectivityError,
|
||||
HandlerOutputAnnotationError,
|
||||
TypeCompatibilityError,
|
||||
ValidationTypeEnum,
|
||||
WorkflowValidationError,
|
||||
@@ -85,12 +100,6 @@ from ._viz import WorkflowViz
|
||||
from ._workflow import Workflow, WorkflowBuilder, WorkflowRunResult
|
||||
from ._workflow_context import WorkflowContext
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_MAX_ITERATIONS",
|
||||
"AgentExecutor",
|
||||
@@ -102,15 +111,20 @@ __all__ = [
|
||||
"CheckpointStorage",
|
||||
"ConcurrentBuilder",
|
||||
"Default",
|
||||
"Edge",
|
||||
"EdgeDuplicationError",
|
||||
"EdgeGroupDeliveryStatus",
|
||||
"Executor",
|
||||
"ExecutorCompletedEvent",
|
||||
"ExecutorEvent",
|
||||
"ExecutorFailedEvent",
|
||||
"ExecutorInvokeEvent",
|
||||
"FanInEdgeGroup",
|
||||
"FanOutEdgeGroup",
|
||||
"FileCheckpointStorage",
|
||||
"FunctionExecutor",
|
||||
"GraphConnectivityError",
|
||||
"HandlerOutputAnnotationError",
|
||||
"InMemoryCheckpointStorage",
|
||||
"InProcRunnerContext",
|
||||
"MagenticAgentDeltaEvent",
|
||||
@@ -137,11 +151,17 @@ __all__ = [
|
||||
"RequestInfoExecutor",
|
||||
"RequestInfoMessage",
|
||||
"RequestResponse",
|
||||
"Runner",
|
||||
"RunnerContext",
|
||||
"SequentialBuilder",
|
||||
"SharedState",
|
||||
"SingleEdgeGroup",
|
||||
"StandardMagenticManager",
|
||||
"SubWorkflowRequestInfo",
|
||||
"SubWorkflowResponse",
|
||||
"SwitchCaseEdgeGroup",
|
||||
"SwitchCaseEdgeGroupCase",
|
||||
"SwitchCaseEdgeGroupDefault",
|
||||
"TypeCompatibilityError",
|
||||
"ValidationTypeEnum",
|
||||
"Workflow",
|
||||
@@ -158,19 +178,18 @@ __all__ = [
|
||||
"WorkflowRunState",
|
||||
"WorkflowStartedEvent",
|
||||
"WorkflowStatusEvent",
|
||||
"WorkflowTracer",
|
||||
"WorkflowValidationError",
|
||||
"WorkflowViz",
|
||||
"__version__",
|
||||
"create_edge_runner",
|
||||
"executor",
|
||||
"handler",
|
||||
"intercepts_request",
|
||||
"validate_workflow_graph",
|
||||
"workflow_tracer",
|
||||
]
|
||||
|
||||
|
||||
# Rebuild models to resolve forward references after all imports are complete
|
||||
import contextlib
|
||||
|
||||
with contextlib.suppress(AttributeError, TypeError, ValueError):
|
||||
# Rebuild WorkflowExecutor to resolve Workflow forward reference
|
||||
WorkflowExecutor.model_rebuild()
|
||||
@@ -0,0 +1,186 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from ._agent import WorkflowAgent
|
||||
from ._checkpoint import (
|
||||
CheckpointStorage,
|
||||
FileCheckpointStorage,
|
||||
InMemoryCheckpointStorage,
|
||||
WorkflowCheckpoint,
|
||||
)
|
||||
from ._concurrent import ConcurrentBuilder
|
||||
from ._const import DEFAULT_MAX_ITERATIONS
|
||||
from ._edge import (
|
||||
Case,
|
||||
Default,
|
||||
Edge,
|
||||
FanInEdgeGroup,
|
||||
FanOutEdgeGroup,
|
||||
SingleEdgeGroup,
|
||||
SwitchCaseEdgeGroup,
|
||||
SwitchCaseEdgeGroupCase,
|
||||
SwitchCaseEdgeGroupDefault,
|
||||
)
|
||||
from ._edge_runner import create_edge_runner
|
||||
from ._events import (
|
||||
AgentRunEvent,
|
||||
AgentRunUpdateEvent,
|
||||
ExecutorCompletedEvent,
|
||||
ExecutorEvent,
|
||||
ExecutorFailedEvent,
|
||||
ExecutorInvokeEvent,
|
||||
RequestInfoEvent,
|
||||
WorkflowCompletedEvent,
|
||||
WorkflowErrorDetails,
|
||||
WorkflowEvent,
|
||||
WorkflowFailedEvent,
|
||||
WorkflowRunState,
|
||||
WorkflowStartedEvent,
|
||||
WorkflowStatusEvent,
|
||||
)
|
||||
from ._executor import (
|
||||
AgentExecutor,
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
Executor,
|
||||
RequestInfoExecutor,
|
||||
RequestInfoMessage,
|
||||
RequestResponse,
|
||||
SubWorkflowRequestInfo,
|
||||
SubWorkflowResponse,
|
||||
WorkflowExecutor,
|
||||
handler,
|
||||
intercepts_request,
|
||||
)
|
||||
from ._function_executor import FunctionExecutor, executor
|
||||
from ._magentic import (
|
||||
MagenticAgentDeltaEvent,
|
||||
MagenticAgentExecutor,
|
||||
MagenticAgentMessageEvent,
|
||||
MagenticBuilder,
|
||||
MagenticCallbackEvent,
|
||||
MagenticCallbackMode,
|
||||
MagenticContext,
|
||||
MagenticFinalResultEvent,
|
||||
MagenticManagerBase,
|
||||
MagenticOrchestratorExecutor,
|
||||
MagenticOrchestratorMessageEvent,
|
||||
MagenticPlanReviewDecision,
|
||||
MagenticPlanReviewReply,
|
||||
MagenticPlanReviewRequest,
|
||||
MagenticProgressLedger,
|
||||
MagenticProgressLedgerItem,
|
||||
MagenticRequestMessage,
|
||||
MagenticResponseMessage,
|
||||
MagenticStartMessage,
|
||||
StandardMagenticManager,
|
||||
)
|
||||
from ._runner import Runner
|
||||
from ._runner_context import (
|
||||
InProcRunnerContext,
|
||||
Message,
|
||||
RunnerContext,
|
||||
)
|
||||
from ._sequential import SequentialBuilder
|
||||
from ._shared_state import SharedState
|
||||
from ._telemetry import EdgeGroupDeliveryStatus, WorkflowTracer, workflow_tracer
|
||||
from ._validation import (
|
||||
EdgeDuplicationError,
|
||||
GraphConnectivityError,
|
||||
HandlerOutputAnnotationError,
|
||||
TypeCompatibilityError,
|
||||
ValidationTypeEnum,
|
||||
WorkflowValidationError,
|
||||
validate_workflow_graph,
|
||||
)
|
||||
from ._viz import WorkflowViz
|
||||
from ._workflow import Workflow, WorkflowBuilder, WorkflowRunResult
|
||||
from ._workflow_context import WorkflowContext
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_MAX_ITERATIONS",
|
||||
"AgentExecutor",
|
||||
"AgentExecutorRequest",
|
||||
"AgentExecutorResponse",
|
||||
"AgentRunEvent",
|
||||
"AgentRunUpdateEvent",
|
||||
"Case",
|
||||
"CheckpointStorage",
|
||||
"ConcurrentBuilder",
|
||||
"Default",
|
||||
"Edge",
|
||||
"EdgeDuplicationError",
|
||||
"EdgeGroupDeliveryStatus",
|
||||
"Executor",
|
||||
"ExecutorCompletedEvent",
|
||||
"ExecutorEvent",
|
||||
"ExecutorFailedEvent",
|
||||
"ExecutorInvokeEvent",
|
||||
"FanInEdgeGroup",
|
||||
"FanOutEdgeGroup",
|
||||
"FileCheckpointStorage",
|
||||
"FunctionExecutor",
|
||||
"GraphConnectivityError",
|
||||
"HandlerOutputAnnotationError",
|
||||
"InMemoryCheckpointStorage",
|
||||
"InProcRunnerContext",
|
||||
"MagenticAgentDeltaEvent",
|
||||
"MagenticAgentExecutor",
|
||||
"MagenticAgentMessageEvent",
|
||||
"MagenticBuilder",
|
||||
"MagenticCallbackEvent",
|
||||
"MagenticCallbackMode",
|
||||
"MagenticContext",
|
||||
"MagenticFinalResultEvent",
|
||||
"MagenticManagerBase",
|
||||
"MagenticOrchestratorExecutor",
|
||||
"MagenticOrchestratorMessageEvent",
|
||||
"MagenticPlanReviewDecision",
|
||||
"MagenticPlanReviewReply",
|
||||
"MagenticPlanReviewRequest",
|
||||
"MagenticProgressLedger",
|
||||
"MagenticProgressLedgerItem",
|
||||
"MagenticRequestMessage",
|
||||
"MagenticResponseMessage",
|
||||
"MagenticStartMessage",
|
||||
"Message",
|
||||
"RequestInfoEvent",
|
||||
"RequestInfoExecutor",
|
||||
"RequestInfoMessage",
|
||||
"RequestResponse",
|
||||
"Runner",
|
||||
"RunnerContext",
|
||||
"SequentialBuilder",
|
||||
"SharedState",
|
||||
"SingleEdgeGroup",
|
||||
"StandardMagenticManager",
|
||||
"SubWorkflowRequestInfo",
|
||||
"SubWorkflowResponse",
|
||||
"SwitchCaseEdgeGroup",
|
||||
"SwitchCaseEdgeGroupCase",
|
||||
"SwitchCaseEdgeGroupDefault",
|
||||
"TypeCompatibilityError",
|
||||
"ValidationTypeEnum",
|
||||
"Workflow",
|
||||
"WorkflowAgent",
|
||||
"WorkflowBuilder",
|
||||
"WorkflowCheckpoint",
|
||||
"WorkflowCompletedEvent",
|
||||
"WorkflowContext",
|
||||
"WorkflowErrorDetails",
|
||||
"WorkflowEvent",
|
||||
"WorkflowExecutor",
|
||||
"WorkflowFailedEvent",
|
||||
"WorkflowRunResult",
|
||||
"WorkflowRunState",
|
||||
"WorkflowStartedEvent",
|
||||
"WorkflowStatusEvent",
|
||||
"WorkflowTracer",
|
||||
"WorkflowValidationError",
|
||||
"WorkflowViz",
|
||||
"create_edge_runner",
|
||||
"executor",
|
||||
"handler",
|
||||
"intercepts_request",
|
||||
"validate_workflow_graph",
|
||||
"workflow_tracer",
|
||||
]
|
||||
+30
-20
@@ -6,6 +6,8 @@ from collections.abc import AsyncIterable, Sequence
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from agent_framework import (
|
||||
AgentRunResponse,
|
||||
AgentRunResponseUpdate,
|
||||
@@ -20,7 +22,6 @@ from agent_framework import (
|
||||
)
|
||||
from agent_framework._pydantic import AFBaseModel
|
||||
from agent_framework.exceptions import AgentExecutionException
|
||||
from pydantic import Field
|
||||
|
||||
from ._events import (
|
||||
AgentRunUpdateEvent,
|
||||
@@ -73,9 +74,10 @@ class WorkflowAgent(BaseAgent):
|
||||
kwargs["workflow"] = workflow
|
||||
|
||||
# Validate the workflow's start executor can handle agent-facing message inputs
|
||||
start_executor = workflow.get_start_executor()
|
||||
if start_executor is None:
|
||||
raise ValueError("Workflow's start executor is not defined.")
|
||||
try:
|
||||
start_executor = workflow.get_start_executor()
|
||||
except KeyError as exc: # Defensive: workflow lacks a configured entry point
|
||||
raise ValueError("Workflow's start executor is not defined.") from exc
|
||||
|
||||
if not start_executor.can_handle_type(list[ChatMessage]):
|
||||
raise ValueError("Workflow's start executor cannot handle list[ChatMessage]")
|
||||
@@ -256,6 +258,9 @@ class WorkflowAgent(BaseAgent):
|
||||
message_id=str(uuid.uuid4()),
|
||||
created_at=datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
)
|
||||
case _:
|
||||
# Ignore non-agent workflow events
|
||||
pass
|
||||
# We only care about the above two events and discard the rest.
|
||||
return None
|
||||
|
||||
@@ -349,16 +354,17 @@ class WorkflowAgent(BaseAgent):
|
||||
if current is None:
|
||||
return incoming
|
||||
raw_list: list[object] = []
|
||||
|
||||
def _add_raw(value: object) -> None:
|
||||
if isinstance(value, list):
|
||||
raw_list.extend(cast(list[object], value))
|
||||
else:
|
||||
raw_list.append(value)
|
||||
|
||||
if current.raw_representation is not None:
|
||||
if isinstance(current.raw_representation, list):
|
||||
raw_list.extend(current.raw_representation)
|
||||
else:
|
||||
raw_list.append(current.raw_representation)
|
||||
_add_raw(current.raw_representation)
|
||||
if incoming.raw_representation is not None:
|
||||
if isinstance(incoming.raw_representation, list):
|
||||
raw_list.extend(incoming.raw_representation)
|
||||
else:
|
||||
raw_list.append(incoming.raw_representation)
|
||||
_add_raw(incoming.raw_representation)
|
||||
return AgentRunResponse(
|
||||
messages=(current.messages or []) + (incoming.messages or []),
|
||||
response_id=current.response_id or incoming.response_id,
|
||||
@@ -407,11 +413,13 @@ class WorkflowAgent(BaseAgent):
|
||||
if merged_additional_properties is None:
|
||||
merged_additional_properties = {}
|
||||
merged_additional_properties.update(aggregated.additional_properties)
|
||||
if aggregated.raw_representation:
|
||||
if isinstance(aggregated.raw_representation, list):
|
||||
raw_representations.extend(aggregated.raw_representation)
|
||||
raw_value = aggregated.raw_representation
|
||||
if raw_value:
|
||||
cast_value = cast(object | list[object], raw_value)
|
||||
if isinstance(cast_value, list):
|
||||
raw_representations.extend(cast(list[object], cast_value))
|
||||
else:
|
||||
raw_representations.append(aggregated.raw_representation)
|
||||
raw_representations.append(cast_value)
|
||||
|
||||
# PHASE 3: HANDLE GLOBAL DANGLING UPDATES (NO RESPONSE_ID)
|
||||
if global_dangling:
|
||||
@@ -427,11 +435,13 @@ class WorkflowAgent(BaseAgent):
|
||||
if merged_additional_properties is None:
|
||||
merged_additional_properties = {}
|
||||
merged_additional_properties.update(flattened.additional_properties)
|
||||
if flattened.raw_representation:
|
||||
if isinstance(flattened.raw_representation, list):
|
||||
raw_representations.extend(flattened.raw_representation)
|
||||
flat_raw = flattened.raw_representation
|
||||
if flat_raw:
|
||||
cast_flat = cast(object | list[object], flat_raw)
|
||||
if isinstance(cast_flat, list):
|
||||
raw_representations.extend(cast(list[object], cast_flat))
|
||||
else:
|
||||
raw_representations.append(flattened.raw_representation)
|
||||
raw_representations.append(cast_flat)
|
||||
|
||||
# PHASE 4: CONSTRUCT FINAL RESPONSE WITH INPUT RESPONSE_ID
|
||||
return AgentRunResponse(
|
||||
+1
-1
@@ -177,7 +177,7 @@ class ConcurrentBuilder:
|
||||
|
||||
Usage:
|
||||
```python
|
||||
from agent_framework.workflow import ConcurrentBuilder
|
||||
from agent_framework import ConcurrentBuilder
|
||||
|
||||
# Minimal: use default aggregator (returns list[ChatMessage])
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).build()
|
||||
+3
-2
@@ -6,10 +6,11 @@ from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from agent_framework._pydantic import AFBaseModel
|
||||
from pydantic import Field
|
||||
|
||||
from agent_framework_workflow._executor import Executor
|
||||
from agent_framework._pydantic import AFBaseModel
|
||||
|
||||
from ._executor import Executor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+2
-1
@@ -12,9 +12,10 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, get_args, get_or
|
||||
if TYPE_CHECKING:
|
||||
from ._workflow import Workflow
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from agent_framework import AgentProtocol, AgentRunResponse, AgentRunResponseUpdate, AgentThread, ChatMessage
|
||||
from agent_framework._pydantic import AFBaseModel
|
||||
from pydantic import Field
|
||||
|
||||
from ._events import (
|
||||
AgentRunEvent,
|
||||
+2
-1
@@ -13,6 +13,8 @@ from enum import Enum
|
||||
from typing import Annotated, Any, Literal, Protocol, TypeVar, Union, cast
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from agent_framework import (
|
||||
AgentProtocol,
|
||||
AgentRunResponse,
|
||||
@@ -25,7 +27,6 @@ from agent_framework import (
|
||||
)
|
||||
from agent_framework._agents import BaseAgent
|
||||
from agent_framework._pydantic import AFBaseModel
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from ._events import WorkflowCompletedEvent, WorkflowEvent
|
||||
from ._executor import Executor, RequestInfoMessage, RequestResponse, handler
|
||||
+1
-1
@@ -103,7 +103,7 @@ class SequentialBuilder:
|
||||
|
||||
Usage:
|
||||
```python
|
||||
from agent_framework.workflow import SequentialBuilder
|
||||
from agent_framework import SequentialBuilder
|
||||
|
||||
workflow = SequentialBuilder().participants([agent1, agent2, summarizer_exec]).build()
|
||||
```
|
||||
+2
-1
@@ -3,11 +3,12 @@
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from agent_framework._pydantic import AFBaseSettings
|
||||
from opentelemetry.trace import Link, NoOpTracer, SpanKind, StatusCode, get_current_span, get_tracer
|
||||
from opentelemetry.trace.span import SpanContext
|
||||
from opentelemetry.util.types import Attributes
|
||||
|
||||
from agent_framework._pydantic import AFBaseSettings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._workflow import Workflow
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user