Skip to content

Commit

Permalink
RAG Starting Scenario
Browse files Browse the repository at this point in the history
  • Loading branch information
KarmaKamikaze committed Mar 6, 2025
1 parent 33478ca commit aee812a
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 46 deletions.
3 changes: 3 additions & 0 deletions ChatRPG/API/ReActLlmClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ private List<AgentTool> CreateTools(Campaign campaign)
"most once.");
tools.Add(battleTool);

var searchScenarioTool = new SearchScenarioTool(_configuration, campaign, "TODO: Narrator-specific instruction", "searchscenariotool", "");
tools.Add(searchScenarioTool);

return tools;
}
}
47 changes: 47 additions & 0 deletions ChatRPG/API/Tools/SearchScenarioTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Text;
using ChatRPG.Data.Models;
using LangChain.Chains.StackableChains.Agents.Tools;
using LangChain.Databases.Postgres;
using LangChain.Providers;
using LangChain.Providers.OpenAI;
using LangChain.Providers.OpenAI.Predefined;
using static LangChain.Chains.Chain;

namespace ChatRPG.API.Tools;

public class SearchScenarioTool(
IConfiguration configuration,
Campaign campaign,
string instruction,
string name,
string? description = null) : AgentTool(name, description)
{
public override async Task<string> ToolTask(string input, CancellationToken token = new CancellationToken())
{
var provider = new OpenAiProvider(configuration.GetSection("ApiKeys").GetValue<string>("OpenAI")!);
var embeddingModel = new TextEmbeddingV3SmallModel(provider);
var llm = new Gpt4OmniModel(provider)
{
Settings = new OpenAiChatSettings() { UseStreaming = false, Temperature = 0.1 }
};

var vectorDatabase =
new PostgresVectorDatabase(configuration.GetSection("ConnectionStrings")
.GetValue<string>("DefaultConnection")!, configuration.GetValue<string>("VectorDatabaseTable")!);
var vectorCollection = await vectorDatabase.GetCollectionAsync("collection-" + campaign.Id, token);

var prompt = new StringBuilder();
prompt.Append(configuration.GetSection("SystemPrompts").GetValue<string>("SearchScenario")!
.Replace("{instruction}", instruction));

var chain = Set(input, "input")
| RetrieveSimilarDocuments(vectorCollection, embeddingModel, amount: 20)
| CombineDocuments(outputKey: "context")
| Template(prompt.ToString())
| LLM(llm.UseConsoleForDebug()); // TODO: Remove debug mode

var response = await chain.RunAsync("text", cancellationToken: token);

return response ?? "System: I'm sorry, I couldn't find any relevant scenarios.";
}
}
33 changes: 18 additions & 15 deletions ChatRPG/ChatRPG.csproj
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-ChatRPG-5312bf6c-6a60-492a-bb3a-fb97f9b3103f</UserSecretsId>
<LangVersion>13</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Blazored.Modal" Version="7.3.1" />
<PackageReference Include="LangChain" Version="0.15.2" />
<PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
<PackageReference Include="LangChain" Version="0.17.0" />
<PackageReference Include="LangChain.Databases.Postgres" Version="0.17.0" />
<PackageReference Include="LangChain.DocumentLoaders.Pdf" Version="0.17.0" />
<PackageReference Include="MailKit" Version="4.10.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.6" />
<PackageReference Include="MimeKit" Version="4.8.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.8" />
<PackageReference Include="Radzen.Blazor" Version="5.2.12" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.2" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" />
<PackageReference Include="MimeKit" Version="4.10.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" />
<PackageReference Include="Radzen.Blazor" Version="6.1.2" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion ChatRPG/Data/Models/Campaign.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public Campaign(User user, string title, string startScenario) : this(user, titl
}

public int Id { get; private set; }
public string? StartScenario { get; private set; }
public string? StartScenario { get; set; }
public User User { get; private set; } = null!;
public string Title { get; private set; } = null!;
public DateTime StartedOn { get; private set; }
Expand Down
76 changes: 74 additions & 2 deletions ChatRPG/Pages/UserCampaignOverview.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,20 @@
<div class="dashboard-title-container">
<h1 class="title dashboard-title text-center">Dashboard</h1>
</div>
<div class="row gap-3">
<!-- Campaign type selection -->
<div class="text-center my-3">
<RadzenSelectBar Size="ButtonSize.Small" @bind-Value="IsOpenWorld" TValue="bool" Change="OnCampaignTypeChange" class="rz-shadow-6 prompt-selector">
<Items>
<RadzenSelectBarItem Text="Open World" Value="true" class="rz-ripple" id="open-world-mode"/>
<RadzenSelectBarItem Text="Predefined" Value="false" class="rz-ripple" id="predefined-mode"/>
</Items>
</RadzenSelectBar>
</div>

<!-- Campaign options -->
@if (IsOpenWorld)
{
<div class="row gap-3">
<div class="col rounded custom-gray-container">
<div class="m-3">
<h3 class="text-center mb-4">Your Campaigns</h3>
Expand Down Expand Up @@ -54,7 +67,7 @@
</div>
<div class="col-5 rounded custom-gray-container">
<div class="m-3">
<h3 class="text-center mb-4">Create a Custom Campaign</h3>
<h3 class="text-center mb-4">Create an Open World Campaign</h3>
<div class="form">
<div class="mb-3">
<label for="inputCampaignTitle" class="form-label">Campaign Title<span
Expand Down Expand Up @@ -101,5 +114,64 @@
</div>
</div>
</div>
}
else
{
<div class="row gap-3">
<div class="col-5 rounded custom-gray-container mx-auto">
<div class="m-3">
<h3 class="text-center mb-4">Predefined Campaign Setup</h3>
<div class="form">
<div class="mb-3">
<label for="inputCampaignTitle" class="form-label">Campaign Title<span
class="required-keyword">(Required)</span></label>
<input type="text" @bind="CampaignTitle" @oninput="UpdateCampaignTitleOnKeyPress"
class="form-control" id="inputCampaignTitle" data-toggle="tooltip"
data-placement="top" title="Campaign title is required.">
@if (string.IsNullOrWhiteSpace(CampaignTitle) && TestFields)
{
<div class="alert-sm alert-danger alert-inside-input mt-1 p-2 rounded" role="alert"
id="campaign-title-alert">
Campaign title is required.
</div>
}
</div>
<div class="mb-3">
<label for="inputCharacterName" class="form-label">Character Name<span
class="required-keyword">(Required)</span></label>
<input type="text" @bind="CharacterName" @oninput="UpdateCharacterNameOnKeyPress"
class="form-control" id="inputCharacterName" data-toggle="tooltip"
data-placement="top" title="Character name is required."/>
@if (string.IsNullOrWhiteSpace(CharacterName) && TestFields)
{
<div class="alert-sm alert-danger alert-inside-input mt-1 p-2 rounded" role="alert"
id="character-name-alert">
Character name is required.
</div>
}
</div>
<div class="mb-3">
<label for="inputCharacterDescription" class="form-label">Character Description</label>
<textarea class="form-control scrollbar" style="resize: none;" @bind="CharacterDescription"
id="inputCharacterDescription" rows="2"></textarea>
</div>
<div class="mb-3">
<label for="inputScenarioPDF" class="form-label">Upload Scenario PDF</label>
<InputFile class="form-control" OnChange="HandleScenarioFileUpload" />
@if (!string.IsNullOrEmpty(FileUploadError))
{
<div class="text-danger mt-2">@FileUploadError</div>
}
</div>
<button class="btn btn-primary" style="width: 100%"
disabled="@(!string.IsNullOrEmpty(FileUploadError) || UploadedFile == null || IsProcessingPdfFile)"
@onclick="CreateAndStartCampaign" id="create-campaign-button">
Create Campaign
</button>
</div>
</div>
</div>
</div>
}
}
</div>
60 changes: 57 additions & 3 deletions ChatRPG/Pages/UserCampaignOverview.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
using Blazored.Modal.Services;
using ChatRPG.Data.Models;
using ChatRPG.Services;
using LangChain.Databases.Postgres;
using LangChain.Providers.OpenAI;
using LangChain.Providers.OpenAI.Predefined;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Environment = ChatRPG.Data.Models.Environment;
Expand All @@ -20,9 +24,13 @@ public partial class UserCampaignOverview : ComponentBase
private List<StartScenario> StartScenarios { get; set; } = [];
private bool TestFields { get; set; }
private int TextAreaRows { get; set; } = 6;
private bool IsOpenWorld { get; set; } = true;
private bool IsProcessingPdfFile { get; set; } = false;
private byte[]? UploadedFile { get; set; }
private string FileUploadError { get; set; } = string.Empty;

[Required][BindProperty] private string CampaignTitle { get; set; } = "";
[Required][BindProperty] private string CharacterName { get; set; } = "";
[Required] [BindProperty] private string CampaignTitle { get; set; } = "";
[Required] [BindProperty] private string CharacterName { get; set; } = "";
[BindProperty] private string CharacterDescription { get; set; } = "";
[BindProperty] private string StartScenario { get; set; } = null!;

Expand All @@ -31,6 +39,7 @@ public partial class UserCampaignOverview : ComponentBase
[Inject] private IPersistenceService? PersistenceService { get; set; }
[Inject] private ICampaignMediatorService? CampaignMediatorService { get; set; }
[Inject] private NavigationManager? NavMan { get; set; }
[Inject] private ScenarioDocumentService? ScenarioDocumentService { get; set; }

[CascadingParameter] public IModalService? ConfirmDeleteModal { get; set; }

Expand Down Expand Up @@ -64,7 +73,18 @@ private async Task CreateAndStartCampaign()
true);
campaign.Environments.Add(environment);
campaign.Characters.Add(player);
await PersistenceService!.SaveAsync(campaign);
await PersistenceService!.SaveAsync(campaign); // Save the campaign ID to the database

if (!IsOpenWorld)
{
// Upload campaign documents to vector database
// UploadedFile should not be able to be null since the button is disabled if it is
await ScenarioDocumentService!.StoreScenarioEmbedding(campaign.Id, UploadedFile!);

campaign.StartScenario = await ScenarioDocumentService.GenerateStartingScenario(campaign.Id);
await PersistenceService!.SaveAsync(campaign);
}

LaunchCampaign(campaign.Id);
}

Expand Down Expand Up @@ -149,4 +169,38 @@ private void AdjustAlerts()

StateHasChanged();
}

private void OnCampaignTypeChange(bool value)
{
IsOpenWorld = value;
IsProcessingPdfFile = false;
FileUploadError = string.Empty;
UploadedFile = null;
}

private async Task HandleScenarioFileUpload(InputFileChangeEventArgs e)
{
var file = e.File;
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
{
FileUploadError = "Only PDF files are allowed.";
UploadedFile = null;
}
else
{
IsProcessingPdfFile = true;
FileUploadError = string.Empty;

try
{
using var memoryStream = new MemoryStream();
await file.OpenReadStream(maxAllowedSize: long.MaxValue).CopyToAsync(memoryStream);
UploadedFile = memoryStream.ToArray();
}
finally
{
IsProcessingPdfFile = false;
}
}
}
}
44 changes: 23 additions & 21 deletions ChatRPG/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
.AddTransient<GameInputHandler>()
.AddTransient<GameStateManager>()
.AddSingleton<ICampaignMediatorService, CampaignMediatorService>()
.AddScoped<JsInteropService>();
.AddScoped<JsInteropService>()
.AddScoped<ScenarioDocumentService>();

builder.Services.Configure<IdentityOptions>(options =>
{
Expand All @@ -44,19 +45,22 @@

WebApplication app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
app.UseMigrationsEndPoint();

using IServiceScope scope = app.Services.CreateScope();
ILogger<Program> logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Initializing database with test user");
try
{
app.UseMigrationsEndPoint();
ApplicationDbContext dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

using IServiceScope scope = app.Services.CreateScope();
ILogger<Program> logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Initializing database with test user");
try
{
ApplicationDbContext dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
dbContext.Database.Migrate();
// Ensure pgvector extension is installed.
dbContext.Database.ExecuteSqlRaw("CREATE EXTENSION IF NOT EXISTS vector;");

dbContext.Database.Migrate();

if (app.Environment.IsDevelopment())
{
UserManager<User> userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
const string username = "test";
User? user = await userManager.FindByNameAsync(username);
Expand All @@ -70,21 +74,19 @@
};
await userManager.CreateAsync(user, password: username);
}

logger.LogInformation("Database was successfully initialized");
}
catch (Exception e)
{
logger.LogError(e, "An error occurred while initializing database");
}

logger.LogInformation("Database was successfully initialized");
}
else
catch (Exception e)
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
logger.LogError(e, "An error occurred while initializing database");
}

app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();

app.UseHttpsRedirection();

if (app.Environment.IsDevelopment())
Expand Down
Loading

0 comments on commit aee812a

Please sign in to comment.