Skip to content

Commit 11bc390

Browse files
authored
Merge pull request #14 from ZatomicAI/perplexity
Added support for Perplexity
2 parents 3f7f4f7 + 2752da7 commit 11bc390

22 files changed

+621
-4
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Zatomic.AI.Providers
22

3-
C# .NET library that provides chat functionality for the following AI providers: AI21 Labs, Amazon Bedrock, Anthropic, Azure OpenAI, Azure Serverless, Cohere, Deep Infra, Fireworks AI, Google Gemini, Groq, Hugging Face, Hyperbolic, Lambda, Meta, Mistral, OpenAI, Together AI, and xAI.
3+
C# .NET library that provides chat functionality for the following AI providers: AI21 Labs, Amazon Bedrock, Anthropic, Azure OpenAI, Azure Serverless, Cohere, Deep Infra, Fireworks AI, Google Gemini, Groq, Hugging Face, Hyperbolic, Lambda, Meta, Mistral, OpenAI, Perplexity, Together AI, and xAI.
44

55
The library calls the chat completions REST APIs and inference endpoints for each of the above AI providers. Everything is strongly-typed with the library handling all JSON serialization/deserialization for all requests and responses. Both non-stream and streaming functionality is supported using `async` methods for improved performance.
66

@@ -115,6 +115,10 @@ The format of the `AppSettigns.Development.json` file is as follows:
115115
"ApiKey": "",
116116
"Model": ""
117117
},
118+
"Perplexity": {
119+
"ApiKey": "",
120+
"Model": ""
121+
},
118122
"TogetherAI": {
119123
"ApiKey": "",
120124
"Model": ""
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Threading.Tasks;
2+
using NUnit.Framework;
3+
using Zatomic.AI.Providers.Perplexity;
4+
5+
namespace Zatomic.AI.Providers.Samples
6+
{
7+
[TestFixture, Explicit]
8+
public class PerplexitySamples : BaseSample
9+
{
10+
private readonly string _apiKey;
11+
private readonly string _model;
12+
13+
public PerplexitySamples()
14+
{
15+
_apiKey = Configuration["Perplexity:ApiKey"];
16+
_model = Configuration["Perplexity:Model"];
17+
}
18+
19+
[Test]
20+
public async Task Chat()
21+
{
22+
var client = new PerplexityChatClient(_apiKey);
23+
var request = new PerplexityChatRequest(_model);
24+
request.AddSystemMessage(SystemPrompt);
25+
request.AddUserMessage(UserPrompt);
26+
27+
var response = await client.ChatAsync(request);
28+
WriteOutput(response.Choices[0].Message.Content);
29+
WriteOutput(response.Usage.PromptTokens, response.Usage.CompletionTokens, response.Usage.TotalTokens, response.Duration.Value);
30+
}
31+
32+
[Test]
33+
public async Task ChatStream()
34+
{
35+
var client = new PerplexityChatClient(_apiKey);
36+
var request = new PerplexityChatRequest(_model);
37+
request.AddSystemMessage(SystemPrompt);
38+
request.AddUserMessage(UserPrompt);
39+
40+
int inputTokens = 0;
41+
int outputTokens = 0;
42+
int totalTokens = 0;
43+
decimal duration = 0;
44+
45+
await foreach (var result in client.ChatStreamAsync(request))
46+
{
47+
WriteOutput(result.Chunk);
48+
49+
if (result.InputTokens.HasValue) inputTokens = result.InputTokens.Value;
50+
if (result.OutputTokens.HasValue) outputTokens = result.OutputTokens.Value;
51+
if (result.TotalTokens.HasValue) totalTokens = result.TotalTokens.Value;
52+
if (result.Duration.HasValue) duration = result.Duration.Value;
53+
}
54+
55+
WriteOutput(inputTokens, outputTokens, totalTokens, duration);
56+
}
57+
}
58+
}

src/Zatomic.AI.Providers/Exceptions/AIExceptionUtility.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Zatomic.AI.Providers.Meta;
1717
using Zatomic.AI.Providers.Mistral;
1818
using Zatomic.AI.Providers.OpenAI;
19+
using Zatomic.AI.Providers.Perplexity;
1920
using Zatomic.AI.Providers.TogetherAI;
2021
using Zatomic.AI.Providers.xAI;
2122

@@ -103,6 +104,11 @@ public static AIException BuildOpenAIAIException(Exception ex, OpenAIChatRequest
103104
return BuildAIException(ex, "OpenAI", request.Model, request, responseJson);
104105
}
105106

107+
public static AIException BuildPerplexityAIException(Exception ex, PerplexityChatRequest request, string responseJson = null)
108+
{
109+
return BuildAIException(ex, "Perplexity", request.Model, request, responseJson);
110+
}
111+
106112
public static AIException BuildTogetherAIAIException(Exception ex, TogetherAIChatRequest request, string responseJson = null)
107113
{
108114
return BuildAIException(ex, "Together AI", request.Model, request, responseJson);

src/Zatomic.AI.Providers/OpenAI/OpenAIChatUserLocation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Zatomic.AI.Providers.OpenAI
55
public class OpenAIChatUserLocation
66
{
77
[JsonProperty("approximate")]
8-
public OpenAIChatUserLocationApproximate approximate { get; set; }
8+
public OpenAIChatUserLocationApproximate Approximate { get; set; }
99

1010
[JsonProperty("type")]
1111
public string Type { get; set; }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Zatomic.AI.Providers.Perplexity
4+
{
5+
public abstract class PerplexityChatBaseContent
6+
{
7+
[JsonProperty("type")]
8+
public string Type { get; set; }
9+
}
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Zatomic.AI.Providers.Perplexity
4+
{
5+
public class PerplexityChatChoice
6+
{
7+
[JsonProperty("delta")]
8+
public PerplexityChatDelta Delta { get; set; }
9+
10+
[JsonProperty("finish_reason")]
11+
public string FinishReason { get; set; }
12+
13+
[JsonProperty("index")]
14+
public int Index { get; set; }
15+
16+
[JsonProperty("message")]
17+
public PerplexityChatOutputMessage Message { get; set; }
18+
}
19+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Net.Http;
6+
using System.Net.Http.Headers;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Zatomic.AI.Providers.Exceptions;
10+
using Zatomic.AI.Providers.Extensions;
11+
12+
namespace Zatomic.AI.Providers.Perplexity
13+
{
14+
public class PerplexityChatClient : BaseClient
15+
{
16+
public string ApiKey { get; set; }
17+
public string ApiUrl { get; } = "https://api.perplexity.ai/chat/completions";
18+
19+
public PerplexityChatClient()
20+
{
21+
}
22+
23+
public PerplexityChatClient(string apiKey) : this()
24+
{
25+
ApiKey = apiKey;
26+
}
27+
28+
public async Task<PerplexityChatResponse> ChatAsync(PerplexityChatRequest request)
29+
{
30+
PerplexityChatResponse response = null;
31+
32+
using (var httpClient = new HttpClient())
33+
{
34+
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
35+
36+
var requestJson = request.Serialize();
37+
var content = new StringContent(requestJson, Encoding.UTF8, "application/json");
38+
39+
string responseJson = null;
40+
41+
try
42+
{
43+
var stopwatch = Stopwatch.StartNew();
44+
45+
var postResponse = await DoWithRetryAsync(() => httpClient.PostAsync(ApiUrl, content));
46+
responseJson = await postResponse.Content.ReadAsStringAsync();
47+
postResponse.EnsureSuccessStatusCode();
48+
49+
stopwatch.Stop();
50+
51+
response = responseJson.Deserialize<PerplexityChatResponse>();
52+
response.Duration = stopwatch.ToDurationInSeconds(2);
53+
}
54+
catch (Exception ex)
55+
{
56+
var aiEx = AIExceptionUtility.BuildPerplexityAIException(ex, request, responseJson);
57+
throw aiEx;
58+
}
59+
}
60+
61+
return response;
62+
}
63+
64+
public async IAsyncEnumerable<AIStreamResponse> ChatStreamAsync(PerplexityChatRequest request)
65+
{
66+
request.Stream = true;
67+
68+
using (var httpClient = new HttpClient())
69+
{
70+
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
71+
72+
var requestJson = request.Serialize();
73+
var postRequest = new HttpRequestMessage(HttpMethod.Post, ApiUrl)
74+
{
75+
Content = new StringContent(requestJson, Encoding.UTF8, "application/json")
76+
};
77+
78+
HttpResponseMessage postResponse = null;
79+
80+
try
81+
{
82+
// This is wrapped in a try-catch in case an error occurs at the start
83+
postResponse = await DoWithRetryAsync(() => httpClient.SendAsync(postRequest, HttpCompletionOption.ResponseHeadersRead));
84+
postResponse.EnsureSuccessStatusCode();
85+
}
86+
catch (Exception ex)
87+
{
88+
var aiEx = AIExceptionUtility.BuildPerplexityAIException(ex, request);
89+
throw aiEx;
90+
}
91+
92+
var streamComplete = false;
93+
var stopwatch = Stopwatch.StartNew();
94+
95+
using (var stream = await postResponse.Content.ReadAsStreamAsync())
96+
using (var reader = new StreamReader(stream))
97+
{
98+
while (!reader.EndOfStream && !streamComplete)
99+
{
100+
string line;
101+
102+
try
103+
{
104+
// This is wrapped in a try-catch in case an error occurs mid-stream
105+
line = await reader.ReadLineAsync();
106+
}
107+
catch (Exception ex)
108+
{
109+
var aiEx = AIExceptionUtility.BuildPerplexityAIException(ex, request);
110+
throw aiEx;
111+
}
112+
113+
// Event messages start with "data: ", so that's why we substring the line at 6
114+
if (!line.IsNullOrEmpty() && line.StartsWith("data: "))
115+
{
116+
var rsp = line.Substring(6).Deserialize<PerplexityChatResponse>();
117+
var streamResponse = new AIStreamResponse { Chunk = rsp.Choices[0].Delta.Content };
118+
119+
if (!rsp.Choices[0].FinishReason.IsNullOrEmpty())
120+
{
121+
streamComplete = true;
122+
stopwatch.Stop();
123+
streamResponse.Duration = stopwatch.ToDurationInSeconds(2);
124+
125+
if (rsp.Usage != null)
126+
{
127+
streamResponse.InputTokens = rsp.Usage.PromptTokens;
128+
streamResponse.OutputTokens = rsp.Usage.CompletionTokens;
129+
streamResponse.TotalTokens = rsp.Usage.TotalTokens;
130+
}
131+
}
132+
133+
yield return streamResponse;
134+
}
135+
}
136+
}
137+
}
138+
}
139+
}
140+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Newtonsoft.Json;
4+
using Newtonsoft.Json.Linq;
5+
6+
namespace Zatomic.AI.Providers.Perplexity
7+
{
8+
public class PerplexityChatContentListConverter : JsonConverter<List<PerplexityChatBaseContent>>
9+
{
10+
public override List<PerplexityChatBaseContent> ReadJson(JsonReader reader, Type objectType, List<PerplexityChatBaseContent> existingValue, bool hasExistingValue, JsonSerializer serializer)
11+
{
12+
var array = JArray.Load(reader);
13+
var items = new List<PerplexityChatBaseContent>();
14+
15+
foreach (var token in array)
16+
{
17+
PerplexityChatBaseContent item;
18+
19+
var type = token["type"]?.Value<string>();
20+
21+
if (type == "text") item = token.ToObject<PerplexityChatTextContent>(serializer);
22+
else if (type == "image_url") item = token.ToObject<PerplexityChatImageUrlContent>(serializer);
23+
else throw new JsonSerializationException($"Unknown content type: {type}");
24+
25+
items.Add(item);
26+
}
27+
28+
return items;
29+
}
30+
31+
public override void WriteJson(JsonWriter writer, List<PerplexityChatBaseContent> value, JsonSerializer serializer)
32+
{
33+
writer.WriteStartArray();
34+
35+
foreach (var item in value)
36+
{
37+
JToken.FromObject(item, serializer).WriteTo(writer);
38+
}
39+
40+
writer.WriteEndArray();
41+
}
42+
}
43+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Zatomic.AI.Providers.Perplexity
4+
{
5+
public class PerplexityChatDelta
6+
{
7+
[JsonProperty("content")]
8+
public string Content { get; set; }
9+
10+
[JsonProperty("role")]
11+
public string Role { get; set; }
12+
}
13+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Zatomic.AI.Providers.Perplexity
4+
{
5+
public class PerplexityChatImageUrl
6+
{
7+
[JsonProperty("url")]
8+
public string Url { get; set; }
9+
}
10+
}

0 commit comments

Comments
 (0)