364 lines
No EOL
13 KiB
C#
364 lines
No EOL
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace GumYum.NPC {
|
|
/// <summary>
|
|
/// Manager for NPC-related operations
|
|
/// </summary>
|
|
public class NPCManager {
|
|
private readonly Client _client;
|
|
|
|
public NPCManager(Client client) { _client = client; }
|
|
|
|
/// <summary>
|
|
/// Spawn a character - returns NPC object
|
|
/// Pass null for npcId to spawn a random NPC
|
|
/// </summary>
|
|
public async Task<NPC>
|
|
SpawnAsync(string universeId, int seed, long? npcId = null,
|
|
CancellationToken cancellationToken = default) {
|
|
var queryParams =
|
|
new Dictionary<string, string> { ["universe_id"] = universeId,
|
|
["seed"] = seed.ToString() };
|
|
|
|
string endpoint;
|
|
if (npcId.HasValue) {
|
|
// Spawn specific NPC
|
|
queryParams["npc_id"] = npcId.Value.ToString();
|
|
endpoint = "npc";
|
|
} else {
|
|
// Spawn random NPC - let API handle the randomness
|
|
endpoint = "npc/spawn";
|
|
}
|
|
|
|
var response = await _client.RequestAsync<Dictionary<string, object>>(
|
|
HttpMethod.Get, endpoint, null, queryParams, cancellationToken);
|
|
|
|
// Check if this is a redirect response
|
|
if (response.ContainsKey("redirect_url")) {
|
|
// Follow the redirect to get actual NPC data
|
|
var redirectPath = response["redirect_url"].ToString();
|
|
// Strip the /v1/ prefix since client adds it
|
|
if (redirectPath.StartsWith("/v1/")) {
|
|
redirectPath = redirectPath.Substring(4);
|
|
}
|
|
|
|
// Make the redirect request
|
|
var npcData = await _client.RequestAsync<Dictionary<string, object>>(
|
|
HttpMethod.Get, redirectPath, null, null, cancellationToken);
|
|
|
|
var npc = new NPC(npcData, _client);
|
|
_client.RaiseCharacterSpawned(npc);
|
|
return npc;
|
|
} else {
|
|
// Direct response (shouldn't happen with /npc/spawn)
|
|
var npc = new NPC(response, _client);
|
|
_client.RaiseCharacterSpawned(npc);
|
|
return npc;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawn with advanced filters - supports any NPC field
|
|
/// Examples:
|
|
/// SpawnFilteredAsync("kingdom", 12345, new { profession = new[] {
|
|
/// "warrior", "knight" }, location = new[] { "barracks", "castle" } })
|
|
/// SpawnFilteredAsync("blade-runner", 67890, new { personality_type = new[]
|
|
/// { 8 }, sex = new[] { "female" } })
|
|
/// </summary>
|
|
public async Task<NPC>
|
|
SpawnFilteredAsync(string universeId, int seed, object filters = null,
|
|
CancellationToken cancellationToken = default) {
|
|
var data = new { universe_id = universeId, world_seed = seed,
|
|
filters = filters ?? new {} };
|
|
|
|
var endpoint = "npc/spawn_filtered";
|
|
|
|
// spawn_filtered uses POST with data in body
|
|
var response = await _client.RequestAsync<Dictionary<string, object>>(
|
|
HttpMethod.Post, endpoint, data, null, cancellationToken);
|
|
|
|
// Check if this is a redirect response
|
|
if (response.ContainsKey("redirect_url")) {
|
|
// Follow the redirect to get actual NPC data
|
|
var redirectPath = response["redirect_url"].ToString();
|
|
// Strip the /v1/ prefix since client adds it
|
|
if (redirectPath.StartsWith("/v1/")) {
|
|
redirectPath = redirectPath.Substring(4);
|
|
}
|
|
|
|
// Make the redirect request
|
|
var npcData = await _client.RequestAsync<Dictionary<string, object>>(
|
|
HttpMethod.Get, redirectPath, null, null, cancellationToken);
|
|
|
|
var npc = new NPC(npcData, _client);
|
|
_client.RaiseCharacterSpawned(npc);
|
|
return npc;
|
|
} else {
|
|
// Direct response (shouldn't happen with /npc/spawn_filtered)
|
|
var npc = new NPC(response, _client);
|
|
_client.RaiseCharacterSpawned(npc);
|
|
return npc;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manager for Universe-related operations
|
|
/// </summary>
|
|
public class UniverseManager {
|
|
private readonly Client _client;
|
|
|
|
public UniverseManager(Client client) { _client = client; }
|
|
|
|
/// <summary>
|
|
/// List public universes (works with API keys)
|
|
/// </summary>
|
|
public async Task<List<Dictionary<string, object>>>
|
|
ListPublicAsync(CancellationToken cancellationToken = default) {
|
|
// API returns {"public_universes": [...]}
|
|
var response = await _client.RequestAsync<Dictionary<string, object>>(
|
|
HttpMethod.Get, "public/universes", null, null, cancellationToken);
|
|
|
|
if (response != null &&
|
|
response.TryGetValue("public_universes", out var universesObj)) {
|
|
// Handle System.Text.Json
|
|
if (universesObj is System.Text.Json.JsonElement jsonElement) {
|
|
return System.Text.Json.JsonSerializer
|
|
.Deserialize<List<Dictionary<string, object>>>(
|
|
jsonElement.GetRawText());
|
|
} else if (universesObj is List<Dictionary<string, object>> list) {
|
|
return list;
|
|
}
|
|
}
|
|
|
|
return new List<Dictionary<string, object>>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// List user's universes (works with JWT from API key exchange)
|
|
/// </summary>
|
|
public async Task<List<Dictionary<string, object>>>
|
|
ListAsync(CancellationToken cancellationToken = default) {
|
|
// API returns {"universes": [...]}
|
|
var response = await _client.RequestAsync<Dictionary<string, object>>(
|
|
HttpMethod.Get, "universes", null, null, cancellationToken);
|
|
|
|
if (response != null &&
|
|
response.TryGetValue("universes", out var universesObj)) {
|
|
// Handle System.Text.Json
|
|
if (universesObj is System.Text.Json.JsonElement jsonElement) {
|
|
return System.Text.Json.JsonSerializer
|
|
.Deserialize<List<Dictionary<string, object>>>(
|
|
jsonElement.GetRawText());
|
|
} else if (universesObj is List<Dictionary<string, object>> list) {
|
|
return list;
|
|
}
|
|
}
|
|
|
|
return new List<Dictionary<string, object>>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copy a universe (requires user authentication)
|
|
/// </summary>
|
|
public Task<Dictionary<string, object>>
|
|
CopyAsync(string publicUniverseId, string customName = "",
|
|
CancellationToken cancellationToken = default) {
|
|
throw new NotSupportedException(
|
|
"[GumYum] Copy() requires user authentication. Use API keys with a " +
|
|
"known universe ID instead.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get universe details
|
|
/// </summary>
|
|
public async Task<Dictionary<string, object>>
|
|
GetUniverseAsync(string universeId,
|
|
CancellationToken cancellationToken = default) {
|
|
return await _client.RequestAsync<Dictionary<string, object>>(
|
|
HttpMethod.Get, $"universes/{universeId}", null, null,
|
|
cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manager for Chat-related operations
|
|
/// </summary>
|
|
public class ChatManager {
|
|
private readonly Client _client;
|
|
|
|
public ChatManager(Client client) { _client = client; }
|
|
|
|
/// <summary>
|
|
/// Generate dialogue (OpenAI-compatible chat.completions)
|
|
/// </summary>
|
|
public async Task<ChatCompletionResponse> CompletionsAsync(
|
|
List<ChatMessage> messages, Dictionary<string, object> npcParams,
|
|
double temperature = 0.8, int maxTokens = 2000, bool stream = false,
|
|
CancellationToken cancellationToken = default) {
|
|
var data = new Dictionary<string, object> { ["model"] = "gumyum-npc",
|
|
["temperature"] = temperature,
|
|
["messages"] = messages,
|
|
["stream"] = stream,
|
|
["npc_params"] = npcParams,
|
|
["max_tokens"] = maxTokens };
|
|
|
|
// Extract npc_id to top level if it exists in npc_params
|
|
if (npcParams.ContainsKey("npc_id")) {
|
|
data["npc_id"] = npcParams["npc_id"];
|
|
}
|
|
|
|
if (stream) {
|
|
throw new NotImplementedException(
|
|
"Streaming is handled via CompletionsStreamAsync");
|
|
} else {
|
|
var response = await _client.RequestAsync<Dictionary<string, object>>(
|
|
HttpMethod.Post, "chat/completions", data, null, cancellationToken);
|
|
|
|
var completionResponse = new ChatCompletionResponse(response);
|
|
|
|
if (completionResponse.Choices?.Count > 0) {
|
|
var firstChoice = completionResponse.Choices[0];
|
|
if (firstChoice?.Message == null) {
|
|
return completionResponse;
|
|
}
|
|
var content = firstChoice.Message.Content;
|
|
var context = completionResponse.NpcContext;
|
|
var moodTransition = completionResponse.MoodTransition;
|
|
|
|
_client.RaiseDialogueReceived(new DialogueReceivedEventArgs {
|
|
Response = content, Context = context, MoodTransition = moodTransition
|
|
});
|
|
}
|
|
|
|
return completionResponse;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate dialogue with streaming support
|
|
/// </summary>
|
|
public async Task CompletionsStreamAsync(
|
|
List<ChatMessage> messages, Dictionary<string, object> npcParams,
|
|
Action<string> onChunk, double temperature = 0.8, int maxTokens = 2000,
|
|
CancellationToken cancellationToken = default) {
|
|
var data = new Dictionary<string, object> { ["model"] = "gumyum-npc",
|
|
["temperature"] = temperature,
|
|
["messages"] = messages,
|
|
["stream"] = true,
|
|
["npc_params"] = npcParams,
|
|
["max_tokens"] = maxTokens };
|
|
|
|
// Extract npc_id to top level if it exists in npc_params
|
|
if (npcParams.ContainsKey("npc_id")) {
|
|
data["npc_id"] = npcParams["npc_id"];
|
|
}
|
|
|
|
await _client.RequestStreamAsync(HttpMethod.Post, "chat/completions", data,
|
|
null, onChunk, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate dialogue (legacy method name for backwards compatibility)
|
|
/// </summary>
|
|
public Task<ChatCompletionResponse>
|
|
ChatAsync(List<ChatMessage> messages, Dictionary<string, object> npcParams,
|
|
double temperature = 0.8, int maxTokens = 2000,
|
|
CancellationToken cancellationToken = default) {
|
|
return CompletionsAsync(messages, npcParams, temperature, maxTokens, false,
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate dialogue with streaming support (legacy method)
|
|
/// </summary>
|
|
public Task ChatStreamAsync(List<ChatMessage> messages,
|
|
Dictionary<string, object> npcParams,
|
|
Action<string> onChunk, double temperature = 0.8,
|
|
int maxTokens = 2000,
|
|
CancellationToken cancellationToken = default) {
|
|
return CompletionsStreamAsync(messages, npcParams, onChunk, temperature,
|
|
maxTokens, cancellationToken);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Chat message structure
|
|
/// </summary>
|
|
public class ChatMessage {
|
|
public string Role { get; set; }
|
|
public string Content { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Chat completion response
|
|
/// </summary>
|
|
public class ChatCompletionResponse {
|
|
public List<ChatChoice> Choices { get; set; }
|
|
public Dictionary<string, object> NpcContext { get; set; }
|
|
public Dictionary<string, object> MoodTransition { get; set; }
|
|
|
|
public ChatCompletionResponse(Dictionary<string, object> data) {
|
|
Choices = new List<ChatChoice>();
|
|
|
|
if (data?.ContainsKey("choices") == true) {
|
|
var choicesObj = data["choices"];
|
|
if (choicesObj is JsonElement choicesJson) {
|
|
// Handle JsonElement array
|
|
foreach (var choiceElement in choicesJson.EnumerateArray()) {
|
|
var choiceDict =
|
|
JsonSerializer.Deserialize<Dictionary<string, object>>(
|
|
choiceElement.GetRawText());
|
|
Choices.Add(new ChatChoice(choiceDict));
|
|
}
|
|
} else if (choicesObj is List<object> choicesList) {
|
|
Choices =
|
|
choicesList
|
|
?.Select(c => new ChatChoice(c as Dictionary<string, object>))
|
|
.ToList() ??
|
|
new List<ChatChoice>();
|
|
}
|
|
}
|
|
|
|
if (data?.ContainsKey("npc_context") == true) {
|
|
NpcContext = data["npc_context"] as Dictionary<string, object>;
|
|
}
|
|
|
|
if (data?.ContainsKey("mood_transition") == true) {
|
|
MoodTransition = data["mood_transition"] as Dictionary<string, object>;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Chat choice in completion response
|
|
/// </summary>
|
|
public class ChatChoice {
|
|
public ChatMessage Message { get; set; }
|
|
|
|
public ChatChoice(Dictionary<string, object> data) {
|
|
if (data?.ContainsKey("message") == true) {
|
|
var msgObj = data["message"];
|
|
if (msgObj is JsonElement msgJson) {
|
|
var msgDict = JsonSerializer.Deserialize<Dictionary<string, object>>(
|
|
msgJson.GetRawText());
|
|
Message = new ChatMessage {
|
|
Role = msgDict?.GetValueOrDefault("role")?.ToString() ?? "assistant",
|
|
Content = msgDict?.GetValueOrDefault("content")?.ToString() ?? ""
|
|
};
|
|
} else if (msgObj is Dictionary<string, object> msgData) {
|
|
Message = new ChatMessage {
|
|
Role = msgData.GetValueOrDefault("role")?.ToString() ?? "assistant",
|
|
Content = msgData.GetValueOrDefault("content")?.ToString() ?? ""
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |