创建代理

代理是一个代码库,它与 AI Shell 接口,可与特定大型语言模型或其他帮助提供程序通信。 用户使用自然语言与代理聊天以获取所需的输出或帮助。 代理作为 C# 类实现,用于从 ILLMAgent 包实现 AIShell.Abstraction 接口。

有关 AIShell.Abstraction 层和 AIShell.Kernel的详细信息,请参阅 AI Shell 体系结构 文档。

本文是创建 Ollama 语言模型代理的分步指南。 本文的目的是提供一个如何创建代理的简单示例。 存储库的文件夹中有更健壮的 Ollama 代理 AIShell.Ollama.Agent 实现。

先决条件

  • .NET 8 SDK 或更高版本
  • PowerShell 7.4.6 或更高版本

创建代理的步骤

在此示例中,我们创建一个代理,以使用 Ollama 与语言模型phi3进行通信。 Ollama 是一个 CLI 工具,用于管理和使用本地构建的 LLM/SLM。

步骤 1:创建新项目

第一步是创建一个新的 classlib 项目。

  1. 创建名为 OllamaAgent 的新文件夹

  2. 运行以下命令以创建新项目:

    dotnet new classlib
    

步骤 2:添加必要的包

在新创建的项目中,需要从 NuGet 库安装 AIShell.Abstraction 包。 使用以下命令安装 NuGet 包:

dotnet add package AIShell.Abstraction --version 1.0.0-preview.2

此命令将包添加到 .csproj 文件。 .csproj 文件应包含以下 XML:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="AIShell.Abstraction" Version="1.0.0-preview.2" />
  </ItemGroup>
</Project>

重要

请务必检查 NuGet 库中的最新版本。

步骤 3:实现代理类

若要实现 ILLMAgent 接口,请修改 Class1.cs 文件。

  1. 将文件重命名为 OllamaAgent.cs
  2. 将类重命名为 OllamaAgent
  3. 在实现中添加代码使用的 .NET 命名空间
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using AIShell.Abstraction;

namespace AIShell.Ollama.Agent;

public sealed class OllamaAgent : ILLMAgent
{

}

步骤 4:添加必要的类成员和方法

接下来,实现代理类的必要变量和方法。 注释提供 OllamaAgent 类的成员的说明。 该 _chatService 成员是 OllamaChatService 类的实例,您将在后续步骤中实现该类。

public sealed class OllamaAgent : ILLMAgent
{
    /// <summary>
    /// The name of the agent
    /// </summary>
    public string Name => "ollama";

    /// <summary>
    /// The description of the agent to be shown at start up
    /// </summary>
    public string Description => "This is an AI assistant that uses the Ollama CLI tool. Be sure to follow all prerequisites in https://aka.ms/ollama/readme";

    /// <summary>
    /// This is the company added to `/like` and `/dislike` verbiage for who the telemetry helps.
    /// </summary>
    public string Company => "Microsoft";

    /// <summary>
    /// These are samples that are shown at start up for good questions to ask the agent
    /// </summary>
    public List<string> SampleQueries => [
        "How do I list files in a given directory?"
    ];

    /// <summary>
    /// These are any optional legal/additional information links you want to provide at start up
    /// </summary>
    public Dictionary<string, string> LegalLinks { private set; get; }

    /// <summary>
    /// This is the chat service to call the API from
    /// </summary>
    private OllamaChatService _chatService;

    /// <summary>
    /// A string builder to render the text at the end
    /// </summary>
    private StringBuilder _text;

    /// <summary>
    /// Dispose method to clean up the unmanaged resource of the chatService
    /// </summary>
    public void Dispose()
    {
        _chatService?.Dispose();
    }

    /// <summary>
    /// Initializing function for the class when the shell registers an agent
    /// </summary>
    /// <param name="config">Agent configuration for any configuration file and other settings</param>
    public void Initialize(AgentConfig config)
    {
        _text = new StringBuilder();
        _chatService = new OllamaChatService();

        LegalLinks = new(StringComparer.OrdinalIgnoreCase)
        {
            ["Ollama Docs"] = "https://github.com/ollama/ollama",
            ["Prerequisites"] = "https://aka.ms/ollama/readme"
        };

    }

    /// <summary>
    /// Get commands that an agent can register to the shell when being loaded
    /// </summary>
    public IEnumerable<CommandBase> GetCommands() => null;

    /// <summary>
    /// Gets the path to the setting file of the agent.
    /// </summary>
    public string SettingFile { private set; get; } = null;

    /// <summary>
    /// Refresh the current chat by starting a new chat session.
    /// An agent can reset chat states in this method.
    /// </summary>
    public void RefreshChat() {}

    /// <summary>
    /// Gets a value indicating whether the agent accepts a specific user action feedback.
    /// </summary>
    /// <param name="action">The user action.</param>
    public bool CanAcceptFeedback(UserAction action) => false;

    /// <summary>
    /// A user action was taken against the last response from this agent.
    /// </summary>
    /// <param name="action">Type of the action.</param>
    /// <param name="actionPayload"></param>
    public void OnUserAction(UserActionPayload actionPayload) {}

    /// <summary>
    /// Main chat function that takes
    /// </summary>
    /// <param name="input">The user input from the chat experience</param>
    /// <param name="shell">The shell that provides host functionality</param>
    /// <returns>Task Boolean that indicates whether the query was served by the agent.</returns>
    public async Task<bool> Chat(string input, IShell shell)
    {

    }
}

对于初始实现,代理返回 “Hello World!”,证明您创建了正确的接口。 您还需要添加一个 try-catch 块,以便在用户尝试取消作时捕获和处理任何异常。

将以下代码添加到 Chat 方法。

public async Task<bool> Chat(string input, IShell shell)
{
    // Get the shell host
    IHost host = shell.Host;

    // get the cancellation token
    CancellationToken token = shell.CancellationToken;

    try
    {
       host.RenderFullResponse("Hello World!");
    }
    catch (OperationCanceledException e)
    {
        _text.AppendLine(e.ToString());

        host.RenderFullResponse(_text.ToString());

        return false;
    }

    return true;
}

步骤 5:添加 Ollama 检查

接下来,需要确保 Ollama 正在运行。

public async Task<bool> Chat(string input, IShell shell)
{
    // Get the shell host
    IHost host = shell.Host;

    // get the cancellation token
    CancellationToken token = shell.CancellationToken;

    if (Process.GetProcessesByName("ollama").Length is 0)
    {
        host.RenderFullResponse("Please be sure that Ollama is installed and the server is running. Ensure that you have met all the prerequisites in the README for this agent.");
        return false;
    }

    // Calls to the API will go here

    return true;
}

步骤 6:创建数据结构以与聊天服务交换数据

在使用 Ollama API 之前,需要创建向 Ollama API 发送输入并从中接收响应的类。 以下 Ollama 示例 显示代理的输入和响应的格式。

此示例在禁用流式处理的情况下调用 Ollama API。 Ollama 生成单个固定响应。 将来,可以添加流式处理功能,以便在代理接收响应时实时呈现响应。

若要定义数据结构,请在名为 OllamaSchema.cs的同一文件夹中创建新文件。 将以下代码复制到该文件。

namespace AIShell.Ollama.Agent;

// Query class for the data to send to the endpoint
internal class Query
{
    public string prompt { get; set; }
    public string model { get; set; }

    public bool stream { get; set; }
}

// Response data schema
internal class ResponseData
{
    public string model { get; set; }
    public string created_at { get; set; }
    public string response { get; set; }
    public bool done { get; set; }
    public string done_reason { get; set; }
    public int[] context { get; set; }
    public double total_duration { get; set; }
    public long load_duration { get; set; }
    public int prompt_eval_count { get; set; }
    public int prompt_eval_duration { get; set; }
    public int eval_count { get; set; }
    public long eval_duration { get; set; }
}

internal class OllamaResponse
{
    public int Status { get; set; }
    public string Error { get; set; }
    public string Api_version { get; set; }
    public ResponseData Data { get; set; }
}

现在,你已准备好构建使用 Ollama API 的聊天服务所需的部分。 不需要单独的聊天服务类,但抽象化对 API 的调用很有用。

在代理所在的同一文件夹中创建名为 OllamaChatService.cs 的新文件。 将示例代码复制到文件中。

提示

此示例对 Ollama API 使用硬编码的终结点和语言模型。 将来,可以在代理配置文件中定义可配置的参数。

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

using AIShell.Abstraction;

namespace AIShell.Ollama.Agent;

internal class OllamaChatService : IDisposable
{
    /// <summary>
    /// Ollama endpoint to call to generate a response
    /// </summary>
    internal const string Endpoint = "http://localhost:11434/api/generate";

    /// <summary>
    /// Http client
    /// </summary>
    private readonly HttpClient _client;

    /// <summary>
    /// Initialization method to initialize the http client
    /// </summary>

    internal OllamaChatService()
    {
        _client = new HttpClient();
    }

    /// <summary>
    /// Dispose of the http client
    /// </summary>
    public void Dispose()
    {
        _client.Dispose();
    }

    /// <summary>
    /// Preparing chat with data to be sent
    /// </summary>
    /// <param name="input">The user input from the chat experience</param>
    /// <returns>The HTTP request message</returns>
    private HttpRequestMessage PrepareForChat(string input)
    {
        // Main data to send to the endpoint
        var requestData = new Query
        {
            model = "phi3",
            prompt = input,
            stream = false
        };

        var json = JsonSerializer.Serialize(requestData);

        var data = new StringContent(json, Encoding.UTF8, "application/json");
        var request = new HttpRequestMessage(HttpMethod.Post, Endpoint) { Content = data };

        return request;
    }

    /// <summary>
    /// Getting the chat response async
    /// </summary>
    /// <param name="context">Interface for the status context used when displaying a spinner.</param>
    /// <param name="input">The user input from the chat experience</param>
    /// <param name="cancellationToken">The cancellation token to exit out of request</param>
    /// <returns>Response data from the API call</returns>
    internal async Task<ResponseData> GetChatResponseAsync(IStatusContext context, string input, CancellationToken cancellationToken)
    {
        try
        {
            HttpRequestMessage request = PrepareForChat(input);
            HttpResponseMessage response = await _client.SendAsync(request, cancellationToken);
            response.EnsureSuccessStatusCode();

            context?.Status("Receiving Payload ...");
            Console.Write(response.Content);
            var content = await response.Content.ReadAsStreamAsync(cancellationToken);
            return JsonSerializer.Deserialize<ResponseData>(content);
        }
        catch (OperationCanceledException)
        {
            // Operation was cancelled by user.
        }

        return null;
    }
}

步骤 7:调用聊天服务

接下来,需要在主代理类中调用聊天服务。 修改 Chat() 方法以调用聊天服务并向用户呈现响应。 以下示例显示了已完成的 Chat() 方法。

public async Task<bool> Chat(string input, IShell shell)
{
    // Get the shell host
    IHost host = shell.Host;

    // get the cancellation token
    CancellationToken token = shell.CancellationToken;

    if (Process.GetProcessesByName("ollama").Length is 0)
    {
        host.RenderFullResponse("Please be sure that Ollama is installed and the server is running. Ensure that you have met all the prerequisites in the README for this agent.");
        return false;
    }

    ResponseData ollamaResponse = await host.RunWithSpinnerAsync(
        status: "Thinking ...",
        func: async context => await _chatService.GetChatResponseAsync(context, input, token)
    ).ConfigureAwait(false);

    if (ollamaResponse is not null)
    {
        // render the content
        host.RenderFullResponse(ollamaResponse.response);
    }

    return true;
}

代理代码已完成。

步骤 8:生成并测试代理

接下来,需要生成并测试代码是否按预期工作。 运行以下命令:

dotnet build

此命令在项目的 \bin\Debug\net8.0 文件夹中生成所有必要的包。

若要 aish 加载代理,需要将 .dll 文件复制到 Agents 文件夹中的文件夹。 文件夹名称应与代理名称相同。

您可以在以下两个位置之一安装代理:

  • 在安装 Agents位置下的 aish.exe 文件夹中。 用于 AI Shell 的 [安装脚本][08] 安装在 %LOCALAPPDATA%\Programs\AIShell中。 创建 %LOCALAPPDATA%\Programs\AIShell\Agents\OllamaAgent 文件夹。
  • 或者,在 %USERPROFILE%\.aish\Agents中安装代理。 创建 %USERPROFILE%\.aish\Agents\OllamaAgent 文件夹。

.dll 文件复制到创建的代理文件夹。 启动 aish时,应会看到代理。

AI Shell
v1.0.0-preview.2

Please select an agent to use:

    azure
   >ollama
    openai-gpt

如何共享自己的代理?

无法在集中式存储库中共享您的代理。 建议为此存储库开发自己的代理。 可以在此存储库的“Agent Sharing”选项卡的 部分中共享分支的链接。 要使用代理,请将代理 dll 文件 agents 放在 的基目录 aish.exe文件夹中。 AI Shell 会自动从该文件夹加载代理。