using System.Threading.Tasks; using OllamaSharp; using OllamaSharp.Models; using OllamaSharp.Models.Chat; using Xunit.Abstractions; namespace OllamaStudy.UseOllamaSharp { public class OllamaApiTest { private readonly ITestOutputHelper _output; private readonly IOptionsMonitor _optionsMonitor; private readonly IConfiguration _configuration; private readonly IOllamaApiClient _ollamaApiClient; public OllamaApiTest ( ITestOutputHelper outputHelper, IOptionsMonitor optionsMonitor, IConfiguration configuration, IOllamaApiClient ollamaApiClient ) { _output = outputHelper; _optionsMonitor = optionsMonitor; _configuration = configuration; _ollamaApiClient = ollamaApiClient; } #region 配置与ID [Fact] public void Config_Test() { var ollamaServerOption = new OllamaServerOption(); _configuration.GetSection("OllamaServer").Bind(ollamaServerOption); Assert.NotNull(ollamaServerOption); Assert.NotNull(ollamaServerOption.OllamaServerUrl); Assert.NotNull(ollamaServerOption.Model); } [Fact] public void OllamaOption_Test() { var options = _optionsMonitor.CurrentValue; Assert.NotNull(options); Assert.NotNull(options.Model); Assert.NotNull(options.OllamaServerUrl); } #endregion #region 生成补全 /// /// 流式生成请求 /// [Fact] public async Task Completion_Request_Streaming_Test() { var generateRequest = new GenerateRequest() { Prompt = """天空为什么是蓝色的?""", //Stream = false, }; IAsyncEnumerable? response = _ollamaApiClient.GenerateAsync(generateRequest); StringBuilder stringBuilder = new StringBuilder(); await foreach (GenerateResponseStream? stream in response) { stringBuilder.Append(stream?.Response); } _output.WriteLine(stringBuilder.ToString()); } /// /// 流式生成请求 /// [Fact] public async Task Completion_Request_NoStreaming_Test() { var generateRequest = new GenerateRequest() { Prompt = """天空为什么是蓝色的?""", Stream = false, }; var responseStream = await _ollamaApiClient.GenerateAsync(generateRequest).StreamToEndAsync(); //处理响应 var responseText = responseStream?.Response; Assert.NotNull(responseText); _output.WriteLine(responseText); } /// /// 带后缀的生成请求 /// /// [Fact] public async Task Completion_Request_WithSuffix_Test() { var generateRequest = new GenerateRequest() { Model = ModelSelecter.ModelWithSuffixAndImage, Prompt = """def compute_gcd(a, b):""", //(可选)模型响应后的文本 Suffix = " return result", Options = new RequestOptions() { Temperature = 0, }, Stream = false, }; var responseStream = await _ollamaApiClient.GenerateAsync(generateRequest).StreamToEndAsync(); //处理响应 Assert.NotNull(responseStream); Assert.NotNull(responseStream.Response); _output.WriteLine(responseStream?.Response); } /// /// 结构化输出请求 /// [Fact] public async Task completion_request_structuredoutputs_test() { var generateRequest = new GenerateRequest() { Prompt = "22岁的 ollama 正忙于拯救世界。使用json响应。", Format = new { type = "object", properties = new { age = new { type = "integer" }, available = new { type = "boolean" } }, required = new string[] { "age", "available" } }, Options = new RequestOptions() { Temperature = 0, }, Stream = false, }; //处理响应 GenerateDoneResponseStream? responseStream = await _ollamaApiClient.GenerateAsync(generateRequest).StreamToEndAsync(); Assert.NotNull(responseStream); Assert.NotNull(responseStream.Response); Assert.True(responseStream.Done); _output.WriteLine(responseStream.Response); } /// /// json模式请求 /// [Fact] public async Task Completion_Request_JsonMode_Test() { var generateRequest = new GenerateRequest() { Prompt = "一天中不同时间的天空是什么颜色的?使用JSON进行响应!", Format = "json", Options = new RequestOptions() { Temperature = 0, }, Stream = false, }; //处理响应 GenerateDoneResponseStream? responseStream = await _ollamaApiClient.GenerateAsync(generateRequest).StreamToEndAsync(); var responseObject = JsonSerializer.Deserialize(responseStream?.Response ?? "{}"); Assert.NotNull(responseStream); Assert.NotNull(responseStream.Response); Assert.True(responseStream.Done); _output.WriteLine(responseStream.Response); } /// /// 带图像的生成请求 /// [Fact] public async Task Completion_Request_WhithImages_Test() { var generateRequest = new GenerateRequest() { // (必需) 模型名称 Model = ModelSelecter.ModelWithSuffixAndImage, // (必需) 生成响应的提示词 Prompt = "这张照片里有什么东西?", //(可选)Base64 编码的图像列表(适用于 LLAVA 等多模态模型) Images = new string[] { "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" }, //(可选)流式处理响应: 可通过设置 false 来禁用流式处理 Stream = false, //(可选)控制模型在请求后加载到内存中的时间(默认值:5M) KeepAlive = "5m", }; //处理响应 var response = await _ollamaApiClient.GenerateAsync(generateRequest).StreamToEndAsync(); _output.WriteLine(response?.Response); Assert.NotNull(response); Assert.NotEmpty(response.Response); } /// /// 原始模式请求 /// 在某些情况下,您可能希望绕过模板系统并提供完整的提示。在这种情况下,您可以使用raw参数禁用模板。还要注意,原始模式不会返回上下文。 /// /// [Fact] public async Task Completion_Request_RawMode_Test() { var generateRequest = new GenerateRequest() { Model = ModelSelecter.ModelWithRawmodel, Prompt = """[INST] 天空为什么是蓝色的? [/INST]""", Stream = false, Raw = true, }; //处理响应 var response = await _ollamaApiClient.GenerateAsync(generateRequest).StreamToEndAsync(); _output.WriteLine(response?.Response); Assert.NotNull(response); Assert.NotEmpty(response.Response); } /// /// 可复现输出的请求 /// /// [Fact] public async Task Completion_Request_ReproducibleOutputs_Test() { //第一次请求 var generateRequest1 = new GenerateRequest() { Prompt = """中华人民共和国的首都是不是北京?回答是或否""", Stream = false, Options = new RequestOptions() { Seed = 999999999, Temperature = 0, }, }; //处理响应 var response1 = await _ollamaApiClient.GenerateAsync(generateRequest1).StreamToEndAsync(); _output.WriteLine(response1?.Response); var response1Text = response1?.Response; //第二次请求 var generateRequest2 = new GenerateRequest() { Prompt = """中华人民共和国的首都是不是北京?回答是或否""", Stream = false, Options = new RequestOptions() { Seed = 999999999, Temperature = 0, } }; //处理响应 var response2 = await _ollamaApiClient.GenerateAsync(generateRequest2).StreamToEndAsync(); _output.WriteLine(response2?.Response); var response2Text = response2?.Response; Assert.Equal(response1Text, response2Text); } /// /// 参数化生成请求 /// [Fact] public async Task Completion_Request_Options_Test() { //使用提供的模型为给定的提示生成响应。这是一个流式端点,因此会有一系列响应。最终的响应对象将包括请求中的统计数据和其他数据。 var generateRequest = new GenerateRequest() { /*基础选项*/ // (必需) 模型名称 Model = ModelSelecter.ModelWithSuffixAndImage, // (必需) 生成响应的提示词 Prompt = "天空为什么是蓝色的?", //(可选)模型响应后的文本:只有专用模型才支持(qwen2.5-coder:3b等), 模型不支持则异常 //Suffix for Fill-In-the-Middle generate //Suffix = " return result", //(可选)Base64 编码的图像列表(适用于 LLAVA 等多模态模型) //Images = new string[] //{ // "iVBORw0KGgoAAAANSUhEUgAAADkAAAA8CAYAAADc1RI2AAAACXBIWXMAABnWAAAZ1gEY0crtAAAAEXRFWHRTb2Z0d2FyZQBTbmlwYXN0ZV0Xzt0AAAAXdEVYdFVzZXIgQ29tbWVudABTY3JlZW5zaG9093UNRwAADM9JREFUaIHVm99PG1ebxz/nzNgGxzbgHwSMkyAICUWwSZ1Cfv8g0Dbv26qJ1Kiq1MtdqbtdaaW97FX3L+gqN91eZKXepIpU9aa9WHUbNZHyo0kgVGmhaSIEBRJwDHZiY/xjxjNnLxLPQt+q79t42u1+pZERnjl+vuc8z3me8z1nBD+BUmrAtu1/EEK8BGwBNv30nj8g1oAFpdR/SynPCiG+W/+lqP2hlPLZtv3vQoh/XP//Xwul1C9+L4T4m+6pA0op9aGU8l+FEBV4SkYp5VNK/RcwXE/rT9tCKYVt20gpAbBtm0qlQrVaBUBKicfjwePxOISUUgghnMsFXBRC/EkIUREAlmV9IIT4JzdaXk+yWCySyWTIZrMsLi7y6NEjqtUqwWCQtrY2EokEsViMxsZGpJROx7hEEqXUB5qm/bNQSg0opW5Th4v+pGGq1SoPHjzgzp07jI+Pk0qlsG0by7KQUjoj3N7ezqFDh9i9ezeBQABN09wcSQAlhNglLMs6I4T4l7paWhdjhmEwNTXFhQsXmJubw7ZtACzLAkDTNGzbdtwzGAyyf/9+EokE0WiU9vZ2GhsbnfbqJayUOiMsy7ojhOitp6EaEYBLly7xySefkMvl0DTNGR0Aj8dT+2E0TdvgmkII4vE4yWSSvXv3EgqFEEI4o14HftB5kibqQm3GTKVSfPjhh/z444/ouo7H48Hr9TqftSsYDKLr+obOAXjw4AHpdJpyuczIyAiBQKBe0wC2SFzMg5cuXWJmZgalFJZlUS6XWVtbo1Ao8PjxY6rVKv39/Zw4cYK+vj6UUlQqlQ3ubpomV65c4c6dO46L14lNuhut1HDz5k0n1izLQtM0vF4vXV1d9Pb2snXrVoaGhkgkEszNzbGyssLk5CQejwefz4fX60VKSblc5saNGwwMDKDr9ZvoKsn79+8DT2JUCEEoFGJ4eJgjR47Q29tLIBDA5/OhlKKzs5OhoSFmZ2dJp9M0NDTQ0NDgkJ2ZmeHx48cbJqFnhQ7/m4jrRblcdgj6/X5eeukl3nzzTTZv3uxMQKZpUi6XkVLS39/P119/zcOHD6lUKti2jWma+Hw+TNMklUrR3t7uDkk3UMtv6ysdpRRtbW1omkY+n+fatWtMTEywtraGpmns27eP1tZWGhoaKJVKG0o+y7LI5/Ou2OYKyZphuq4jpaStrY2Ojg7Gx8f57LPPOHXqFGtrayilGBoawuv1YpomwWCQYrGI3+9nbW3N6aD1OdUNSKg/4cKTOGxubnYmnWg0ytDQEGNjY058RiIR8vk8hmGQSCTYsWMHu3fvJhKJYNs21WrVuZRSdHR01G0XPCVZL2pJu6urC4BsNsv4+Dh3797F7/cDT0Z7YWGB69evMz09zfT0NGtra3R2dvL222+TTCadDrJtm1gsRjQadcM8dLfqxBrRY8eO8corryCl5Ny5c/T09ADg9/sZGRnhwIEDBAIBpJT4fD50Xaenp4ejR48yNzdHNpulWq0Sj8ddSR8Auhszay0mDcPg3XffJRQKkUqlyOfzbN26FXiyvGpqaqKpqekvSrUaYaUUJ0+epLGx0Zml3YCredLv99PQ0OCUbM3NzTz//PMopZBSOp/rja910OzsLH6/n5WVFQ4ePPiznfGscKWV2hqyUCgwMTGBbdtkMhkKhQKTk5MsLS1hmubPGi2EoFAocO/ePQYHB3n11Vf56KOPWFlZcW0ktffee+/f6m2sliMNw2B2dpaBgQE+/fRTbt68ydLSEtlsFk3TiMfjzv01KKUwDIOuri4OHjzoxGFHRwft7e2upBFX3fXQoUOcOXMGj8fDG2+8wZYtWzh48CCxWGzDkmo9lFIEAgF6enooFApcuXKFnTt3smvXLtcmHnec/in8fj+GYaCUwufzkUqleP/997l79y6ZTMYp+9aj5gWlUolz585hmibRaNQ1guDySAohWF5eZnV1le+//57z588jpeSbb76hu7ubEydOcOTIESKRyIbnlFLMzc1hWRbHjx+no6Pjryp6vwaukKzVnEop+vv7mZqaYseOHQwPD7O4uMimTZswDINMJsPq6irhcNh51jRNDMNgZWWFY8eOOQWAizoPwrIs5VZZJ4Tgxo0bXLt2jXfeeYdMJkMqlcLj8RAOh2lvb98gQa6srJBOpx1ZpKOjg0Ag4CpBcNldAQYGBpiammJ+fp7u7m7i8fiGpdx6Ne/atWtIKenr6yMajTpk3YYrI7k+fpRSZDIZbt26RV9fH62trXi9Xsel19bWWFpaYnp6Gp/PRzKZJJ1O4/V6icfjzr1uwlWSNSKWZZFOp1lcXHQWwbXlVC6XQ9d1tm3bRkdHB1JKMpkMUkqCweDPqurrO/FZbHVt4lkPKSUtLS18++23fPHFF4RCIQYGBmhsbCQYDNLd3U0sFkPTNCdPTkxM0NHRQSKR2FAA1Aiu78hfC9djEp6s6m/fvs2XX35JqVQiFouxurrqaDi1fZFaLtR1neXlZWZmZjhw4ABer9chFQgEaG5udureZ4HrJG3b5s6dO3z++eek02kaGxtJpVIUCgXK5TK6rpNIJDYk+9roTExMMDU1hWEYjpuOjo5y4sSJuuLUVfkDYHV1lUuXLlEulymXy5RKJSqVCn19fezZs4eurq4NeRJgYWGB2dlZyuUy8/PzTmFg2zb79u2r2z5XyrpajlRKcfHiRebn5zl8+DCmaSKEoLe3l9dff53+/n7C4fCGUSkWi9y8eZMHDx4ghCASidDW1ubc09nZWfds65r8cfv2bc6fP++o6L29vRw9epSmpiaGh4fZtWsXDQ0NGwy2bZvr168zPj5OsVgEnsTn5s2bCYVCaJrmLLrrgSvuWq1WOXv2LMVi0dkDyeVynD59mp6eHiKRCF6vF9io8f7www989dVXzMzMoGnaBoE5kUhgmuZf1LnPAtdI1gxNJBKEw2Gmp6dJJpPouk4mk9kgZyilSKfTXLlyhfn5earVKoZhYBgGhUIBXdfx+XycOnXKmaDqcVlX3NXn89HX14eUkocPHyKEYGFhgUwmw8cff8zZs2e5cOGCY2ylUuHq1atcvnyZarVKOBx25I5qteqo6cePH9+wxf6sRF0hKaVkcHCQlpYWGhsbWVpa4tGjR9y4cYOxsTHy+Tzj4+NMT08D8N1333H58mVWVlbI5/Pk83k8Hg/RaJRwOExjYyPJZJJQKOSGee5pPP39/fh8PkKhEC0tLU6y9/v95HI5isUi2WyWmZkZLl68yL1795BSYhgGq6urpNNpp2CIRCKMjo46cVwvXJtdt2zZgs/nc2rQ/fv309raimmatLa2Eo/H2b17N/Pz89y/fx+fz0e1WnVUvFKpxOLiIjMzMwQCAbZsqXtv2IFrFY/f76dYLLK8vEwsFkMpRS6XwzAMlpeXKRaLTE5OcuvWLSzLIh6PY5omuVyOx48fO/uZtm3z4osv4vf7/3i6q5SSWCzG5OQkKysrdHd3EwqFCIfDlEol5ubmuHr1Krdu3cK2bUKhEF6vl2g0SiQSoVgsksvlGBwcZM+ePa6uLSX89VNUfyt27txJqVTCsixmZ2cZGxtDSkkgEKBarTI2NkalUnHkjkKhgGVZCCEIBALs2rWLt956i1Ao5JqwDC6qdUop9u3bx6ZNT44g3L9/n9nZWQqFgnPao6mpiZaWFnRdxzRN0uk02WwWwzBoaWnh5MmTxONxZ8P2D6egCyHo6elhz5496LpONpvFNE2y2eyGexsaGojFYrS1tREMBikUCkgpefnll+nr63NViqxBunwCisHBQTZt2uTsM5ZKJedMnfOjUjrxmEwmOX36NAMDA7+ZxuOquAzQ1dVFc3Mz1WoV27YxDINcLudsrMKTkdd1nXg8zmuvvcaRI0cIBoNuHzlz4Lq43NbWhmmajlJeE68sy3JOf/j9frq7uxkZGWH79u2/iYuuh16PdlLD+mebm5vJ5/MIIdA0Db/fj9frdSSPSCRCb28vx48fp7Oz07VzAb8EVxV0eFKsd3Z2ks1mnUnG5/NRKpWQUjIwMMDo6Cjbtm1zNU38Elz3E6/Xy+joKJOTk7S0tBAMBpFSEgqFeO655xgZGXFW+79F/P0cdGBNCFH3+bqawbqus3fvXsLhsEMwEAjwwgsvcPjwYVpbW3+3EXyKNQksuNmiEIJYLEYymXRUtwMHDjA8PExra+vvNnrrsODKod71qCnoMzMzpNNpfD4f27dvp6mpCeB3ddOn9pz5TY5n1z5r109n0N+RpBJC/J0UQnynlPrQrVZrIyWlRNM0dF13RcJ4Fiil/kMIMen6KxN/IHwlhPizEKJSO1tXEUL8SSn1AeDePvb/DZRS6oPaOyHwM3H49DWmvxdCvMz/v9eYvpBS/qf4yWtM/wPjku1ykLIL1wAAAABJRU5ErkJggg==", // "base64" //}, /*高级选项*/ //(可选)返回响应的格式:json 或 JSON schema //Format = "json", //选项参数 Options = new RequestOptions() { //设置模型回溯多长时间以防止重复(0 = 禁用,-1 = num_ctx) RepeatLastN = 60, //设置对重复项的惩罚力度(越低越宽松):较高的值(例如1.5)将更严厉地惩罚重复,而较低的值(如0.9)将更宽容。(默认值:1.1) RepeatPenalty = 1.1f, //模型的温度:值越大,模型的答案越具有创造性。(默认值:0.8) Temperature = 0.8f, //设置用于生成的随机数种子。将其设置为特定数字将使模型为同一提示生成相同的文本。(默认值:0) Seed = 0, //设置要使用的停止序列。遇到此模式时,LLM 将停止生成文本并返回。通过在模型文件中指定多个单独的停止参数,可以设置多个停止模式 Stop = new string[] { "exc", "stop" }, //生成文本时要预测的最大令牌数。(默认值:-1,无限生成) NumPredict = -1, //降低产生无意义的可能性。较高的值(例如 100)将给出更多样化的答案,而较低的值(例如 10)将更保守。(默认值:40) TopK = 40, //与 top-k 一起使用。较高的值(例如 0.95)将导致文本更加多样化,而较低的值(例如 0.5)将生成更集中和保守的文本。(默认值:0.9) TopP = 0.9f, //启用F16键值对 (默认值:false) F16kv = false, //根据令牌在提示中的出现频率对其进行处罚 FrequencyPenalty = 0.0f, //返回所有令牌的logits,而不仅仅是最后一个。(默认值:False) LogitsAll = false, //启用低VRAM模式(默认值:False) LowVram = false, //此选项控制哪个GPU用于小张量。在所有GPU上分割计算的开销是不值得的。GPU将使用稍多的VRAM来存储临时结果的暂存缓冲区。默认情况下,使用GPU 0。 MainGpu = null, //top_p的替代方案,旨在确保质量和多样性的平衡。min_p表示相对于最有可能的令牌的概率,考虑令牌的最小概率。例如,当min_p=0.05并且最可能的令牌具有0.9的概率时,值小于0.05*0.9=0.045的logits被过滤掉。(默认值:0.0) MinP = 0.0f, //启用Mirostat采样以控制困惑。(默认值:0,0=禁用,1=Mirostat,2=Mirostat2.0) MiroStat = 0, //影响算法对生成文本反馈的响应速度。较低的学习率将导致较慢的调整,而较高的学习率会使算法更具响应性。(默认值:0.1) MiroStatEta = 0.1f, //控制输出的连贯性和多样性之间的平衡。较低的值将导致文本更加集中和连贯。(默认值:5.0) MiroStatTau = 5.0f, //设置用于生成下一个标记的上下文窗口的大小 NumCtx = 2048, //启用NUMA支持。(默认值:False) Numa = false, //提示处理最大批量。(默认值:512) NumBatch = 512, //要发送到GPU的层数。在macOS上,默认为1启用金属支持,0禁用。 NumGpu = 0, //变压器层中GQA组的数量。某些型号需要,例如骆驼2:70b为8 NumGqa = null, //从初始提示开始保留的令牌数。(默认值:4,-1=全部) NumKeep = 4, //设置计算期间要使用的线程数。默认情况下,Ollama会检测到这一点以获得最佳性能。建议将此值设置为系统具有的物理CPU核数(而不是逻辑核数)。 //NumThread = null, //惩罚换行符(默认值:True) PenalizeNewline = true, //根据令牌在提示中的存在对其进行处罚。(默认值:0.0) PresencePenalty = 0.0f, //无尾采样用于减少输出中不太可能的令牌的影响。更高的值(例如2.0)将更多地减少影响,而1.0的值将禁用此设置。(默认值:1) TfsZ = 1, //用于采样的典型p值。论文中描述的本地典型采样实现https://arxiv.org/abs/2202.00666.(默认值:1.0) TypicalP = 1.0f, //将模型锁定在内存中以防止交换。这可以提高性能,但它会使用更多的RAM,并可能减慢加载速度。(默认值:False) UseMlock = false, //默认情况下,模型被映射到内存中,这允许系统根据需要仅加载必要的部分。禁用mmap会使加载速度变慢,但如果您没有使用mlock,则可以减少分页。如果模型大于RAM,关闭mmap会阻止其加载。 //默认值:true) UseMmap = true, //只加载词汇,不加载权重。(默认值:False) VocabOnly = false, }, //(可选)流式处理响应: 可通过设置 false 来禁用流式处理 Stream = true, //(可选)系统消息(覆盖 Modelfile 中定义的内容) //System = "Modelfile 文件格式", //(可选)要使用的提示模板(覆盖 Modelfile 中定义的内容),内容为 Modelfile 文件格式的字符串 Template = """ You are a helpful assistant. {prompt} """, //(可选)如果为 true,则不会对提示应用任何格式。如果您在对 API 的请求中指定了完整的模板化提示,则可以选择使用 raw 参数 // 在某些情况下,您可能希望绕过模板系统并提供完整的提示。在这种情况下,您可以使用raw参数禁用格式化。 //Raw = false, //(可选)控制模型在请求后加载到内存中的时间(默认值:5M) KeepAlive = "5m", //(可选且已弃用):从上一个请求返回的 context 参数 /generate,这可用于保持较短的对话记忆 //Context = new long[] { 25897,46546546,546458}, }; //处理响应 IAsyncEnumerable? responses = _ollamaApiClient.GenerateAsync(generateRequest); StringBuilder stringBuilder = new StringBuilder(); await foreach (GenerateResponseStream? stream in responses) { stringBuilder.Append(stream?.Response); } _output.WriteLine(stringBuilder.ToString()); Assert.NotNull(responses); Assert.NotEmpty(responses); } /// /// 加载模型 /// [Fact] public async Task Completion_Request_LoadModel_Test() { var generateRequest = new GenerateRequest() { //默认模型 //Model = ModelSelecter.ModelWithTool }; //处理响应 GenerateDoneResponseStream? responseStream = await _ollamaApiClient.GenerateAsync(generateRequest).StreamToEndAsync(); Assert.NotNull(responseStream); Assert.NotNull(responseStream.Response); Assert.True(responseStream.Done); } /// /// 卸载模型 /// [Fact] public async Task Completion_Request_UnLoadModel_Test() { var generateRequest = new GenerateRequest() { //默认模型 //Model = ModelSelecter.ModelWithTool, //保持活跃0时长,即立即卸载 KeepAlive = "0", }; //处理响应 GenerateDoneResponseStream? responseStream = await _ollamaApiClient.GenerateAsync(generateRequest).StreamToEndAsync(); Assert.NotNull(responseStream); Assert.NotNull(responseStream.Response); Assert.True(responseStream.Done); } #endregion #region 生成对话补全(OllamaApiClient) /// /// 流式对话请求 测试 /// /// [Fact] public async Task ClientRequest_Streaming_Test() { var chatRequest = new ChatRequest() { Messages = new[] { new Message() { Role = ChatRole.User, Content = "天空为什么是蓝的?", } }, Think = true, }; StringBuilder responseText = new StringBuilder(); var chatResponses = _ollamaApiClient.ChatAsync(chatRequest); await foreach (var response in chatResponses) { responseText.Append(response?.Message.Content); } _output.WriteLine(responseText.ToString()); } /// /// 非流式对话请求 测试 /// /// [Fact] public async Task ClientRequest_NoStreaming_Test() { var chatRequest = new ChatRequest() { Messages = new[] { new Message() { Role = ChatRole.User, Content = "天空为什么是蓝的?", } }, Think = false, //(可选)禁用流式处理响应 Stream = false, }; var chatDoneResponse = await _ollamaApiClient.ChatAsync(chatRequest).StreamToEndAsync(); _output.WriteLine(chatDoneResponse?.Message.Content); Assert.NotNull(chatDoneResponse); Assert.NotNull(chatDoneResponse.Message?.Content); Assert.True(chatDoneResponse.Done); } /// /// 结构化输出对话请求 测试 /// /// [Fact] public async Task ClientRequest_StructuredOutputs_Test() { var chatRequest = new ChatRequest() { Messages = new[] { new Message() { Role = ChatRole.User, Content = "Ollama is 22 years old and busy saving the world. Return a JSON object with the age and availability.", } }, Think = false, //(可选)禁用流式处理响应 Stream = false, Format = new { type = "object", properties = new { age = new { type = "integer" }, available = new { type = "boolean" } }, required = new string[] { "age", "available" } }, }; var chatDoneResponse = await _ollamaApiClient.ChatAsync(chatRequest).StreamToEndAsync(); _output.WriteLine(chatDoneResponse?.Message.Content); var jsonObject = new { age = 0, available = false }; var responseObject = Newtonsoft.Json.JsonConvert.DeserializeAnonymousType(chatDoneResponse?.Message.Content ?? "{}", jsonObject); Assert.NotNull(responseObject); Assert.Equal(22, responseObject.age); Assert.True(responseObject.available); } /// /// 带历史上下文的对话请求 测试 /// /// [Fact] public async Task ClientRequest_WithHistory_Test() { var chatRequest = new ChatRequest() { Messages = new List(), Think = false, Stream = false, }; var history = new List(); List messages = new List() { new Message { Role="user", Content = "为什么天空是蓝色的?"}, new Message { Role="assistant", Content = "due to rayleigh scattering."}, new Message { Role="user", Content = "how is that different than mie scattering?使用中文回答"} }; foreach (var message in messages) { List current = chatRequest.Messages.ToList(); current.Add(message); chatRequest.Messages = current; var chatDoneResponse = await _ollamaApiClient.ChatAsync(chatRequest).StreamToEndAsync(); history.Add(chatDoneResponse?.Message.Content ?? ""); } _output.WriteLine(string.Join(Environment.NewLine + "++++++++++++++++++++++++++++++++++++++" + Environment.NewLine, history)); } /// /// 带图像的对话请求 测试 /// /// [Fact] public async Task ClientRequest_WithImages_Test() { var chatRequest = new ChatRequest() { Model = ModelSelecter.ModelWithVision, Messages = new[] { new Message() { Role = ChatRole.User, Content = "这张图片的内容是什么?用中文回答!", Images = new string[] { "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" }, } }, Think = false, Stream = false, }; var chatDoneResponse = await _ollamaApiClient.ChatAsync(chatRequest).StreamToEndAsync(); _output.WriteLine(chatDoneResponse?.Message.Content); } /// /// 可复现输出的对话请求 测试 /// /// [Fact] public async Task ClientRequest_ReproducibleOutputs_Test() { var chatRequest = new ChatRequest() { Messages = new[] { new Message() { Role = ChatRole.User, Content = "为什么天空是蓝的?", } }, Think = false, Stream = false, Options = new RequestOptions() { Seed = 19491001, Temperature = 0f, }, }; //第一次请求 var doneResponse = await _ollamaApiClient.ChatAsync(chatRequest).StreamToEndAsync(); var responseText1 = doneResponse?.Message.Content; //第二次请求 var doneResponse2 = await _ollamaApiClient.ChatAsync(chatRequest).StreamToEndAsync(); var responseText2 = doneResponse?.Message.Content; Assert.Equal(responseText1, responseText2); _output.WriteLine(responseText1); _output.WriteLine(responseText2); } /// /// 支持工具调用的对话请求 测试 /// /// /// 携带工具本地执行结果,再次发送给大模型不 /// /// [Fact] public async Task ClientRequest_WithTools_Test() { List Messages = new List(); Messages.Add(new Message() { Role = ChatRole.User, Content = "获取北京市当前的时间?", }); var chatRequest = new ChatRequest() { Model = ModelSelecter.ModelWithRawmodel, Messages = Messages, Stream = false, Tools = new[] { new { type = "function", function = new { name = "GetCurrentTime", description = "获取指定城市的当前时间", parameters = new { type = "object", properties = new { city = new { type = "string", description = "城市名称" } } }, required = new string[] { "city" } }, } }, }; //第1步:向大模型发送带tool信息的请求 var doneResponse = await _ollamaApiClient.ChatAsync(chatRequest).StreamToEndAsync(); //第2步:接收大模型响应,解析响应中携带有“工具调用信息” //响应信息 Message toolResponseMessage = doneResponse!.Message; //加入请求信息列表 Messages.Add(doneResponse.Message); //函数信息 var currentFunction = toolResponseMessage.ToolCalls!.First().Function; //参数信息 var city = currentFunction?.Arguments!["city"]?.ToString() ?? string.Empty; //第3步:使用大模型响应中的函数信息,调用本地工具函数,获取执行结果 /*真实项目应该用特性、反射等技术,动态执行。此处仅作演示,直接写死第一个函数*/ //调用本地工具函数 var callCurrentDate = GetCurrentTime(city); //第4步:传递工具执行结果给大模型,大模型生成最终回复 //函数调用信息 var ss = string.Join(", ", currentFunction?.Arguments?.Select(kvp => $"{kvp.Key}:{kvp.Value}") ?? new string[] { }); var toolResultText = $"{currentFunction?.Name ?? "(unnamed tool)"}({string.Join(", ",ss)})"; Messages.Add(new Message() { Role = ChatRole.Tool, Content = $"Tool:{toolResultText}:\nResult:{callCurrentDate.ToString("yyyy-MM-dd HH:mm:ss")}"}); chatRequest.Messages = Messages; //理论上:大模型应该根据工具调用结果,生成最终回复,但此处没有真正起到作用 var doneResponse2 = await _ollamaApiClient.ChatAsync(chatRequest).StreamToEndAsync(); var lastContentText = doneResponse2?.Message.Content; _output.WriteLine(lastContentText); } #endregion #region 生成对话补全(Chat方便、功能不全,应该在开发中) /// /// 流式对话请求 测试 /// /// [Fact] public async Task ChatRequest_Streaming_Test() { var myChat = new Chat(_ollamaApiClient) { Think = true, }; StringBuilder responseText = new StringBuilder(); IAsyncEnumerable? chatResponses = myChat.SendAsync("天空为什么是蓝的?"); await foreach (var response in chatResponses) { responseText.Append(response); } _output.WriteLine(responseText.ToString()); } /// /// 非流式对话请求 测试 /// /// [Fact] public async Task ChatRequest_NoStreaming_Test() { var myChat = new Chat(_ollamaApiClient) { Think = true, }; var chatDoneResponse = await myChat.SendAsync("天空为什么是蓝的?").StreamToEndAsync(); _output.WriteLine(chatDoneResponse); Assert.NotNull(chatDoneResponse); } /// /// 结构化输出对话请求(没有直接实现) /// /// [Fact] public void ChatRequest_StructuredOutputs_Test() { //var chatRequest = new ChatRequest() //{ // Messages = new[] // { // new Message() // { // Role = ChatRole.User, // Content = "Ollama is 22 years old and busy saving the world. Return a JSON object with the age and availability.", // } // }, // Think = false, // //(可选)禁用流式处理响应 // Stream = false, // Format = new // { // type = "object", // properties = new { age = new { type = "integer" }, available = new { type = "boolean" } }, // required = new string[] { "age", "available" } // }, //}; //var chatDoneResponse = await _ollamaApiClient.ChatAsync(chatRequest).StreamToEndAsync(); //_output.WriteLine(chatDoneResponse?.Message.Content); //var jsonObject = new { age = 0, available = false }; //var responseObject = Newtonsoft.Json.JsonConvert.DeserializeAnonymousType(chatDoneResponse?.Message.Content ?? "{}", jsonObject); //Assert.NotNull(responseObject); //Assert.Equal(22, responseObject.age); //Assert.True(responseObject.available); } /// /// 带历史上下文的对话请求 测试 /// /// [Fact] public async Task ChatRequest_WithHistory_Test() { var myChat = new Chat(_ollamaApiClient); List messages = new List() { new Message { Role="user", Content = "为什么天空是蓝色的?"}, new Message { Role="assistant", Content = "due to rayleigh scattering."}, new Message { Role="user", Content = "how is that different than mie scattering?使用中文回答"} }; foreach (var message in messages) { var chatDoneResponse = await myChat.SendAsAsync(message.Role!.Value, message.Content??"").StreamToEndAsync(); _output.WriteLine(chatDoneResponse); _output.WriteLine("----------------------------------------------------"); } } /// /// 带图像的对话请求 测试 /// /// [Fact] public async Task ChatRequest_WithImages_Test() { var myChat = new Chat(_ollamaApiClient) { Model = ModelSelecter.ModelWithVision, }; var base64img1 ="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"; var chatDoneResponse = await myChat.SendAsync("这张图片的内容是什么?用中文回答!", new string[] { base64img1 }).StreamToEndAsync(); _output.WriteLine(chatDoneResponse); Assert.NotNull(chatDoneResponse); Assert.Contains("企鹅", chatDoneResponse); } /// /// 可复现输出的对话请求 测试 /// /// [Fact] public async Task ChatRequest_ReproducibleOutputs_Test() { //第一次请求 var myChat1 = new Chat(_ollamaApiClient) { Think = false, Options = new RequestOptions() { Seed = 19491001, Temperature = 0f, }, }; var doneResponse1 = await myChat1.SendAsync("通常情况,天空是什么颜色?").StreamToEndAsync(); //第二次请求 var myChat2 = new Chat(_ollamaApiClient) { Think = false, Options = new RequestOptions() { Seed = 19491001, Temperature = 0f, }, }; var doneResponse2 = await myChat2.SendAsync("通常情况,天空是什么颜色?").StreamToEndAsync(); Assert.Equal(doneResponse1, doneResponse2); _output.WriteLine(doneResponse1); _output.WriteLine(doneResponse2); } /// /// 支持工具调用的对话请求 测试 /// /// /// 携带工具本地执行结果,再次发送给大模型不 /// /// [Fact] public async Task ChatRequest_WithTools_Test() { var chat = new Chat(_ollamaApiClient) {Model = ModelSelecter.ModelWithRawmodel}; List Tools = [new GetWeatherTool(), new GetLatLonAsyncTool(), new GetPopulationTool()]; StringBuilder outputString = new StringBuilder(1000); //各个步骤,被Chat类包装了 await foreach (var answerToken in chat.SendAsync("上海市的天气怎么样?", Tools)) { outputString.Append(answerToken); } _output.WriteLine(outputString.ToString()); } #endregion #region 列出本地模型 [Fact] public async Task List_Local_Models_Test() { var models = await _ollamaApiClient.ListLocalModelsAsync(); _output.WriteLine("本地模型有:"); foreach (OllamaSharp.Models.Model model in models) { _output.WriteLine(model.Name); } _output.WriteLine($"共:{models.Count()} 个"); Assert.NotNull(models); Assert.True(models.Any()); } [Theory] [InlineData(ModelSelecter.ModelWithTool)] [InlineData(ModelSelecter.ModelWithVision)] [InlineData(ModelSelecter.ModelWithEmbedding)] public async Task Exist_Local_Models_Test(string selectedModel) { var models = await _ollamaApiClient.ListLocalModelsAsync(); _output.WriteLine("本地模型有:"); foreach (OllamaSharp.Models.Model model in models) { _output.WriteLine(model.Name); } _output.WriteLine($"共:{models.Count()} 个"); Assert.NotNull(models); Assert.Contains(models, m => m.Name == selectedModel); } #endregion #region 列出运行中的模型 /// /// 列出运行中的模型 /// /// [Fact] public async Task List_Running_Models_Test() { var models = await _ollamaApiClient.ListRunningModelsAsync(); _output.WriteLine("运行中的模型有:"); foreach (OllamaSharp.Models.Model model in models) { _output.WriteLine(model.Name); } _output.WriteLine($"共:{models.Count()} 个"); Assert.NotNull(models); } /// /// 指定模型是否在运行中 /// /// /// [Theory] [InlineData(ModelSelecter.ModelWithTool)] public async Task IsRunning_Models_Test(string selectedModel) { var models = await _ollamaApiClient.ListRunningModelsAsync(); _output.WriteLine("运行中的模型有:"); foreach (OllamaSharp.Models.Model model in models) { _output.WriteLine(model.Name); } _output.WriteLine($"共:{models.Count()} 个"); var isRunning = models.Any(m => m.Name == selectedModel); var message = $"{selectedModel}模型:{(isRunning ? "运行中" : "未运行")}"; _output.WriteLine(message); Assert.True(true, message); } #endregion #region Ollama服务运行状态 [Fact] public async Task OllamaServer_IsRunning_Test() { var isRunning = await _ollamaApiClient.IsRunningAsync(); var ollamaState = $"Ollama服务:{(isRunning ? "运行中" : "未运行")}"; _output.WriteLine(ollamaState); Assert.True(isRunning, ollamaState); } #endregion #region 版本信息 /// /// 获取版本信息 /// /// [Fact] public async Task Get_Version_Test() { var version = await _ollamaApiClient.GetVersionAsync(); _output.WriteLine($"Version: {version}"); Assert.NotNull(version); } #endregion #region tools 方法 /// /// Gets the current weather for a given location. /// /// The location or city to get the weather for /// The unit to measure the temperature in /// The weather for the given location [OllamaTool] public static string GetWeather(string location, Unit unit) => $"It's cold at only 6° {unit} in {location}."; /// /// Gets the latitude and longitude for a given location. /// /// The location to get the latitude and longitude for /// The weather for the given location [OllamaTool] public async static Task GetLatLonAsync(string location) { await Task.Delay(200).ConfigureAwait(false); return $"{new Random().Next(20, 50)}.4711, {new Random().Next(3, 15)}.0815"; } /// /// Gets the amount of people living in a given city /// /// The city to get the population info for /// The population of a given city [OllamaTool] public static int GetPopulation(string city) => new Random().Next(1000, 10000000); public static DateTime GetCurrentTime(string city) { return DateTime.Now; } #endregion } public enum Unit { Celsius, Fahrenheit } }