MarketAlly.AIPlugin.Extensions/TestConversationApp/Program.cs

541 lines
18 KiB
C#
Executable File

using MarketAlly.AIPlugin;
using MarketAlly.AIPlugin.Conversation;
using MarketAlly.AIPlugin.Conversation.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace TestConversationApp
{
// Test plugin 1: Calculator
[AIPlugin("Calculator", "Performs basic arithmetic operations", FriendlyName = "Math Calculator")]
public class CalculatorPlugin : IAIPlugin
{
[AIParameter("First number", required: true)]
public double Number1 { get; set; }
[AIParameter("Second number", required: true)]
public double Number2 { get; set; }
[AIParameter("Operation (add, subtract, multiply, divide)", required: true)]
public string Operation { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["number1"] = typeof(double),
["number2"] = typeof(double),
["operation"] = typeof(string)
};
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
double n1 = Convert.ToDouble(parameters["number1"]);
double n2 = Convert.ToDouble(parameters["number2"]);
string op = parameters["operation"].ToString().ToLower();
double result = op switch
{
"add" => n1 + n2,
"subtract" => n1 - n2,
"multiply" => n1 * n2,
"divide" => n2 != 0 ? n1 / n2 : throw new DivideByZeroException(),
_ => throw new ArgumentException($"Unknown operation: {op}")
};
await Task.CompletedTask;
return new AIPluginResult(result, $"Calculated {n1} {op} {n2} = {result}");
}
}
// Test plugin 2: Weather (for structured output)
[AIPlugin("GetWeather", "Get current weather for a city", FriendlyName = "Weather Service")]
public class WeatherPlugin : IAIPlugin
{
[AIParameter("City name", required: true)]
public string City { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["city"] = typeof(string)
};
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var city = parameters["city"].ToString();
// Simulated weather data
var weatherData = new
{
city = city,
temperature = 72,
condition = "Sunny",
humidity = 45,
windSpeed = 10
};
await Task.CompletedTask;
return new AIPluginResult(weatherData);
}
}
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("=== AIConversation Test App ===\n");
// Collect API keys for all providers
var apiKeys = new Dictionary<AIProvider, string>();
var providerModels = new Dictionary<AIProvider, string>();
var claudeKey = "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";// Environment.GetEnvironmentVariable("CLAUDE_API_KEY");
if (!string.IsNullOrEmpty(claudeKey))
{
apiKeys[AIProvider.Claude] = claudeKey;
providerModels[AIProvider.Claude] = ModelConstants.Claude.Sonnet45;
}
var openaiKey = "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";// Environment.GetEnvironmentVariable("OPENAI_API_KEY");
if (!string.IsNullOrEmpty(openaiKey))
{
apiKeys[AIProvider.OpenAI] = openaiKey;
providerModels[AIProvider.OpenAI] = "gpt-4o";
}
var geminiKey = "AIzaSyDKdJeSOoL9UxdcxEPVzUEqcIiqRkkJ2mM";//Environment.GetEnvironmentVariable("GEMINI_API_KEY");
if (!string.IsNullOrEmpty(geminiKey))
{
apiKeys[AIProvider.Gemini] = geminiKey;
providerModels[AIProvider.Gemini] = "gemini-2.0-flash-exp";
}
// Check if any API keys are available
if (apiKeys.Count == 0)
{
Console.WriteLine("ERROR: No API keys found.");
Console.WriteLine("Please set at least one of the following environment variables:");
Console.WriteLine(" CLAUDE_API_KEY - For Claude API");
Console.WriteLine(" OPENAI_API_KEY - For OpenAI API");
Console.WriteLine(" GEMINI_API_KEY - For Gemini API");
Console.WriteLine("\nExample:");
Console.WriteLine(" export CLAUDE_API_KEY='your-key-here' (Linux/Mac)");
Console.WriteLine(" $env:CLAUDE_API_KEY='your-key-here' (PowerShell)");
return;
}
Console.WriteLine($"Found API keys for {apiKeys.Count} provider(s): {string.Join(", ", apiKeys.Keys)}");
Console.WriteLine();
try
{
// Run tests 1-6 and 8 for each available provider
foreach (var providerEntry in apiKeys)
{
var provider = providerEntry.Key;
var apiKey = providerEntry.Value;
var model = providerModels[provider];
Console.WriteLine(new string('=', 80));
Console.WriteLine($"TESTING PROVIDER: {provider.ToString().ToUpper()} (Model: {model})");
Console.WriteLine(new string('=', 80));
Console.WriteLine();
try
{
// Non-streaming tests
await Test1_BasicConversation(provider, apiKey, model);
Console.WriteLine("\n" + new string('-', 60) + "\n");
await Test2_WithTools(provider, apiKey, model);
Console.WriteLine("\n" + new string('-', 60) + "\n");
await Test3_StructuredOutput(provider, apiKey, model);
Console.WriteLine("\n" + new string('-', 60) + "\n");
await Test4_DynamicTools(provider, apiKey, model);
Console.WriteLine("\n" + new string('-', 60) + "\n");
// Streaming tests
await Test5_BasicStreaming(provider, apiKey, model);
Console.WriteLine("\n" + new string('-', 60) + "\n");
await Test6_StreamingWithTools(provider, apiKey, model);
Console.WriteLine("\n" + new string('-', 60) + "\n");
await Test8_StreamingWithCustomOptions(provider, apiKey, model);
Console.WriteLine("\n" + new string('-', 60) + "\n");
Console.WriteLine($"✅ All tests completed successfully for {provider}!");
}
catch (Exception ex)
{
Console.WriteLine($"\n❌ ERROR testing {provider}: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
Console.WriteLine();
}
// Run Test7 separately (it handles multiple providers)
Console.WriteLine(new string('=', 80));
Console.WriteLine("MULTI-PROVIDER TEST");
Console.WriteLine(new string('=', 80));
Console.WriteLine();
await Test7_StreamingWithMultipleProviders(apiKeys);
Console.WriteLine("\n" + new string('-', 60) + "\n");
// Summary
Console.WriteLine(new string('=', 80));
Console.WriteLine("SUMMARY");
Console.WriteLine(new string('=', 80));
Console.WriteLine($"✅ All tests completed for {apiKeys.Count} provider(s): {string.Join(", ", apiKeys.Keys)}");
}
catch (Exception ex)
{
Console.WriteLine($"\n❌ FATAL ERROR: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
}
static async Task Test1_BasicConversation(AIProvider provider, string apiKey, string model)
{
Console.WriteLine($"TEST 1 [{provider.ToString().ToUpper()}]: Basic Conversation (No Tools)");
Console.WriteLine("---------------------------------------");
var conversation = AIConversationBuilder.Create()
.UseProvider(provider, apiKey)
.UseModel(model)
.Build();
var response = await conversation.SendAsync("Say hello in exactly 3 words");
Console.WriteLine($"Response: {response.FinalMessage}");
Console.WriteLine($"Tokens used: {response.TotalTokensUsed}");
Console.WriteLine($"Duration: {response.Duration.TotalMilliseconds}ms");
}
static async Task Test2_WithTools(AIProvider provider, string apiKey, string model)
{
Console.WriteLine($"TEST 2 [{provider.ToString().ToUpper()}]: Conversation with Automatic Tool Execution");
Console.WriteLine("---------------------------------------------------");
var conversation = AIConversationBuilder.Create()
.UseProvider(provider, apiKey)
.UseModel(model)
.RegisterPlugin<CalculatorPlugin>()
.Build();
var response = await conversation.SendAsync("What is 25 multiplied by 48?");
Console.WriteLine($"Response: {response.FinalMessage}");
Console.WriteLine($"Tools executed: {response.ToolExecutionCount}");
Console.WriteLine($"Tokens used: {response.TotalTokensUsed}");
if (response.ToolResults.Count > 0)
{
Console.WriteLine("\nTool Results:");
foreach (var result in response.ToolResults)
{
Console.WriteLine($" - {result.ToolName}: {result.Result}");
}
}
}
static async Task Test3_StructuredOutput(AIProvider provider, string apiKey, string model)
{
Console.WriteLine($"TEST 3 [{provider.ToString().ToUpper()}]: Structured Output (No AI Chatter)");
Console.WriteLine("------------------------------------------");
var conversation = AIConversationBuilder.Create()
.UseProvider(provider, apiKey)
.UseModel(model)
.RegisterPlugin<WeatherPlugin>()
.Build();
var response = await conversation.SendForStructuredOutputAsync<object>(
"Get weather for London",
"GetWeather"
);
Console.WriteLine($"Structured output received: {response.IsStructuredOutput}");
if (response.IsStructuredOutput)
{
dynamic weather = response.StructuredOutput;
Console.WriteLine($"City: {weather.city}");
Console.WriteLine($"Temperature: {weather.temperature}°F");
Console.WriteLine($"Condition: {weather.condition}");
Console.WriteLine($"Humidity: {weather.humidity}%");
Console.WriteLine($"Wind Speed: {weather.windSpeed} mph");
}
Console.WriteLine($"AI Message: {response.FinalMessage ?? "(none - structured output only)"}");
}
static async Task Test4_DynamicTools(AIProvider provider, string apiKey, string model)
{
Console.WriteLine($"TEST 4 [{provider.ToString().ToUpper()}]: Dynamic Tool Loading/Unloading");
Console.WriteLine("---------------------------------------");
var conversation = AIConversationBuilder.Create()
.UseProvider(provider, apiKey)
.UseModel(model)
.WithDynamicTools(true)
.RegisterPlugin<CalculatorPlugin>(activateImmediately: false)
.RegisterPlugin<WeatherPlugin>(activateImmediately: false)
.Build();
// Request 1: Only calculator
Console.WriteLine("\nRequest 1: Activating Calculator only");
conversation.ActivatePlugins("Calculator");
var activePlugins = conversation.GetActivePlugins();
Console.WriteLine($"Active plugins: {string.Join(", ", activePlugins)}");
var response1 = await conversation.SendAsync("Calculate 100 divided by 4");
Console.WriteLine($"Response: {response1.FinalMessage}");
// Request 2: Only weather
Console.WriteLine("\nRequest 2: Switching to Weather only");
conversation.DeactivatePlugins("Calculator");
conversation.ActivatePlugins("GetWeather");
activePlugins = conversation.GetActivePlugins();
Console.WriteLine($"Active plugins: {string.Join(", ", activePlugins)}");
var response2 = await conversation.SendAsync("What's the weather in Paris?");
Console.WriteLine($"Response: {response2.FinalMessage}");
}
static async Task Test5_BasicStreaming(AIProvider provider, string apiKey, string model)
{
Console.WriteLine($"TEST 5 [{provider.ToString().ToUpper()}]: Basic Streaming (No Tools)");
Console.WriteLine("-----------------------------------");
var conversation = AIConversationBuilder.Create()
.UseProvider(provider, apiKey)
.UseModel(model)
.Build();
Console.Write("Response: ");
var fullText = "";
await foreach (var chunk in conversation.SendStreamingAsync("Write a haiku about coding"))
{
switch (chunk.Type)
{
case ConversationStreamingResponseType.TextDelta:
Console.Write(chunk.TextDelta);
fullText += chunk.TextDelta;
break;
case ConversationStreamingResponseType.Complete:
Console.WriteLine();
Console.WriteLine($"\nTokens used: {chunk.TotalTokensUsed} (in: {chunk.InputTokens}, out: {chunk.OutputTokens})");
Console.WriteLine($"Duration: {chunk.Elapsed?.TotalMilliseconds}ms");
Console.WriteLine($"Stop reason: {chunk.StopReason}");
break;
case ConversationStreamingResponseType.Error:
Console.WriteLine($"\n❌ Error: {chunk.ErrorMessage}");
break;
}
}
}
static async Task Test6_StreamingWithTools(AIProvider provider, string apiKey, string model)
{
Console.WriteLine($"TEST 6 [{provider.ToString().ToUpper()}]: Streaming with Automatic Tool Execution");
Console.WriteLine("------------------------------------------------");
var conversation = AIConversationBuilder.Create()
.UseProvider(provider, apiKey)
.UseModel(model)
.RegisterPlugin<CalculatorPlugin>()
.RegisterPlugin<WeatherPlugin>()
.Build();
Console.WriteLine("Query: Calculate 156 times 89, then tell me the weather in Tokyo\n");
Console.Write("Response: ");
var toolCount = 0;
await foreach (var chunk in conversation.SendStreamingAsync("Calculate 156 times 89, then tell me the weather in Tokyo"))
{
switch (chunk.Type)
{
case ConversationStreamingResponseType.TextDelta:
Console.Write(chunk.TextDelta);
break;
case ConversationStreamingResponseType.ToolCallDetected:
Console.WriteLine($"\n\n🔧 Tool detected: {chunk.ToolFriendlyName ?? chunk.ToolName} (ID: {chunk.ToolCallId})");
break;
case ConversationStreamingResponseType.ToolExecutionStarting:
Console.Write($" ▶ Executing {chunk.ToolFriendlyName ?? chunk.ToolName}...");
break;
case ConversationStreamingResponseType.ToolExecutionComplete:
Console.WriteLine($" ✅");
Console.WriteLine($" Result: {chunk.ToolResult.Result}");
toolCount++;
break;
case ConversationStreamingResponseType.ToolExecutionFailed:
Console.WriteLine($" ❌");
Console.WriteLine($" Error: {chunk.ErrorMessage}");
break;
case ConversationStreamingResponseType.ContinuingAfterTools:
Console.WriteLine("\n🔄 Continuing conversation after tool execution...\n");
Console.Write("Response: ");
break;
case ConversationStreamingResponseType.Complete:
Console.WriteLine();
Console.WriteLine($"\n📊 Summary:");
Console.WriteLine($" Tools executed: {chunk.ToolExecutionCount}");
Console.WriteLine($" Tokens used: {chunk.TotalTokensUsed} (in: {chunk.InputTokens}, out: {chunk.OutputTokens})");
Console.WriteLine($" Duration: {chunk.Elapsed?.TotalMilliseconds}ms");
break;
case ConversationStreamingResponseType.Error:
Console.WriteLine($"\n❌ Error: {chunk.ErrorMessage}");
break;
}
}
}
static async Task Test7_StreamingWithMultipleProviders(Dictionary<AIProvider, string> apiKeys)
{
Console.WriteLine("TEST 7: Streaming Across Multiple Providers");
Console.WriteLine("--------------------------------------------");
var providerModels = new Dictionary<AIProvider, (string Model, string Name)>
{
{ AIProvider.Claude, (ModelConstants.Claude.Sonnet45, "Claude") },
{ AIProvider.OpenAI, ("gpt-4o", "OpenAI") },
{ AIProvider.Gemini, ("gemini-2.0-flash-exp", "Gemini") }
};
foreach (var providerEntry in apiKeys)
{
var provider = providerEntry.Key;
var key = providerEntry.Value;
if (!providerModels.ContainsKey(provider))
continue;
var (model, name) = providerModels[provider];
try
{
Console.WriteLine($"\n{name} ({model}):");
Console.Write(" ");
var conversation = AIConversationBuilder.Create()
.UseProvider(provider, key)
.UseModel(model)
.RegisterPlugin<CalculatorPlugin>()
.Build();
await foreach (var chunk in conversation.SendStreamingAsync("What is 12 times 12?"))
{
switch (chunk.Type)
{
case ConversationStreamingResponseType.TextDelta:
Console.Write(chunk.TextDelta);
break;
case ConversationStreamingResponseType.ToolCallDetected:
Console.Write($"[Tool: {chunk.ToolFriendlyName}] ");
break;
case ConversationStreamingResponseType.Complete:
Console.WriteLine($"\n ✅ Completed in {chunk.Elapsed?.TotalMilliseconds}ms");
break;
case ConversationStreamingResponseType.Error:
Console.WriteLine($"\n ❌ Error: {chunk.ErrorMessage}");
break;
}
}
}
catch (Exception ex)
{
Console.WriteLine($" ❌ Failed: {ex.Message}");
}
}
}
static async Task Test8_StreamingWithCustomOptions(AIProvider provider, string apiKey, string model)
{
Console.WriteLine($"TEST 8 [{provider.ToString().ToUpper()}]: Streaming with Custom Options");
Console.WriteLine("--------------------------------------");
var conversation = AIConversationBuilder.Create()
.UseProvider(provider, apiKey)
.UseModel(model)
.RegisterPlugin<CalculatorPlugin>()
.WithStreaming(options =>
{
options.EnableStreaming = true;
options.BufferTextDeltas = true;
options.BufferSize = 20;
options.EnableHeartbeat = true;
options.HeartbeatInterval = TimeSpan.FromSeconds(1);
options.IncludeToolArguments = true;
options.UseFriendlyToolNames = true;
})
.Build();
Console.WriteLine("Query: Calculate 999 multiplied by 888\n");
Console.Write("Response: ");
await foreach (var chunk in conversation.SendStreamingAsync("Calculate 999 multiplied by 888"))
{
switch (chunk.Type)
{
case ConversationStreamingResponseType.TextDelta:
Console.Write(chunk.TextDelta);
break;
case ConversationStreamingResponseType.ToolCallDetected:
Console.WriteLine($"\n\n🔧 Tool: {chunk.ToolFriendlyName}");
break;
case ConversationStreamingResponseType.ToolExecutionStarting:
if (chunk.ToolArguments != null)
{
Console.WriteLine($" Arguments:");
foreach (var arg in chunk.ToolArguments)
{
Console.WriteLine($" {arg.Key}: {arg.Value}");
}
}
Console.Write($" Executing...");
break;
case ConversationStreamingResponseType.ToolExecutionComplete:
Console.WriteLine($" ✅");
break;
case ConversationStreamingResponseType.Heartbeat:
Console.Write(".");
break;
case ConversationStreamingResponseType.Complete:
Console.WriteLine();
Console.WriteLine($"\n📊 Buffered text streaming with size={20}");
Console.WriteLine($" Tokens: {chunk.TotalTokensUsed}, Duration: {chunk.Elapsed?.TotalMilliseconds}ms");
break;
case ConversationStreamingResponseType.Error:
Console.WriteLine($"\n❌ Error: {chunk.ErrorMessage}");
break;
}
}
}
}
}