feat: refactor turn processing and world state management; remove obsolete files and enhance database interactions
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { Action } from "../../contracts/action";
|
||||
import type { InterpreterOutput } from "../../contracts/intent";
|
||||
import type { WorldState } from "../../contracts/world";
|
||||
import type { ResolveIntentInput } from "../resolveIntent";
|
||||
|
||||
export const LLM_INTERPRETER_VERSION = "llm-v1-ollama";
|
||||
@@ -140,20 +141,98 @@ function toReasonCode(value: string | undefined):
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a compact world-context block so the model can resolve entity references
|
||||
* (e.g. "the key" → targetId: "key_1") without inventing IDs.
|
||||
*
|
||||
* Includes:
|
||||
* - The actor's current room
|
||||
* - Every entity visible in that room (items, doors, characters)
|
||||
* - Items in the actor's inventory
|
||||
* - All room entities (needed to populate locationId for move actions)
|
||||
*/
|
||||
function buildWorldContext(worldState: WorldState, actorId: string): string {
|
||||
const actor = worldState.entities[actorId];
|
||||
const actorLocation = actor ? String(actor.attributes.location ?? "") : "";
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
if (actorLocation) {
|
||||
const room = worldState.entities[actorLocation];
|
||||
lines.push(`actor_location_id: ${actorLocation}${room ? ` (${room.name})` : ""}`);
|
||||
}
|
||||
|
||||
for (const entity of Object.values(worldState.entities)) {
|
||||
if (entity.id === actorId) continue;
|
||||
|
||||
const loc = String(entity.attributes.location ?? "");
|
||||
const inRoom = loc === actorLocation;
|
||||
const inActorInventory = loc === `inventory:${actorId}`;
|
||||
const isRoom = entity.type === "room";
|
||||
|
||||
if (inRoom || inActorInventory || isRoom) {
|
||||
const context = inActorInventory
|
||||
? "actor_inventory"
|
||||
: isRoom
|
||||
? "room"
|
||||
: "in_actor_room";
|
||||
// Emit only fields the model needs to build actions.
|
||||
const extras: string[] = [];
|
||||
if (entity.attributes.locked === true) extras.push("locked");
|
||||
if (entity.attributes.open === true) extras.push("open");
|
||||
if (entity.attributes.takeable === true) extras.push("takeable");
|
||||
if (entity.attributes.openable === true) extras.push("openable");
|
||||
const extrasStr = extras.length ? ` [${extras.join(",")}]` : "";
|
||||
lines.push(
|
||||
`entity id=${entity.id} name=${JSON.stringify(entity.name)} type=${entity.type} context=${context}${extrasStr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.length ? lines.join("\n") : "(no world context available)";
|
||||
}
|
||||
|
||||
function buildPrompt(input: ResolveIntentInput): { system: string; user: string } {
|
||||
const system = [
|
||||
"You are an intent-to-actions resolver for a text adventure engine.",
|
||||
"Return ONLY JSON with this shape:",
|
||||
'{"status":"resolved|needs_clarification|rejected","selectedActions":[{"type":"inspect|move|take|open|introduce|describe|transfer","targetId":"optional","locationId":"optional","metadata":{"optional":"object"}}],"selectedConfidence":0.0,"clarification":{"reasonCode":"UNRECOGNIZED_INTENT|AMBIGUOUS_REFERENCE|EMPTY_INPUT|LOW_CONFIDENCE|INTERNAL_INVALID_OUTPUT","question":"string","field":"verb|target|item|recipient|location"},"rationale":"brief"}',
|
||||
"If unresolved, selectedActions must be an empty array and clarification must be present.",
|
||||
"Use canonical action types only. Do not invent fields.",
|
||||
].join(" ");
|
||||
"You will receive the current world state and a player command.",
|
||||
"Return ONLY a JSON object with this exact shape (no markdown, no prose):",
|
||||
JSON.stringify({
|
||||
status: "resolved|needs_clarification|rejected",
|
||||
selectedActions: [
|
||||
{
|
||||
type: "inspect|move|open|take|introduce|describe|transfer",
|
||||
targetId: "entity id from world context, or omit",
|
||||
locationId: "room id for move, or omit",
|
||||
metadata: { note: "omit if unused" },
|
||||
},
|
||||
],
|
||||
selectedConfidence: 0.0,
|
||||
clarification: {
|
||||
reasonCode:
|
||||
"UNRECOGNIZED_INTENT|AMBIGUOUS_REFERENCE|EMPTY_INPUT|LOW_CONFIDENCE|INTERNAL_INVALID_OUTPUT",
|
||||
question: "question to ask player",
|
||||
field: "verb|target|item|recipient|location",
|
||||
},
|
||||
rationale: "one sentence",
|
||||
}),
|
||||
"Rules:",
|
||||
"- Use entity ids from the world context for targetId and locationId. Never invent ids.",
|
||||
"- If status is resolved, selectedActions must be non-empty and clarification must be omitted.",
|
||||
"- If status is not resolved, selectedActions must be empty and clarification must be present.",
|
||||
"- Use only the canonical action types listed above.",
|
||||
].join("\n");
|
||||
|
||||
const worldContext = input.worldState
|
||||
? buildWorldContext(input.worldState, input.actorId)
|
||||
: "(no world context provided)";
|
||||
|
||||
const user = [
|
||||
`actorId: ${input.actorId}`,
|
||||
`input: ${JSON.stringify(input.rawText)}`,
|
||||
`minimum_confidence: ${input.minConfidence}`,
|
||||
].join("\n");
|
||||
`world_context:\n${worldContext}`,
|
||||
`player_input: ${JSON.stringify(input.rawText)}`,
|
||||
].join("\n\n");
|
||||
|
||||
return { system, user };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { InterpreterOutput } from "../contracts/intent";
|
||||
import type { WorldState } from "../contracts/world";
|
||||
import { resolveDeterministicIntent } from "./adapters/deterministicResolver";
|
||||
import { resolveLlmIntent } from "./adapters/llmResolver";
|
||||
import {
|
||||
@@ -11,6 +12,7 @@ const DEFAULT_MIN_CONFIDENCE = 0.65;
|
||||
type InterpretTurnOptions = {
|
||||
mode?: ResolverMode;
|
||||
minConfidence?: number;
|
||||
worldState?: WorldState;
|
||||
};
|
||||
|
||||
function getResolverMode(options?: InterpretTurnOptions): ResolverMode {
|
||||
@@ -25,6 +27,7 @@ function buildInput(rawText: string, actorId: string, options?: InterpretTurnOpt
|
||||
rawText,
|
||||
actorId,
|
||||
minConfidence: options?.minConfidence ?? DEFAULT_MIN_CONFIDENCE,
|
||||
worldState: options?.worldState,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { InterpreterOutput } from "../contracts/intent";
|
||||
import type { WorldState } from "../contracts/world";
|
||||
|
||||
export type ResolverMode = "deterministic" | "llm" | "hybrid";
|
||||
|
||||
@@ -6,6 +7,8 @@ export type ResolveIntentInput = {
|
||||
rawText: string;
|
||||
actorId: string;
|
||||
minConfidence: number;
|
||||
/** Optional world state — passed to LLM resolvers to provide entity context. */
|
||||
worldState?: WorldState;
|
||||
};
|
||||
|
||||
export type IntentResolver = {
|
||||
|
||||
Reference in New Issue
Block a user