Skip to content

Commit 51a25e8

Browse files
Create AddEdgeTool description to finalize Scribe Agent
Co-authored-by: Sarmisuper <stharm20@student.aau.dk>
1 parent 86d0ae8 commit 51a25e8

File tree

10 files changed

+118
-44
lines changed

10 files changed

+118
-44
lines changed

ChatRPG/API/Tools/AddEdgeTool.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ private static bool TryAddEdge(NarrativeGraph graph, AddEdgeInput newEdge, out s
7575
errorMessages.Add($"An edge already exists between {newEdge.SourceNodeName} and {newEdge.TargetNodeName}.");
7676
}
7777

78-
if (errorMessages.Count != 0)
78+
if (!ToolUtilities.NodesValidForNewEdge(sourceNode, targetNode, newEdge, out errorMessages))
7979
{
8080
errorMessage =
81-
$"Invalid input provided for the node. " +
81+
$"Invalid input provided for the edge. " +
8282
$"Please correct the following errors:\n{string.Join("\n", errorMessages)}";
8383
return false;
8484
}

ChatRPG/API/Tools/AddNodeTool.cs

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,26 +71,9 @@ private static bool TryAddNode(NarrativeGraph graph, AddNodeInput newNode, out s
7171
sourceNode ??= edge.SourceNodeName == newNode.Name ? node : null;
7272
targetNode ??= edge.TargetNodeName == newNode.Name ? node : null;
7373

74-
if (targetNode is null)
74+
if (ToolUtilities.NodesValidForNewEdge(sourceNode, targetNode, edge, out errorMessages))
7575
{
76-
errorMessages.Add($"Target node with name {edge.TargetNodeName} not found.");
77-
}
78-
79-
if (sourceNode is null)
80-
{
81-
errorMessages.Add($"Source node with name {edge.SourceNodeName} not found.");
82-
}
83-
else if (sourceNode == targetNode)
84-
{
85-
errorMessages.Add($"Node {sourceNode.Name} cannot have an edge to itself.");
86-
}
87-
else if (targetNode is not null && sourceNode.Edges.Any(e => e.TargetNode == targetNode))
88-
{
89-
errorMessages.Add($"An edge already exists between {edge.SourceNodeName} and {edge.TargetNodeName}.");
90-
}
91-
else if (targetNode is not null)
92-
{
93-
edgesToAdd.Add(new NarrativeEdge(edge.Conditions!, sourceNode, targetNode));
76+
edgesToAdd.Add(new NarrativeEdge(edge.Conditions!, sourceNode!, targetNode!));
9477
}
9578
}
9679

ChatRPG/API/Tools/ToolUtilities.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ public static string ConstructSummary(Campaign campaign, bool shouldIncludePrevi
9999
if (shouldIncludePreviousMessages)
100100
{
101101
var content = campaign.Messages.TakeLast(IncludedPreviousMessages).Select(m => m.Content);
102-
result += "\n\nUse these previous messages as context. They only serve to give a hint of the current scenario:";
102+
result +=
103+
"\n\nUse these previous messages as context. They only serve to give a hint of the current scenario:";
103104
foreach (var message in content)
104105
{
105106
result += $"\n {message}";
@@ -109,5 +110,28 @@ public static string ConstructSummary(Campaign campaign, bool shouldIncludePrevi
109110
return result;
110111
}
111112

113+
public static bool NodesValidForNewEdge(NarrativeNode? sourceNode, NarrativeNode? targetNode, AddEdgeInput newEdge,
114+
out List<string> errorMessages)
115+
{
116+
errorMessages = [];
117+
if (targetNode is null)
118+
{
119+
errorMessages.Add($"Target node with name {newEdge.TargetNodeName} not found.");
120+
}
112121

122+
if (sourceNode is null)
123+
{
124+
errorMessages.Add($"Source node with name {newEdge.SourceNodeName} not found.");
125+
}
126+
else if (sourceNode == targetNode)
127+
{
128+
errorMessages.Add($"Node {sourceNode.Name} cannot have an edge to itself.");
129+
}
130+
else if (targetNode is not null && sourceNode.Edges.Any(e => e.TargetNode == targetNode))
131+
{
132+
errorMessages.Add($"An edge already exists between {newEdge.SourceNodeName} and {newEdge.TargetNodeName}.");
133+
}
134+
135+
return errorMessages.Count == 0;
136+
}
113137
}

ChatRPG/Data/ApplicationDbContext.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
2121
{
2222
base.OnModelCreating(modelBuilder);
2323
modelBuilder.Entity<NarrativeEdge>()
24+
.Ignore(edge => edge.SourceNodeName)
25+
.Ignore(edge => edge.TargetNodeName)
2426
.HasOne(edge => edge.TargetNode)
2527
.WithMany()
2628
.HasForeignKey(edge => edge.TargetNodeId)
2729
.OnDelete(DeleteBehavior.Restrict);
2830

2931
modelBuilder.Entity<NarrativeEdge>()
32+
.Ignore(edge => edge.SourceNodeName)
33+
.Ignore(edge => edge.TargetNodeName)
3034
.HasOne(edge => edge.SourceNode)
3135
.WithMany(node => node.Edges)
3236
.HasForeignKey(edge => edge.SourceNodeId)

ChatRPG/Data/Migrations/ApplicationDbContextModelSnapshot.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
6161

6262
b.HasIndex("UserId");
6363

64-
b.ToTable("Campaigns");
64+
b.ToTable("Campaigns", (string)null);
6565
});
6666

6767
modelBuilder.Entity("ChatRPG.Data.Models.Character", b =>
@@ -104,7 +104,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
104104

105105
b.HasIndex("EnvironmentId");
106106

107-
b.ToTable("Characters");
107+
b.ToTable("Characters", (string)null);
108108
});
109109

110110
modelBuilder.Entity("ChatRPG.Data.Models.Environment", b =>
@@ -130,7 +130,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
130130

131131
b.HasIndex("CampaignId");
132132

133-
b.ToTable("Environments");
133+
b.ToTable("Environments", (string)null);
134134
});
135135

136136
modelBuilder.Entity("ChatRPG.Data.Models.Message", b =>
@@ -158,7 +158,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
158158

159159
b.HasIndex("CampaignId");
160160

161-
b.ToTable("Message");
161+
b.ToTable("Message", (string)null);
162162
});
163163

164164
modelBuilder.Entity("ChatRPG.Data.Models.NarrativeEdge", b =>
@@ -188,7 +188,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
188188

189189
b.HasIndex("TargetNodeId");
190190

191-
b.ToTable("NarrativeEdges");
191+
b.ToTable("NarrativeEdges", (string)null);
192192
});
193193

194194
modelBuilder.Entity("ChatRPG.Data.Models.NarrativeGraph", b =>
@@ -201,7 +201,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
201201

202202
b.HasKey("Id");
203203

204-
b.ToTable("NarrativeGraphs");
204+
b.ToTable("NarrativeGraphs", (string)null);
205205
});
206206

207207
modelBuilder.Entity("ChatRPG.Data.Models.NarrativeNode", b =>
@@ -230,7 +230,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
230230

231231
b.HasIndex("GraphId");
232232

233-
b.ToTable("NarrativeNodes");
233+
b.ToTable("NarrativeNodes", (string)null);
234234
});
235235

236236
modelBuilder.Entity("ChatRPG.Data.Models.StartScenario", b =>
@@ -251,7 +251,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
251251

252252
b.HasKey("Id");
253253

254-
b.ToTable("StartScenarios");
254+
b.ToTable("StartScenarios", (string)null);
255255
});
256256

257257
modelBuilder.Entity("ChatRPG.Data.Models.User", b =>

ChatRPG/Data/Models/NarrativeEdge.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,25 @@ public NarrativeEdge(List<string> conditions, NarrativeNode sourceNode, Narrativ
2121

2222
[JsonIgnore]
2323
public int Id { get; private set; }
24+
2425
public ICollection<string> Conditions { get; private set; }
26+
2527
[JsonIgnore]
2628
public int SourceNodeId { get; private set; }
29+
30+
public string SourceNodeName => SourceNode.Name;
31+
32+
[JsonIgnore]
2733
public NarrativeNode SourceNode { get; private set; }
34+
2835
[JsonIgnore]
2936
public int TargetNodeId { get; private set; }
37+
38+
public string TargetNodeName => TargetNode.Name;
39+
40+
[JsonIgnore]
3041
public NarrativeNode TargetNode { get; private set; }
42+
3143
public Status EdgeStatus { get; private set; } = Status.Unvisited;
3244

3345
public enum Status
@@ -44,7 +56,7 @@ public string Serialize()
4456
public override string ToString()
4557
{
4658
var sb = new StringBuilder();
47-
sb.Append($"{EdgeStatus} Edge from {SourceNode.Id} to {TargetNode.Id} with conditions: ");
59+
sb.Append($"{EdgeStatus} Edge from {SourceNodeName} to {TargetNodeName} with conditions: ");
4860
sb.Append(string.Join(", ", Conditions));
4961

5062
return sb.ToString();

ChatRPG/Data/Models/NarrativeGraph.cs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ public class NarrativeGraph
1010

1111
[JsonIgnore]
1212
public int Id { get; private set; }
13+
1314
[JsonIgnore]
1415
public ICollection<Campaign> Campaigns { get; private set; } = new List<Campaign>();
16+
1517
public HashSet<NarrativeNode> Nodes { get; private set; } = [];
1618

1719
public void AddNode(NarrativeNode node)
@@ -52,20 +54,27 @@ public void PrintGraph()
5254
public override string ToString()
5355
{
5456
var startNode = GetStartNode();
57+
if (startNode == null)
58+
{
59+
return "Warning: No start node found. Create a node with no incoming edges.";
60+
}
61+
5562
var sb = new StringBuilder();
56-
sb.Append(startNode != null
57-
? $"Start Node: {startNode}"
58-
: "Warning: No start node found. Create a node with no incoming edges.");
63+
sb.AppendLine($"Start Node: {startNode}\n");
64+
var visited = new HashSet<NarrativeNode>();
5965

60-
foreach (var node in Nodes)
66+
Dfs(startNode);
67+
return sb.ToString();
68+
69+
void Dfs(NarrativeNode node)
6170
{
62-
sb.Append(node);
71+
if (!visited.Add(node)) return; // Skip if already visited
72+
sb.AppendLine(node.ToString());
6373
foreach (var edge in node.Edges)
6474
{
65-
sb.Append($" {edge}");
75+
sb.AppendLine($" {edge}");
76+
Dfs(edge.TargetNode); // Recursively visit adjacent nodes
6677
}
6778
}
68-
69-
return sb.ToString();
7079
}
7180
}

ChatRPG/Pages/UserCampaignOverview.razor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ private async Task CreateAndStartCampaign()
103103
// UploadedFile should not be able to be null since the button is disabled if it is
104104
await ScenarioDocumentService!.StoreScenarioEmbedding(campaign.Id, UploadedFile!);
105105

106-
var graph = await ReActScribeAgent!.ScribeNarrativeGraph(UploadedFile!);
106+
campaign.NarrativeGraph = await ReActScribeAgent!.ScribeNarrativeGraph(UploadedFile!);
107107

108-
campaign.StartScenario = await ScenarioDocumentService.GenerateStartingScenario(campaign.Id);
108+
campaign.StartScenario = await ScenarioDocumentService.GenerateStartingScenario(campaign);
109109
await PersistenceService!.SaveAsync(campaign);
110110
}
111111

ChatRPG/Services/ReActScribeAgent.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,48 @@ private static List<AgentTool> CreateTools(NarrativeGraph graph)
114114
"that traversal logic remains consistent with the scenario documents.");
115115
tools.Add(addNodeTool);
116116

117-
var addEdgeTool = new AddEdgeTool(graph, "addedgetool", "Add an edge to the narrative graph");
117+
var addEdgeTool = new AddEdgeTool(graph, "addedgetool",
118+
"This tool must be used to add a new edge (connection) between two existing nodes in the " +
119+
"narrative graph. This tool should be used when you determine that a new pathway should be " +
120+
"established between two already-defined story points.\n\nEach edge represents a story-driven " +
121+
"connection between two nodes, allowing the player to progress based on specific conditions. " +
122+
"These conditions act as prerequisites that must be met before the player is allowed to traverse " +
123+
"the edge. An edge must include:\n " +
124+
"- A source node name, which is the starting point of the edge.\n " +
125+
"- A target node name, which is the destination of the edge.\n " +
126+
"- A list of conditions, which describe what the player must accomplish to traverse the edge.\n " +
127+
"Conditions should be framed as easy-to-answer questions, verifying if the player has completed " +
128+
"specific story requirements. These could be based on prior encounters, collected items, or " +
129+
"completed quests, such as:\n " +
130+
"- \"Has the player spoken to the village elder?\"\n " +
131+
"- \"Has the player recovered the stolen artifact from the crypt?\"\n " +
132+
"- \"Has the player defeated the guardian of the temple.\"\n " +
133+
"After calling this tool, you will receive an updated string representation of the graph, showing " +
134+
"the newly added edge and its connection between nodes. This allows you to verify relationships " +
135+
"and ensure logical story progression.\n " +
136+
"Usage Format:\n " +
137+
"The tool requires valid JSON input structured as follows:\n " +
138+
"{ \"conditions\": [ \"condition 1 for traversing the edge\", \"condition 2 for traversing the edge\" ], " +
139+
"\"sourcenodename\": \"the name of the source node which already exists in the graph\", " +
140+
"\"targetnodename\": \"the name of the target node which already exists in the graph\" } " +
141+
"The conditions list may be empty if the edge does not require prerequisites for traversal.\n " +
142+
"Example 1: Unlocking the Crypt\n " +
143+
"In this scenario, the player must obtain the Rusted Key before they can enter the Ancient Crypt.\n " +
144+
"Input to AddEdgeTool:\n " +
145+
"{ \"sourcenodename\": \"Old Graveyard\", \"targetnodename\": \"Ancient Crypt\", \"conditions\": " +
146+
"[ \"Has the player obtained the Rusted Key?\" ] } Outcome:\n " +
147+
"- The Old Graveyard is now connected to the Ancient Crypt.\n " +
148+
"- The player cannot enter the crypt until they have obtained the Rusted Key.\n " +
149+
"Example 2: Gaining Access to the Royal Chamber\n " +
150+
"To enter the Royal Chamber, the player must have:\n " +
151+
"1. Met Sir Ivan, the Wizard, who provides the key to the chamber.\n " +
152+
"2. Defeated the Elite Guards stationed outside.\n " +
153+
"Input to AddEdgeTool:\n " +
154+
"{ \"sourcenodename\": \"Castle Courtyard\", \"targetnodename\": \"Royal Chamber\", \"conditions\": " +
155+
"[ \"Has the player met Sir Ivan, the Wizard?\", \"Has the player defeated the Elite Guards?\" ] } " +
156+
"Outcome:\n " +
157+
"- The Castle Courtyard is now connected to the Royal Chamber.\n " +
158+
"- The player cannot enter until both conditions are fulfilled.");
118159
tools.Add(addEdgeTool);
119160

120161
return tools;

ChatRPG/Services/ScenarioDocumentService.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text;
2+
using ChatRPG.Data.Models;
23
using LangChain.Databases.Postgres;
34
using LangChain.Providers.OpenAI;
45
using LangChain.Providers.OpenAI.Predefined;
@@ -47,7 +48,7 @@ public async Task StoreScenarioEmbedding(int campaignId, byte[] scenarioDocument
4748
chunkOverlap: 200)); // To pick the chunk overlap you need to estimate the size of the smallest piece of information. It may happen that one chunk ends with `Ron's hair` and the other one starts with `is red`.In this case, an embedding would miss important context, and not be generated properly. With overlap the end of the first chunk will appear in the beginning of the other, eliminating the problem.
4849
}
4950

50-
public async Task<string> GenerateStartingScenario(int campaignId)
51+
public async Task<string> GenerateStartingScenario(Campaign campaign)
5152
{
5253
var provider = new OpenAiProvider(_openAiKey);
5354
var embeddingModel = new TextEmbeddingV3SmallModel(provider);
@@ -58,7 +59,7 @@ public async Task<string> GenerateStartingScenario(int campaignId)
5859

5960
var vectorDatabase =
6061
new PostgresVectorDatabase(_connectionString);
61-
var vectorCollection = await vectorDatabase.GetCollectionAsync("~collection-" + campaignId);
62+
var vectorCollection = await vectorDatabase.GetCollectionAsync("~collection-" + campaign.Id);
6263

6364
var prompt = new StringBuilder();
6465
prompt.Append(_startingScenarioPrompt);

0 commit comments

Comments
 (0)