feat: enhance parser feedback mechanism for improved user guidance

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-23 22:55:16 -04:00
parent c2d12ffcc9
commit 2f6af46c79
5 changed files with 174 additions and 16 deletions

View File

@@ -17,6 +17,7 @@ export interface AppStateSnapshot {
export interface TurnResult {
narration: string;
parser: "fallback";
parser_feedback?: string;
actions: Action[];
accepted: Action[];
rejected: { action: Action; reason: string }[];
@@ -152,10 +153,15 @@ function narrateResult(
worldState: WorldState,
accepted: Action[],
rejected: { action: Action; reason: string }[],
latentReason?: string
latentReason?: string,
parserFeedback?: string
): string {
const lines: string[] = [];
if (parserFeedback) {
lines.push(parserFeedback);
}
if (latentReason) {
lines.push(latentReason);
}
@@ -212,7 +218,7 @@ export function createCharacterGardenApp(dbPath: string): CharacterGardenApp {
function processTurn(input: string): TurnResult {
const turnNumber = db.listTurns().length + 1;
const { actions, parser } = extractActionsFromProse(input);
const { actions, parser, parser_feedback: parserFeedback } = extractActionsFromProse(input);
let activeWorldState = worldState;
let latentResolution: TurnResult["latent_resolution"];
@@ -260,7 +266,13 @@ export function createCharacterGardenApp(dbPath: string): CharacterGardenApp {
const validation = validate(normalizedActions, activeWorldState);
const nextWorldState = applyChanges(activeWorldState, validation.state_changes);
const narration = narrateResult(nextWorldState, validation.accepted, validation.rejected, latentResolution?.reason);
const narration = narrateResult(
nextWorldState,
validation.accepted,
validation.rejected,
latentResolution?.reason,
parserFeedback
);
const turnRecord: Turn = {
id: randomUUID(),
@@ -300,6 +312,7 @@ export function createCharacterGardenApp(dbPath: string): CharacterGardenApp {
return {
narration,
parser,
parser_feedback: parserFeedback,
actions: normalizedActions,
accepted: validation.accepted,
rejected: validation.rejected,

View File

@@ -1,10 +1,52 @@
import { Action } from "./types";
import { Action, ALLOWED_VERBS, Entity } from "./types";
export interface ExtractedActions {
actions: Action[];
parser: "fallback";
parser_feedback?: string;
}
export interface ActionExtractionPrompt {
system: string;
user: string;
}
function toEntityLine(entity: Entity): string {
const location = typeof entity.attributes["location"] === "string"
? ` location=${entity.attributes["location"]}`
: "";
return `- ${entity.id} [${entity.type}] "${entity.name}"${location}`;
}
export function buildActionExtractionPrompt(input: string, entities: Entity[], actorId = "player"): ActionExtractionPrompt {
const entityDigest = entities
.slice()
.sort((left, right) => left.id.localeCompare(right.id))
.map(toEntityLine)
.join("\n");
const system = [
"You convert player prose into canonical game actions.",
"Only produce actions that are valid in the current world snapshot.",
`Allowed verbs: ${ALLOWED_VERBS.join(", ")}`,
"Use exact entity ids from the world snapshot for actor and target.",
"If intent is unclear or target is missing, return no actions and a parser_feedback string suggesting rephrasing.",
"Return strict JSON only: {\"actions\": Action[], \"parser_feedback\"?: string}",
].join("\n");
const user = [
`Actor id: ${actorId}`,
"World snapshot entities:",
entityDigest || "- (none)",
`Player input: ${input}`,
].join("\n");
return { system, user };
}
const REPHRASE_EXAMPLES = "Try rephrasing like: 'look around', 'go to the shed', 'open the gate', or 'pull out my phone'.";
const ROOM_ALIASES: Record<string, string> = {
garden: "garden",
shed: "shed",
@@ -54,8 +96,9 @@ export function extractActionsFromProse(input: string, actorId = "player"): Extr
if (!text) {
return {
actions: [{ actor: actorId, verb: "inspect", target: actorId }],
actions: [],
parser: "fallback",
parser_feedback: `I couldn't parse an empty turn. ${REPHRASE_EXAMPLES}`,
};
}
@@ -67,6 +110,14 @@ export function extractActionsFromProse(input: string, actorId = "player"): Extr
};
}
if (/(go|move|walk|head|travel)/.test(text) && !room) {
return {
actions: [],
parser: "fallback",
parser_feedback: `I understood movement, but not the destination. Try 'go to the shed' or 'go to the garden'.`,
};
}
if (/(open)/.test(text)) {
return {
actions: [{ actor: actorId, verb: "open", target: resolveTarget(text) ?? "gate" }],
@@ -82,15 +133,33 @@ export function extractActionsFromProse(input: string, actorId = "player"): Extr
}
if (/(take|pick up|grab)/.test(text)) {
const target = resolveTarget(text);
if (!target) {
return {
actions: [],
parser: "fallback",
parser_feedback: `I understood 'take' but not what item you meant. Try 'take the bench' or 'pull out my phone'.`,
};
}
return {
actions: [{ actor: actorId, verb: "take", target: resolveTarget(text) ?? undefined }],
actions: [{ actor: actorId, verb: "take", target }],
parser: "fallback",
};
}
if (/(drop|put down|set down)/.test(text)) {
const target = resolveTarget(text);
if (!target) {
return {
actions: [],
parser: "fallback",
parser_feedback: `I understood 'drop' but not which item. Try 'drop phone' or 'drop keys'.`,
};
}
return {
actions: [{ actor: actorId, verb: "drop", target: resolveTarget(text) ?? undefined }],
actions: [{ actor: actorId, verb: "drop", target }],
parser: "fallback",
};
}
@@ -103,8 +172,17 @@ export function extractActionsFromProse(input: string, actorId = "player"): Extr
}
if (/(use|press|activate)/.test(text)) {
const target = resolveTarget(text);
if (!target) {
return {
actions: [],
parser: "fallback",
parser_feedback: `I understood 'use' but not the target. Try 'use gate' or 'use phone'.`,
};
}
return {
actions: [{ actor: actorId, verb: "use", target: resolveTarget(text) ?? undefined }],
actions: [{ actor: actorId, verb: "use", target }],
parser: "fallback",
};
}
@@ -125,7 +203,8 @@ export function extractActionsFromProse(input: string, actorId = "player"): Extr
}
return {
actions: [{ actor: actorId, verb: "inspect", target: actorId, params: { raw_input: input } }],
actions: [],
parser: "fallback",
parser_feedback: `I couldn't map that request to a game action. ${REPHRASE_EXAMPLES}`,
};
}

View File

@@ -47,6 +47,7 @@ type Snapshot = {
type TurnResult = {
narration: string;
parser: string;
parser_feedback?: string;
actions: Array<Record<string, unknown>>;
accepted: Array<Record<string, unknown>>;
rejected: Array<{ action: Record<string, unknown>; reason: string }>;
@@ -146,6 +147,9 @@ export default function App() {
<section className="result-card">
<h2>Latest result</h2>
<p>{latest.narration}</p>
{latest.parser_feedback ? (
<p className="parser-hint">Parser guidance: {latest.parser_feedback}</p>
) : null}
<pre>{JSON.stringify({ actions: latest.actions, rejected: latest.rejected, latent: latest.latent_resolution }, null, 0)}</pre>
</section>
) : null}

View File

@@ -166,6 +166,16 @@ pre {
color: #ffd2b8;
}
.parser-hint {
margin: 6px 0;
padding: 8px 10px;
border-radius: 10px;
border: 1px solid rgba(220, 191, 141, 0.3);
background: rgba(220, 191, 141, 0.1);
color: #f0d8aa;
font-size: 0.9rem;
}
@media (max-width: 900px) {
.inspector-grid {
grid-template-columns: 1fr;