Skip to content

Commit 7eb2564

Browse files
committed
#4 - Image Generation Tool
#5 - Reasoning Tool #6 - Refactor tools to use IGrokTool instead of GrokToolDefinition
1 parent c6b88d7 commit 7eb2564

File tree

13 files changed

+983
-74
lines changed

13 files changed

+983
-74
lines changed

README.md

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# GrokSdk 🚀
22

3+
If you find this tool helpful or are able to use it in your projects, please drop me a note on [X](https://x.com/twhidden) to let me know—it encourages me to keep it going!
4+
35
[![NuGet](https://img.shields.io/nuget/v/GrokSdk.svg)](https://www.nuget.org/packages/GrokSdk/)
46
[![NuGet Downloads](https://img.shields.io/nuget/dt/GrokSdk.svg)](https://www.nuget.org/packages/GrokSdk/)
57
[![Build Status](https://github.com/twhidden/grok/actions/workflows/ci.yml/badge.svg)](https://github.com/twhidden/grok/actions/workflows/ci.yml)
68
[![.NET 8.0](https://img.shields.io/badge/.NET-8.0-blue.svg)](https://dotnet.microsoft.com/)
79
[![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-blue.svg)](https://dotnet.microsoft.com/en-us/platform/dotnet-standard)
810

9-
An unofficial .NET library for interacting with Grok's API, with `GrokThread` as its centerpiece. This library streamlines conversation management, tool integration (e.g., image generation), and real-time streaming in .NET applications.
11+
An unofficial .NET library for interacting with Grok's API, with `GrokThread` as its centerpiece. This library streamlines conversation management, tool integration (e.g., image generation and reasoning), and real-time streaming in .NET applications.
1012

1113
## Installation
1214

@@ -46,12 +48,18 @@ internal class Program
4648
// Show welcome message
4749
Console.WriteLine("Welcome to the Grok Chat Console. Type your questions below. Type 'quit' to exit.");
4850

49-
// Register an image generation tool
51+
// Register your own tool (Generic)
5052
thread.RegisterTool(new GrokToolDefinition(
51-
ImageGeneratorHelper.GrokToolName,
52-
"Generate images based on user input request;",
53-
ImageGeneratorHelper.GrokArgsRequest,
54-
ImageGeneratorHelper.ProcessGrokRequestForImage));
53+
"Tool Name",
54+
"Tool Instruction",
55+
"Tool Arguments",
56+
<ToolExecutionFunctionCallback()>));
57+
58+
// Built in Grok Image Generation Tool
59+
thread.RegisterTool(new GrokToolImageGeneration(sdk))
60+
61+
// Built in Grok Reasoning Tool
62+
thread.RegisterTool(new GrokToolReasoning(sdk))
5563

5664
// Preload context with neutral discussion (non-API processed)
5765
thread.AddUserMessage("Alex Said: I tried a new coffee shop today.");
@@ -98,6 +106,10 @@ internal class Program
98106
Console.ForegroundColor = ConsoleColor.Red;
99107
Console.WriteLine("Error Processing...");
100108
break;
109+
case StreamState.CallingTool:
110+
Console.ForegroundColor = ConsoleColor.Magenta;
111+
Console.WriteLine("Calling Tool...");
112+
break;
101113
}
102114
Console.ResetColor();
103115
}
@@ -197,6 +209,44 @@ internal static class ImageGeneratorHelper
197209
- **Compression**: Shrinks history with `CompressHistoryAsync` to optimize tokens.
198210
- **State Tracking**: Monitors states (`Thinking`, `Streaming`, etc.) for feedback.
199211

212+
## New Built-in Tools
213+
214+
### Image Generation Tool
215+
The `GrokToolImageGeneration` tool allows Grok to generate images based on a text prompt. Users can specify the number of images and the response format (`url` or `base64`).
216+
217+
#### Usage Example
218+
```csharp
219+
var thread = new GrokThread(client);
220+
thread.RegisterTool(new GrokToolImageGeneration(client));
221+
thread.AddSystemInstruction("You can generate images using the 'generate_image' tool.");
222+
223+
await foreach (var message in thread.AskQuestion("Generate an image of a sunset"))
224+
{
225+
if (message is GrokToolResponse toolResponse && toolResponse.ToolName == "generate_image")
226+
{
227+
Console.WriteLine($"Image URL: {toolResponse.ToolResponse}");
228+
}
229+
}
230+
```
231+
232+
### Reasoning Tool
233+
The `GrokReasoningTool` enables Grok to perform complex reasoning on a given problem, with configurable effort levels ("low" or "high").
234+
235+
#### Usage Example
236+
```csharp
237+
var thread = new GrokThread(client);
238+
thread.RegisterTool(new GrokReasoningTool(client));
239+
thread.AddSystemInstruction("You can perform reasoning using the 'reason' tool.");
240+
241+
await foreach (var message in thread.AskQuestion("Explain why the sky is blue with high effort"))
242+
{
243+
if (message is GrokToolResponse toolResponse && toolResponse.ToolName == "reason")
244+
{
245+
Console.WriteLine($"Reasoning: {toolResponse.ToolResponse}");
246+
}
247+
}
248+
```
249+
200250
## Additional Examples
201251

202252
### Simple Chat
@@ -245,4 +295,4 @@ Visit [GitHub](https://github.com/twhidden/grok):
245295

246296
## License
247297

248-
This project is licensed under the MIT License, free for everyone to use, modify, and distribute with no cost or warranty
298+
This project is licensed under the MIT License, free for everyone to use, modify, and distribute with no cost or warranty.

src/GrokSdk.ConsoleDemo/Program.cs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
using GrokSdk;
2+
using GrokSdk.Tools;
23

34
internal class Program
45
{
56
private static async Task Main(string[] args)
67
{
78
// Initialize the HTTP client and GrokClient
9+
string apiKey = GetApiKey();
810
var httpClient = new HttpClient();
9-
var sdk = new GrokClient(httpClient,
10-
"xai-Key-Here");
11+
var sdk = new GrokClient(httpClient, apiKey);
1112

1213
// Create a GrokThread instance to manage the conversation
1314
var thread = new GrokThread(sdk);
1415

16+
// Register some pre-made tools
17+
thread.RegisterTool(new GrokToolImageGeneration(sdk));
18+
thread.RegisterTool(new GrokToolReasoning(sdk));
19+
1520
// Welcome message
1621
Console.WriteLine("Welcome to the Grok Chat Console. Type your questions below. Type 'quit' to exit.");
1722

@@ -54,6 +59,10 @@ private static async Task Main(string[] args)
5459
Console.ForegroundColor = ConsoleColor.Red;
5560
Console.WriteLine("Error Processing...");
5661
break;
62+
case StreamState.CallingTool:
63+
Console.ForegroundColor = ConsoleColor.Magenta;
64+
Console.WriteLine("Calling Tool...");
65+
break;
5766
}
5867

5968
Console.ResetColor();
@@ -78,4 +87,32 @@ private static async Task Main(string[] args)
7887
// Farewell message
7988
Console.WriteLine("Goodbye!");
8089
}
90+
91+
/// <summary>
92+
/// Retrieves the xAI API key, either from a stored file in the current directory or by prompting the user.
93+
/// </summary>
94+
/// <returns>The xAI API key.</returns>
95+
private static string GetApiKey()
96+
{
97+
string filePath = Path.Combine(Directory.GetCurrentDirectory(), ".xai_key");
98+
if (File.Exists(filePath))
99+
{
100+
string key = File.ReadAllText(filePath).Trim();
101+
if (!string.IsNullOrEmpty(key))
102+
{
103+
return key;
104+
}
105+
}
106+
107+
Console.WriteLine("Please enter your xAI API key:");
108+
string enteredKey = Console.ReadLine().Trim();
109+
while (string.IsNullOrEmpty(enteredKey))
110+
{
111+
Console.WriteLine("API key cannot be empty. Please enter a valid key:");
112+
enteredKey = Console.ReadLine().Trim();
113+
}
114+
115+
File.WriteAllText(filePath, enteredKey);
116+
return enteredKey;
117+
}
81118
}

src/GrokSdk.Tests/GrokToolTests.cs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
using GrokSdk.Tools;
2+
using Newtonsoft.Json;
3+
4+
namespace GrokSdk.Tests;
5+
6+
[TestClass]
7+
public class GrokToolTests : GrokClientTestBaseClass
8+
{
9+
[ClassInitialize]
10+
public static void ClassInitialize(TestContext context)
11+
{
12+
ApiToken = GetApiKeyFromFileOrEnv();
13+
}
14+
15+
[TestMethod]
16+
[TestCategory("Live")]
17+
public async Task ExecuteAsync_LiveImageGeneration_ReturnsImageData()
18+
{
19+
using var httpClient = new HttpClient();
20+
var client = new GrokClient(httpClient, ApiToken ?? throw new Exception("API Token not set"));
21+
22+
// Test with 'url' response format
23+
var toolUrl = new GrokToolImageGeneration(client);
24+
var argsUrl = JsonConvert.SerializeObject(new
25+
{ prompt = "A serene mountain landscape", n = 1, response_format = "url" });
26+
await WaitForRateLimitAsync();
27+
string? resultUrl = null;
28+
try
29+
{
30+
resultUrl = await toolUrl.ExecuteAsync(argsUrl);
31+
}
32+
catch (GrokSdkException ex)
33+
{
34+
Assert.Fail($"API call failed with status {ex.StatusCode}: {ex.Message}\nResponse: {ex.Response}");
35+
}
36+
37+
Assert.IsNotNull(resultUrl, "Response should not be null.");
38+
var responseUrl = JsonConvert.DeserializeObject<dynamic>(resultUrl);
39+
Assert.IsNull(responseUrl.error, "Error should be null.");
40+
Assert.IsNotNull(responseUrl.images, "Images should not be null.");
41+
Assert.AreEqual(1, responseUrl.images.Count, "Expected 1 image.");
42+
Assert.IsTrue(Uri.IsWellFormedUriString(responseUrl.images[0].url.ToString(), UriKind.Absolute),
43+
"Image URL should be valid.");
44+
45+
// Test with 'base64' response format
46+
var toolBase64 = new GrokToolImageGeneration(client);
47+
var argsBase64 =
48+
JsonConvert.SerializeObject(new { prompt = "A futuristic robot", n = 1, response_format = "base64" });
49+
await WaitForRateLimitAsync();
50+
string resultBase64 = null;
51+
try
52+
{
53+
resultBase64 = await toolBase64.ExecuteAsync(argsBase64);
54+
}
55+
catch (GrokSdkException ex)
56+
{
57+
Assert.Fail($"API call failed with status {ex.StatusCode}: {ex.Message}\nResponse: {ex.Response}");
58+
}
59+
60+
Assert.IsNotNull(resultBase64, "Response should not be null.");
61+
var responseBase64 = JsonConvert.DeserializeObject<dynamic>(resultBase64);
62+
Assert.IsNull(responseBase64.error, "Error should be null.");
63+
Assert.IsNotNull(responseBase64.images, "Images should not be null.");
64+
Assert.AreEqual(1, responseBase64.images.Count, "Expected 1 image.");
65+
Assert.IsNotNull(responseBase64.images[0].b64_json, "Base64 image data should not be null.");
66+
67+
// Safety Check for Live Unit Tests to prevent API exhaustion
68+
await WaitForRateLimitAsync();
69+
}
70+
71+
[TestMethod]
72+
[TestCategory("Live")]
73+
public async Task GrokThread_WithImageTool_GeneratesTwoImageUrls()
74+
{
75+
// Arrange
76+
using var httpClient = new HttpClient();
77+
var client = new GrokClient(httpClient, ApiToken ?? throw new Exception("API Token not set"));
78+
79+
var thread = new GrokThread(client);
80+
thread.RegisterTool(new GrokToolImageGeneration(client));
81+
82+
thread.AddSystemInstruction(
83+
"You are an assistant that can generate images using the 'generate_image' tool. " +
84+
"When the user asks to generate images, use the tool with the specified prompt and number of images, " +
85+
"and include the image URLs in your response."
86+
);
87+
88+
var userMessage =
89+
"Use the generate_image tool to generate two images of a sunset with response_format 'url' and return their URLs.";
90+
91+
// Act
92+
await WaitForRateLimitAsync();
93+
94+
var toolCalled = false;
95+
string toolResponseJson = null;
96+
97+
await foreach (var message in thread.AskQuestion(userMessage))
98+
if (message is GrokToolResponse { ToolName: "generate_image" } toolResponse1)
99+
{
100+
toolCalled = true;
101+
toolResponseJson = toolResponse1.ToolResponse;
102+
break; // Assuming only one tool call for simplicity
103+
}
104+
105+
// Assert
106+
Assert.IsTrue(toolCalled, "The 'generate_image' tool was not called.");
107+
108+
var toolResponse = JsonConvert.DeserializeObject<dynamic>(toolResponseJson);
109+
if (toolResponse.error != null) Assert.Fail($"Tool returned an error: {toolResponse.error}");
110+
111+
var images = toolResponse.images;
112+
Assert.IsNotNull(images, "Images array is missing.");
113+
Assert.AreEqual(2, images.Count, "Expected two images.");
114+
115+
foreach (var image in images)
116+
{
117+
Assert.IsNotNull(image.url, "Image URL is missing.");
118+
Assert.IsTrue(Uri.IsWellFormedUriString(image.url.ToString(), UriKind.Absolute), "Image URL is invalid.");
119+
}
120+
121+
await WaitForRateLimitAsync();
122+
}
123+
124+
[TestMethod]
125+
[TestCategory("Live")]
126+
public async Task ExecuteAsync_LiveReasoning_ReturnsReasoningResult()
127+
{
128+
using var httpClient = new HttpClient();
129+
var client = new GrokClient(httpClient, ApiToken ?? throw new Exception("API Token not set"));
130+
131+
// Test with effort "low"
132+
var tool = new GrokToolReasoning(client);
133+
var argsLow = JsonConvert.SerializeObject(new { problem = "Why is the sky blue?", effort = "low" });
134+
await WaitForRateLimitAsync();
135+
string? resultLow = null;
136+
try
137+
{
138+
resultLow = await tool.ExecuteAsync(argsLow);
139+
}
140+
catch (GrokSdkException ex)
141+
{
142+
Assert.Fail($"API call failed with status {ex.StatusCode}: {ex.Message}\nResponse: {ex.Response}");
143+
}
144+
145+
Assert.IsNotNull(resultLow, "Response should not be null.");
146+
var responseLow = JsonConvert.DeserializeObject<dynamic>(resultLow);
147+
Assert.IsNull(responseLow.error, "Error should be null for valid request.");
148+
Assert.IsNotNull(responseLow.reasoning, "Reasoning should not be null.");
149+
Assert.IsTrue(responseLow.reasoning.ToString().Length > 0, "Reasoning should not be empty.");
150+
151+
// Test with effort "high"
152+
var argsHigh =
153+
JsonConvert.SerializeObject(new { problem = "Explain quantum mechanics briefly.", effort = "high" });
154+
await WaitForRateLimitAsync();
155+
string resultHigh = null;
156+
try
157+
{
158+
resultHigh = await tool.ExecuteAsync(argsHigh);
159+
}
160+
catch (GrokSdkException ex)
161+
{
162+
Assert.Fail($"API call failed with status {ex.StatusCode}: {ex.Message}\nResponse: {ex.Response}");
163+
}
164+
165+
Assert.IsNotNull(resultHigh, "Response should not be null.");
166+
var responseHigh = JsonConvert.DeserializeObject<dynamic>(resultHigh);
167+
Assert.IsNull(responseHigh.error, "Error should be null for valid request.");
168+
Assert.IsNotNull(responseHigh.reasoning, "Reasoning should not be null.");
169+
Assert.IsTrue(responseHigh.reasoning.ToString().Length > 0, "Reasoning should not be empty.");
170+
171+
// Test with invalid effort
172+
var argsInvalidEffort = JsonConvert.SerializeObject(new { problem = "Some problem", effort = "medium" });
173+
await WaitForRateLimitAsync();
174+
string resultInvalidEffort = null;
175+
try
176+
{
177+
resultInvalidEffort = await tool.ExecuteAsync(argsInvalidEffort);
178+
}
179+
catch (GrokSdkException ex)
180+
{
181+
Assert.Fail($"API call failed with status {ex.StatusCode}: {ex.Message}\nResponse: {ex.Response}");
182+
}
183+
184+
Assert.IsNotNull(resultInvalidEffort, "Response should not be null.");
185+
var responseInvalidEffort = JsonConvert.DeserializeObject<dynamic>(resultInvalidEffort);
186+
Assert.IsNotNull(responseInvalidEffort.error, "Error should not be null for invalid effort.");
187+
Assert.AreEqual("Invalid effort level. Must be 'low' or 'high'.", responseInvalidEffort.error.ToString());
188+
189+
// Test with missing problem
190+
var argsMissingProblem = JsonConvert.SerializeObject(new { effort = "low" });
191+
await WaitForRateLimitAsync();
192+
string resultMissingProblem = null;
193+
try
194+
{
195+
resultMissingProblem = await tool.ExecuteAsync(argsMissingProblem);
196+
}
197+
catch (GrokSdkException ex)
198+
{
199+
Assert.Fail($"API call failed with status {ex.StatusCode}: {ex.Message}\nResponse: {ex.Response}");
200+
}
201+
202+
Assert.IsNotNull(resultMissingProblem, "Response should not be null.");
203+
var responseMissingProblem = JsonConvert.DeserializeObject<dynamic>(resultMissingProblem);
204+
Assert.IsNotNull(responseMissingProblem.error, "Error should not be null for missing problem.");
205+
Assert.AreEqual("Problem cannot be empty.", responseMissingProblem.error.ToString());
206+
207+
// Safety Check for Live Unit Tests to prevent API exhaustion
208+
await WaitForRateLimitAsync();
209+
}
210+
}

src/GrokSdk/GrokSdk.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<!-- NuGet Metadata -->
1010
<NuspecFile>GrokSdk.nuspec</NuspecFile>
1111
<PackageId>Grok</PackageId>
12-
<Version>0.5.0</Version>
12+
<Version>1.0.0</Version>
1313
<Authors>TWhidden</Authors>
1414
<Description>xAI Grok dotnet integration</Description>
1515
<PackageTags>xAI;Grok;dotnet</PackageTags>

0 commit comments

Comments
 (0)