Skip to content

Commit

Permalink
Merge branch 'release/2.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
KarmaKamikaze committed Nov 13, 2024
2 parents 0b8fab7 + c7c0137 commit 91492c0
Show file tree
Hide file tree
Showing 64 changed files with 5,227 additions and 1,001 deletions.
54 changes: 54 additions & 0 deletions ChatRPG/API/ChatRPGSummarizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using LangChain.Memory;
using LangChain.Providers;
using static LangChain.Chains.Chain;

namespace ChatRPG.API;

// ReSharper disable once InconsistentNaming
public static class ChatRPGSummarizer
{
public const string SummaryPrompt = @"
Progressively summarize the interaction between the player and the GM. Append to the summary so that new messages are most represented, while still remembering key details of the far past. The player describes their actions in response to the game world, and the GM narrates the outcome, revealing the next part of the adventure. Return a new summary based on each exchange taking into account the previous summary. Expand the summary so that it is long enough to capture the essence of the story from the beginning without forgetting key details. Do not remove important people, events, or locations from the summary.
EXAMPLE
Current summary:
The player enters the forest and cautiously looks around. The GM describes towering trees and a narrow path leading deeper into the woods. The player decides to follow the path, staying alert.
New lines of conversation:
Player: I move carefully down the path, keeping an eye out for any hidden dangers.
GM: As you continue, the air grows colder, and you hear rustling in the bushes ahead. Suddenly, a shadowy figure leaps out in front of you.
New summary:
The player enters the forest and follows a narrow path, staying alert. The GM introduces a shadowy figure that appears ahead after rustling is heard in the bushes.
END OF EXAMPLE
Current summary:
{summary}
New lines of conversation:
{new_lines}
New summary:";

public static async Task<string> SummarizeAsync(
this IChatModel chatModel,
IEnumerable<Message> newMessages,
string existingSummary,
MessageFormatter? formatter = null,
CancellationToken cancellationToken = default)
{
formatter ??= new MessageFormatter();
formatter.HumanPrefix = "Player";
formatter.AiPrefix = "GM";

var newLines = formatter.Format(newMessages);

var chain =
Set(existingSummary, outputKey: "summary")
| Set(newLines, outputKey: "new_lines")
| Template(SummaryPrompt)
| LLM(chatModel);

return await chain.RunAsync("text", cancellationToken: cancellationToken).ConfigureAwait(true) ?? string.Empty;
}
}
16 changes: 0 additions & 16 deletions ChatRPG/API/HttpClientFactory.cs

This file was deleted.

59 changes: 0 additions & 59 deletions ChatRPG/API/HttpMessageHandlerFactory.cs

This file was deleted.

7 changes: 0 additions & 7 deletions ChatRPG/API/IOpenAiLlmClient.cs

This file was deleted.

9 changes: 9 additions & 0 deletions ChatRPG/API/IReActLlmClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ChatRPG.Data.Models;

namespace ChatRPG.API;

public interface IReActLlmClient
{
Task<string> GetChatCompletionAsync(Campaign campaign, string actionPrompt, string input);
IAsyncEnumerable<string> GetStreamedChatCompletionAsync(Campaign campaign, string actionPrompt, string input);
}
80 changes: 80 additions & 0 deletions ChatRPG/API/LlmEventProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Text;
using System.Threading.Channels;
using LangChain.Providers;
using LangChain.Providers.OpenAI;

namespace ChatRPG.API;

public class LlmEventProcessor
{
private readonly object _lock = new object();
private readonly StringBuilder _buffer = new StringBuilder();
private bool _foundFinalAnswer = false;
private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();

public LlmEventProcessor(OpenAiChatModel model)
{
model.DeltaReceived += OnDeltaReceived;
model.ResponseReceived += OnResponseReceived;
}

public async IAsyncEnumerable<string> GetContentStreamAsync()
{
while (await _channel.Reader.WaitToReadAsync())
{
while (_channel.Reader.TryRead(out var chunk))
{
yield return chunk;
}
}
}

private void OnDeltaReceived(object? sender, ChatResponseDelta delta)
{
lock (_lock)
{
if (_foundFinalAnswer)
{
// Directly output content after "Final Answer: " has been detected
_channel.Writer.TryWrite(delta.Content);
}
else
{
// Accumulate the content in the buffer
_buffer.Append(delta.Content);

// Check if the buffer contains "Final Answer: "
var bufferString = _buffer.ToString();
int finalAnswerIndex = bufferString.IndexOf("Final Answer:", StringComparison.Ordinal);

if (finalAnswerIndex != -1)
{
// Output everything after "Final Answer: " has been detected
int startOutputIndex = finalAnswerIndex + "Final Answer:".Length;

// Switch to streaming mode
_foundFinalAnswer = true;

// Output any content after "Final Answer: "
_channel.Writer.TryWrite(bufferString[startOutputIndex..]);

// Clear the buffer since it's no longer needed
_buffer.Clear();
}
}
}
}

private void OnResponseReceived(object? sender, ChatResponse response)
{
lock (_lock)
{
_buffer.Clear(); // Clear buffer to avoid carrying over any previous data
if (!_foundFinalAnswer) return;

// Reset the state so that the process can start over
_foundFinalAnswer = false;
_channel.Writer.TryComplete();
}
}
}
49 changes: 4 additions & 45 deletions ChatRPG/API/OpenAiGptMessage.cs
Original file line number Diff line number Diff line change
@@ -1,73 +1,32 @@
using OpenAI_API.Chat;
using System.Text.Json;
using System.Text.RegularExpressions;
using ChatRPG.API.Response;
using ChatRPG.Data.Models;
using ChatRPG.Pages;
using Microsoft.IdentityModel.Tokens;

namespace ChatRPG.API;

public class OpenAiGptMessage
{
public OpenAiGptMessage(ChatMessageRole role, string content)
public OpenAiGptMessage(MessageRole role, string content)
{
Role = role;
Content = content;
NarrativePart = "";
UpdateNarrativePart();
if (!Content.IsNullOrEmpty() && NarrativePart.IsNullOrEmpty() && role.Equals(ChatMessageRole.Assistant))
{
NarrativePart = Content;
}
}

public OpenAiGptMessage(ChatMessageRole role, string content, UserPromptType userPromptType) : this(role, content)
public OpenAiGptMessage(MessageRole role, string content, UserPromptType userPromptType) : this(role, content)
{
UserPromptType = userPromptType;
}

public ChatMessageRole Role { get; }
public MessageRole Role { get; }
public string Content { get; private set; }
public string NarrativePart { get; private set; }
public readonly UserPromptType UserPromptType = UserPromptType.Do;
private static readonly Regex NarrativeRegex =
new(pattern: "^\\s*{\\s*\"narrative\":\\s*\"([^\"]*)", RegexOptions.IgnoreCase);

public LlmResponse? TryParseFromJson()
{
try
{
JsonSerializerOptions options = new()
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<LlmResponse>(Content, options);
}
catch (JsonException)
{
return new LlmResponse { Narrative = Content }; // Format was unexpected
}
}

public void AddChunk(string chunk)
{
Content += chunk.Replace("\\\"", "'");
UpdateNarrativePart();
}

private void UpdateNarrativePart()
{
Match match = NarrativeRegex.Match(Content);
if (match is { Success: true, Groups.Count: 2 })
{
NarrativePart = match.Groups[1].ToString();
}
}

public static OpenAiGptMessage FromMessage(Message message)
{
ChatMessageRole role = ChatMessageRole.FromString(message.Role.ToString().ToLower());
return new OpenAiGptMessage(role, message.Content);
return new OpenAiGptMessage(message.Role, message.Content);
}
}
52 changes: 0 additions & 52 deletions ChatRPG/API/OpenAiLlmClient.cs

This file was deleted.

Loading

0 comments on commit 91492c0

Please sign in to comment.