feat: enhance parser feedback mechanism for improved user guidance
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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}`,
|
||||
};
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user