Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 52 additions & 4 deletions conversational-skills/procode-skill-sdk-js/src/mock-server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ app.use(bodyParser.json());

// routes and api calls

const bluepointsSkill = await createBluepointsSkill('en');
// NOTE: the skill is built per request, not once at module load. `Skill` holds
// `slotsInFlight`, and its `Slot` objects are mutated during orchestration (e.g.
// `slotInFlight.setError`). A single shared instance leaks that mutated state into
// every later turn -- across sessions and across users.

/**
* This function lists the skills available by your provider. It will be called by the Assistant UI when selecting skills to create a code-based action.
Expand Down Expand Up @@ -96,16 +99,61 @@ function getSkill(req,res,next) {
res.json(response);
}

/**
* Restores slot visibility and entity schemas that earlier turns produced.
*
* Because the skill is rebuilt per request, anything a previous turn did to a Slot
* object (`show()`, `schema = ...`) is gone. watsonx round-trips `state` back to us
* on every turn, so that is where such decisions have to live. This mirrors what the
* Java SDK already does in SkillOrchestrator.initializeSlotHandlers, via the same
* `visible_slots` and `<slot>_schema` keys.
*/
function rehydrateSlots(skill, state) {
const localVars = state?.local_variables || {};

for (const slotName of localVars.visible_slots || []) {
const slot = skill.slotsInFlight?.find(slotName);
if (!slot) continue;
slot.show();
const schema = localVars[`${slotName}_schema`];
if (schema) slot.schema = schema;
}
}

/**
* Persists the inverse of rehydrateSlots, so the next turn can restore it.
*/
function persistSlots(skillResponse) {
const slots = skillResponse.output?.generic?.find(item => item.response_type === 'slots')?.slots;
if (!slots || !skillResponse.state) return;

skillResponse.state.local_variables ??= {};
skillResponse.state.local_variables.visible_slots = slots.map(slot => slot.name);
for (const slot of slots) {
if (slot.schema) skillResponse.state.local_variables[`${slot.name}_schema`] = slot.schema;
}
}

/**
* This function implements the runtime contract between watsonx Assistant and the Provider
* It follows the OAS specification /orchestrate Operation. See https://github.com/watson-developer-cloud/assistant-toolkit/blob/master/conversational-skills/procode-endpoints.md#oas
* @param req - This is an express request. The `body` property adheres to the schema defined in the OAS specification `OrchestrationRequest` component
* @param res - This is an express response. The json returned should adhere to the schema defined in the OAS specification `OrchestrationResponse` component
* @param next
* @param next
*/
async function orchestrate(req,res,next) {
const skillResponse = await bluepointsSkill.orchestrate({input:req.body.input,context:req.body.context, slots:req.body.slots, state: req.body.state, confirmation_event: req.body.confirmation_event});
res.json(skillResponse);
try {
const bluepointsSkill = await createBluepointsSkill(req.body.context?.global?.language || 'en');
rehydrateSlots(bluepointsSkill, req.body.state);
const skillResponse = await bluepointsSkill.orchestrate({input:req.body.input,context:req.body.context, slots:req.body.slots, state: req.body.state, confirmation_event: req.body.confirmation_event});
persistSlots(skillResponse);
res.json(skillResponse);
} catch (error) {
// Without this, a throw inside the skill becomes an unhandled rejection and
// takes the whole process down -- killing every other user's session too.
console.error('orchestrate failed:', error);
res.status(500).json({error: error.message});
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class LanguageManager {

// Try to dynamically import the language bundle
try {
const bundle = (await import(`${this.basePath}/${scenario}/${lang}.json`, { assert: { type: 'json' } })).default;
const bundle = (await import(`${this.basePath}/${scenario}/${lang}.json`, { with: { type: 'json' } })).default;

// Cache the loaded bundle
if (!this.cache[scenario]) {
Expand All @@ -50,7 +50,10 @@ export class LanguageManager {
if (lang !== 'en') {
return this.loadLanguageBundle(scenario, 'en');
}
throw new Error(`Language bundle not found for scenario: ${scenario}, language: ${lang}`);
// Keep the original error as the cause. Anything can land here -- a syntax
// error in the JSON, an unsupported import attribute -- and reporting all of
// them as "not found" sends you looking for a file that is right there.
throw new Error(`Language bundle not found for scenario: ${scenario}, language: ${lang}`, { cause: error });
}
}

Expand Down