Skip to content

Commit

Permalink
Nebula AI .NET Integration (Beta) (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xFirekeeper authored Jan 24, 2025
1 parent a6afc39 commit 1e59bf6
Show file tree
Hide file tree
Showing 10 changed files with 1,003 additions and 0 deletions.
68 changes: 68 additions & 0 deletions Thirdweb.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Thirdweb;
using Thirdweb.AI;
using Thirdweb.Pay;

DotEnv.Load();
Expand Down Expand Up @@ -35,6 +36,73 @@

#endregion

#region AI

// Prepare some context
var myChain = 11155111;
var myWallet = await SmartWallet.Create(personalWallet: await PrivateKeyWallet.Generate(client), chainId: myChain, gasless: true);
var myContractAddress = "0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8"; // DropERC1155
var usdcAddress = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238";

// Create a Nebula session
var nebula = await ThirdwebNebula.Create(client);

// Chat, passing wallet context
var response1 = await nebula.Chat(message: "What is my wallet address?", wallet: myWallet);
Console.WriteLine($"Response 1: {response1.Message}");

// Chat, passing contract context
var response2 = await nebula.Chat(
message: "What's the total supply of token id 0 for this contract?",
context: new NebulaContext(contractAddresses: new List<string> { myContractAddress }, chainIds: new List<BigInteger> { myChain })
);
Console.WriteLine($"Response 2: {response2.Message}");

// Chat, passing multiple messages and context
var response3 = await nebula.Chat(
messages: new List<NebulaChatMessage>
{
new($"Tell me the name of this contract: {myContractAddress}", NebulaChatRole.User),
new("The name of the contract is CatDrop", NebulaChatRole.Assistant),
new("What's the symbol of this contract?", NebulaChatRole.User),
},
context: new NebulaContext(contractAddresses: new List<string> { myContractAddress }, chainIds: new List<BigInteger> { myChain })
);
Console.WriteLine($"Response 3: {response3.Message}");

// Execute, this directly sends transactions
var executionResult = await nebula.Execute("Approve 1 USDC to vitalik.eth", wallet: myWallet, context: new NebulaContext(contractAddresses: new List<string>() { usdcAddress }));
if (executionResult.TransactionReceipts != null && executionResult.TransactionReceipts.Count > 0)
{
Console.WriteLine($"Receipt: {executionResult.TransactionReceipts[0]}");
}
else
{
Console.WriteLine($"Message: {executionResult.Message}");
}

// Batch execute
var batchExecutionResult = await nebula.Execute(
new List<NebulaChatMessage>
{
new("What's the address of vitalik.eth", NebulaChatRole.User),
new("The address of vitalik.eth is 0xd8dA6BF26964aF8E437eEa5e3616511D7G3a3298", NebulaChatRole.Assistant),
new("Approve 1 USDC to them", NebulaChatRole.User),
},
wallet: myWallet,
context: new NebulaContext(contractAddresses: new List<string>() { usdcAddress })
);
if (batchExecutionResult.TransactionReceipts != null && batchExecutionResult.TransactionReceipts.Count > 0)
{
Console.WriteLine($"Receipts: {JsonConvert.SerializeObject(batchExecutionResult.TransactionReceipts, Formatting.Indented)}");
}
else
{
Console.WriteLine($"Message: {batchExecutionResult.Message}");
}

#endregion

#region Get Social Profiles

// var socialProfiles = await Utils.GetSocialProfiles(client, "joenrv.eth");
Expand Down
136 changes: 136 additions & 0 deletions Thirdweb.Tests/Thirdweb.AI/Thirdweb.AI.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Numerics;
using Thirdweb.AI;

namespace Thirdweb.Tests.AI;

public class NebulaTests : BaseTests
{
private const string NEBULA_TEST_CONTRACT = "0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8";
private const string NEBULA_TEST_USDC_ADDRESS = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238";
private const int NEBULA_TEST_CHAIN = 11155111;

public NebulaTests(ITestOutputHelper output)
: base(output) { }

[Fact(Timeout = 120000)]
public async Task Create_CreatesSession()
{
var nebula = await ThirdwebNebula.Create(this.Client);
Assert.NotNull(nebula);
Assert.NotNull(nebula.SessionId);
}

[Fact(Timeout = 120000)]
public async Task Create_ResumesSession()
{
var nebula = await ThirdwebNebula.Create(this.Client);
var sessionId = nebula.SessionId;
Assert.NotNull(nebula);
Assert.NotNull(nebula.SessionId);

nebula = await ThirdwebNebula.Create(this.Client, sessionId);
Assert.NotNull(nebula);
Assert.Equal(sessionId, nebula.SessionId);
}

[Fact(Timeout = 120000)]
public async Task Chat_Single_ReturnsResponse()
{
var nebula = await ThirdwebNebula.Create(this.Client);
var response = await nebula.Chat(
message: "What's the symbol of this contract?",
context: new NebulaContext(contractAddresses: new List<string> { NEBULA_TEST_CONTRACT }, chainIds: new List<BigInteger> { NEBULA_TEST_CHAIN })
);
Assert.NotNull(response);
Assert.NotNull(response.Message);
Assert.Contains("CAT", response.Message);
}

[Fact(Timeout = 120000)]
public async Task Chat_Single_NoContext_ReturnsResponse()
{
var nebula = await ThirdwebNebula.Create(this.Client);
var response = await nebula.Chat(message: $"What's the symbol of this contract: {NEBULA_TEST_CONTRACT} (Sepolia)?");
Assert.NotNull(response);
Assert.NotNull(response.Message);
Assert.Contains("CAT", response.Message);
}

[Fact(Timeout = 120000)]
public async Task Chat_Multiple_ReturnsResponse()
{
var nebula = await ThirdwebNebula.Create(this.Client);
var response = await nebula.Chat(
messages: new List<NebulaChatMessage>
{
new("What's the symbol of this contract?", NebulaChatRole.User),
new("The symbol is CAT", NebulaChatRole.Assistant),
new("What's the name of this contract?", NebulaChatRole.User),
},
context: new NebulaContext(contractAddresses: new List<string> { NEBULA_TEST_CONTRACT }, chainIds: new List<BigInteger> { NEBULA_TEST_CHAIN })
);
Assert.NotNull(response);
Assert.NotNull(response.Message);
Assert.Contains("CatDrop", response.Message, StringComparison.OrdinalIgnoreCase);
}

[Fact(Timeout = 120000)]
public async Task Chat_UnderstandsWalletContext()
{
var wallet = await PrivateKeyWallet.Generate(this.Client);
var expectedAddress = await wallet.GetAddress();
var nebula = await ThirdwebNebula.Create(this.Client);
var response = await nebula.Chat(message: "What is my wallet address?", wallet: wallet);
Assert.NotNull(response);
Assert.NotNull(response.Message);
Assert.Contains(expectedAddress, response.Message);
}

[Fact(Timeout = 120000)]
public async Task Execute_ReturnsMessageAndReceipt()
{
var signer = await PrivateKeyWallet.Generate(this.Client);
var wallet = await SmartWallet.Create(signer, NEBULA_TEST_CHAIN);
var nebula = await ThirdwebNebula.Create(this.Client);
var response = await nebula.Execute(
new List<NebulaChatMessage>
{
new("What's the address of vitalik.eth", NebulaChatRole.User),
new("The address of vitalik.eth is 0xd8dA6BF26964aF8E437eEa5e3616511D7G3a3298", NebulaChatRole.Assistant),
new("Approve 1 USDC to them", NebulaChatRole.User),
},
wallet: wallet,
context: new NebulaContext(contractAddresses: new List<string>() { NEBULA_TEST_USDC_ADDRESS })
);
Assert.NotNull(response);
Assert.NotNull(response.Message);
Assert.NotNull(response.TransactionReceipts);
Assert.NotEmpty(response.TransactionReceipts);
Assert.NotNull(response.TransactionReceipts[0].TransactionHash);
Assert.True(response.TransactionReceipts[0].TransactionHash.Length == 66);
}

[Fact(Timeout = 120000)]
public async Task Execute_ReturnsMessageAndReceipts()
{
var signer = await PrivateKeyWallet.Generate(this.Client);
var wallet = await SmartWallet.Create(signer, NEBULA_TEST_CHAIN);
var nebula = await ThirdwebNebula.Create(this.Client);
var response = await nebula.Execute(
new List<NebulaChatMessage>
{
new("What's the address of vitalik.eth", NebulaChatRole.User),
new("The address of vitalik.eth is 0xd8dA6BF26964aF8E437eEa5e3616511D7G3a3298", NebulaChatRole.Assistant),
new("Approve 1 USDC to them", NebulaChatRole.User),
},
wallet: wallet,
context: new NebulaContext(contractAddresses: new List<string>() { NEBULA_TEST_USDC_ADDRESS })
);
Assert.NotNull(response);
Assert.NotNull(response.Message);
Assert.NotNull(response.TransactionReceipts);
Assert.NotEmpty(response.TransactionReceipts);
Assert.NotNull(response.TransactionReceipts[0].TransactionHash);
Assert.True(response.TransactionReceipts[0].TransactionHash.Length == 66);
}
}
32 changes: 32 additions & 0 deletions Thirdweb/Thirdweb.AI/ChatClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Text;
using Newtonsoft.Json;

namespace Thirdweb.AI;

internal class ChatClient
{
private readonly IThirdwebHttpClient _httpClient;

public ChatClient(IThirdwebHttpClient httpClient)
{
this._httpClient = httpClient;
}

public async Task<ChatResponse> SendMessageAsync(ChatParamsSingleMessage message)
{
var content = new StringContent(JsonConvert.SerializeObject(message), Encoding.UTF8, "application/json");
var response = await this._httpClient.PostAsync($"{Constants.NEBULA_API_URL}/chat", content);
_ = response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ChatResponse>(responseContent);
}

public async Task<ChatResponse> SendMessagesAsync(ChatParamsMultiMessages messages)
{
var content = new StringContent(JsonConvert.SerializeObject(messages), Encoding.UTF8, "application/json");
var response = await this._httpClient.PostAsync($"{Constants.NEBULA_API_URL}/chat", content);
_ = response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ChatResponse>(responseContent);
}
}
32 changes: 32 additions & 0 deletions Thirdweb/Thirdweb.AI/ExecutionClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Text;
using Newtonsoft.Json;

namespace Thirdweb.AI;

internal class ExecutionClient
{
private readonly IThirdwebHttpClient _httpClient;

public ExecutionClient(IThirdwebHttpClient httpClient)
{
this._httpClient = httpClient;
}

public async Task<ChatResponse> ExecuteAsync(ChatParamsSingleMessage command)
{
var content = new StringContent(JsonConvert.SerializeObject(command), Encoding.UTF8, "application/json");
var response = await this._httpClient.PostAsync($"{Constants.NEBULA_API_URL}/execute", content);
_ = response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ChatResponse>(responseContent);
}

public async Task<ChatResponse> ExecuteBatchAsync(ChatParamsMultiMessages commands)
{
var content = new StringContent(JsonConvert.SerializeObject(commands), Encoding.UTF8, "application/json");
var response = await this._httpClient.PostAsync($"{Constants.NEBULA_API_URL}/execute", content);
_ = response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ChatResponse>(responseContent);
}
}
28 changes: 28 additions & 0 deletions Thirdweb/Thirdweb.AI/FeedbackClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Text;
using Newtonsoft.Json;

namespace Thirdweb.AI;

internal class FeedbackClient
{
private readonly IThirdwebHttpClient _httpClient;

public FeedbackClient(IThirdwebHttpClient httpClient)
{
this._httpClient = httpClient;
}

/// <summary>
/// Submits feedback for a specific session and request.
/// </summary>
/// <param name="feedback">The feedback parameters to submit.</param>
/// <returns>The submitted feedback details.</returns>
public async Task<Feedback> SubmitFeedbackAsync(FeedbackParams feedback)
{
var content = new StringContent(JsonConvert.SerializeObject(feedback), Encoding.UTF8, "application/json");
var response = await this._httpClient.PostAsync($"{Constants.NEBULA_API_URL}/feedback", content);
_ = response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ResponseModel<Feedback>>(responseContent).Result;
}
}
64 changes: 64 additions & 0 deletions Thirdweb/Thirdweb.AI/SessionManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Text;
using Newtonsoft.Json;

namespace Thirdweb.AI;

internal class SessionManager
{
private readonly IThirdwebHttpClient _httpClient;

public SessionManager(IThirdwebHttpClient httpClient)
{
this._httpClient = httpClient;
}

public async Task<List<SessionList>> ListSessionsAsync()
{
var response = await this._httpClient.GetAsync($"{Constants.NEBULA_API_URL}/session/list");
_ = response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ResponseModel<List<SessionList>>>(content).Result;
}

public async Task<Session> GetSessionAsync(string sessionId)
{
var response = await this._httpClient.GetAsync($"{Constants.NEBULA_API_URL}/session/{sessionId}");
_ = response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ResponseModel<Session>>(content).Result;
}

public async Task<Session> CreateSessionAsync(CreateSessionParams parameters)
{
var content = new StringContent(JsonConvert.SerializeObject(parameters), Encoding.UTF8, "application/json");
var response = await this._httpClient.PostAsync($"{Constants.NEBULA_API_URL}/session", content);
_ = response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ResponseModel<Session>>(responseContent).Result;
}

public async Task<Session> UpdateSessionAsync(string sessionId, UpdateSessionParams parameters)
{
var content = new StringContent(JsonConvert.SerializeObject(parameters), Encoding.UTF8, "application/json");
var response = await this._httpClient.PutAsync($"{Constants.NEBULA_API_URL}/session/{sessionId}", content);
_ = response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ResponseModel<Session>>(responseContent).Result;
}

public async Task<SessionDeleteResponse> DeleteSessionAsync(string sessionId)
{
var response = await this._httpClient.DeleteAsync($"{Constants.NEBULA_API_URL}/session/{sessionId}");
_ = response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ResponseModel<SessionDeleteResponse>>(content).Result;
}

public async Task<Session> ClearSessionAsync(string sessionId)
{
var response = await this._httpClient.PostAsync($"{Constants.NEBULA_API_URL}/session/{sessionId}/clear", null);
_ = response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ResponseModel<Session>>(content).Result;
}
}
Loading

0 comments on commit 1e59bf6

Please sign in to comment.