update owliver skill
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import { doc, insights, list, note, text } from './blocks';
|
||||
import { doc, heading, insights, list, note, skillSection, text } from './blocks';
|
||||
import { ASSISTANT_CONTEXTS } from './contexts';
|
||||
import { poolFor } from '@/lib/workforce';
|
||||
import { matchSkill } from '@/lib/skills/registry';
|
||||
import { owliverCapabilityLabel } from '@/lib/skills/surfaces';
|
||||
import {
|
||||
matchOwliverSkill, owliverSkillsForContext, presentable, resolveOwliverResponse,
|
||||
responseTitle, summaryLines,
|
||||
} from '@/lib/skills/owliverResolver';
|
||||
import {
|
||||
appliedToday, askWhichPosition, assignmentPreview, assignmentToExecute, availability,
|
||||
candidateDetail, candidateMatches, candidateRoute, interviewReady, matchWorkforceIntent,
|
||||
@@ -472,6 +477,96 @@ function proposeTraining(skill, question, courses, skillCategories) {
|
||||
};
|
||||
}
|
||||
|
||||
/* ── Declared skills ────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* A skill answering from its own definition.
|
||||
*
|
||||
* The whole reply is composed from what the `.md` declared and what the shared
|
||||
* resolver read: the title is the definition's, the capability is the shape it
|
||||
* offers, the figures are the application's own records, and the follow-ups are
|
||||
* the other capabilities the same definition carries. Nothing in this function
|
||||
* knows which skill it is serving, and nothing may be added that does — the
|
||||
* moment a skill id appears here, the definition has stopped being the source
|
||||
* of truth.
|
||||
*/
|
||||
function declaredAnswer({ skill, capability, question, skillContext }) {
|
||||
const resolved = resolveOwliverResponse({ skill, capability, question, context: skillContext });
|
||||
if (!resolved) return null;
|
||||
|
||||
const { section, data, missing } = resolved;
|
||||
const title = responseTitle(skill, section);
|
||||
|
||||
/* Other shapes of the same reading, offered rather than explained. A chip
|
||||
per remaining capability, worded from the definition's own suggestions
|
||||
where it wrote one for that shape. */
|
||||
const followUp = skill.owliver.capabilities
|
||||
.filter((c) => c !== capability)
|
||||
.map((c) => {
|
||||
const written = skill.owliver.suggestions.find((s) => s.capability === c);
|
||||
return {
|
||||
label: written?.label || `${skill.name} as ${owliverCapabilityLabel(c).toLowerCase()}`,
|
||||
prompt: written?.prompt || `Show ${skill.name} as a ${c}`,
|
||||
};
|
||||
})
|
||||
.slice(0, 3);
|
||||
|
||||
/**
|
||||
* The reading needs a record the page has not supplied. Say which, and offer
|
||||
* the ones there are — an answer about the wrong position would be worse
|
||||
* than a question.
|
||||
*/
|
||||
if (missing === 'position') {
|
||||
const open = (skillContext.positions || []).filter((p) => p.status === 'active').slice(0, 6);
|
||||
return {
|
||||
kind: 'skill',
|
||||
skill,
|
||||
doc: doc(
|
||||
text(`**${title}** reads one position. Which should I read?`),
|
||||
list(open.map((p) => p.title)),
|
||||
note('Open a position, or name one and I will read it.')
|
||||
),
|
||||
followUp: open.slice(0, 4).map((p) => ({ label: p.title, prompt: `${question} for ${p.title}` })),
|
||||
};
|
||||
}
|
||||
if (missing === 'candidate') {
|
||||
return {
|
||||
kind: 'skill',
|
||||
skill,
|
||||
doc: doc(
|
||||
text(`**${title}** reads one candidate.`),
|
||||
note('Open a candidate, or name one and I will read their record.')
|
||||
),
|
||||
};
|
||||
}
|
||||
/* A reading with nothing in it says so, in the resolver's own words. No
|
||||
figure is invented to fill a shape. */
|
||||
if (data.unavailable || data.empty) {
|
||||
return {
|
||||
kind: 'skill',
|
||||
skill,
|
||||
doc: doc(heading(title), text(data.emptyNote || 'There is nothing to report yet.')),
|
||||
followUp,
|
||||
};
|
||||
}
|
||||
|
||||
/* A summary is prose; every other capability is the section the page would
|
||||
draw, drawn here. */
|
||||
const body = capability === 'summary'
|
||||
? [
|
||||
list(summaryLines(data)),
|
||||
data.total != null ? note(`${data.total} record${data.total === 1 ? '' : 's'} in total.`) : null,
|
||||
]
|
||||
: [skillSection(section, presentable(data))];
|
||||
|
||||
return {
|
||||
kind: 'skill',
|
||||
skill,
|
||||
doc: doc(heading(title, section.description || undefined), ...body),
|
||||
followUp,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Skills that can act on this page, ahead of the page's own responder.
|
||||
*
|
||||
@@ -480,10 +575,76 @@ function proposeTraining(skill, question, courses, skillCategories) {
|
||||
* bartender position" open a form instead of returning a pipeline report that
|
||||
* happens to match the word "position".
|
||||
*/
|
||||
function resolveSkill({ question, contextId, disabledSkills, customSkills, roles, skillCategories, courses }) {
|
||||
const skill = matchSkill(question, contextId, disabledSkills, customSkills);
|
||||
function resolveSkill({
|
||||
question, contextId, disabledSkills, customSkills, roles, skillCategories, courses,
|
||||
skillContext = null,
|
||||
}) {
|
||||
/**
|
||||
* One question, one skill, then one way of answering it.
|
||||
*
|
||||
* This used to be two independent matchers run in sequence: declared skills
|
||||
* were resolved first and answered immediately, and only a question that
|
||||
* matched none of them reached the trigger matcher and the coded flows below.
|
||||
* That ordering was the bug. A declared definition could claim a question
|
||||
* belonging to a flow it knew nothing about — "create a position" was
|
||||
* answered by a candidate-matching skill — because the first matcher never
|
||||
* asked whether anything else had a stronger claim.
|
||||
*
|
||||
* So both matchers run, and *then* the question is dispatched:
|
||||
*
|
||||
* 1. A skill with a coded flow owns its own triggers. Create Position and
|
||||
* Forge authoring are conversations with state, not readings, and no
|
||||
* declaration may take one over.
|
||||
* 2. Otherwise a declared answer, if the definition earned the question.
|
||||
* 3. Otherwise the definition reads itself back.
|
||||
*
|
||||
* The property this gives is the one that matters: a skill added tomorrow
|
||||
* cannot capture a flow that already exists, whatever it declares.
|
||||
*/
|
||||
const declared = skillContext
|
||||
? matchOwliverSkill(
|
||||
question,
|
||||
owliverSkillsForContext(contextId, disabledSkills, customSkills)
|
||||
)
|
||||
: null;
|
||||
|
||||
const triggered = matchSkill(question, contextId, disabledSkills, customSkills);
|
||||
|
||||
/**
|
||||
* Which of the two named the question.
|
||||
*
|
||||
* A trigger is an explicit claim on a phrase, so it usually names the skill.
|
||||
* Two cases where it must not, both of which were live bugs:
|
||||
*
|
||||
* - **A verbatim suggestion outranks a trigger.** A definition that
|
||||
* published "Show hiring activity" and had the reader click it has the
|
||||
* strongest claim there is. A *different* definition whose trigger merely
|
||||
* contains those words — including the name every definition falls back
|
||||
* to when it declares no triggers — has a weaker one, and was winning.
|
||||
* - **An answer outranks a description.** A skill with no declared
|
||||
* capabilities can only read its own definition back. If it takes a
|
||||
* question another definition can actually answer, the reader gets a
|
||||
* restatement of the question instead of the figures.
|
||||
*
|
||||
* Neither rule names a skill: both compare what the two definitions declared.
|
||||
*/
|
||||
const canAnswer = (s) => Boolean(s?.owliver?.enabled && s.owliver.capabilities.length);
|
||||
/* A definition that collects answers in the chat or names an action to run is
|
||||
a flow, not a reading, and owns the phrases it declared — read off what it
|
||||
declares rather than from a list of ids, so a flow added later is protected
|
||||
by the same rule. */
|
||||
const ownsFlow = (s) => Boolean(s?.conversation?.length || s?.actions?.length);
|
||||
|
||||
const skill = (triggered && ownsFlow(triggered) && (!declared || !declared.exact))
|
||||
? triggered
|
||||
: declared && (declared.exact || !canAnswer(triggered))
|
||||
? declared.skill
|
||||
: triggered || declared?.skill || null;
|
||||
if (!skill) return null;
|
||||
|
||||
/* The declared answer is only offered for the skill that actually won. */
|
||||
const declaredForSkill = declared && declared.skill.id === skill.id ? declared : null;
|
||||
|
||||
/**
|
||||
* Forge authoring. Same contract as Create Position: what the sentence
|
||||
* answered is filled in, the rest is left blank, and the flow opens rather
|
||||
@@ -522,9 +683,19 @@ function resolveSkill({ question, contextId, disabledSkills, customSkills, roles
|
||||
return { kind: 'skill', skill, ...beginPositionFlow({ question, skill, roles }) };
|
||||
}
|
||||
|
||||
/* A skill with no coded handler still answers, from its own definition. That
|
||||
is what makes a Markdown-only skill useful on the page it names without a
|
||||
line of Owliver being changed for it. */
|
||||
/**
|
||||
* No coded flow owns this one, so the definition answers it.
|
||||
*
|
||||
* A declared `owliver:` block is the richer answer — a real reading, drawn in
|
||||
* the shape the author asked for — so it is tried first. A definition without
|
||||
* one still reads itself back, which is what makes a Markdown-only skill
|
||||
* useful on the page it names without a line of Owliver being changed for it.
|
||||
*/
|
||||
if (declaredForSkill) {
|
||||
const answer = declaredAnswer({ ...declaredForSkill, question, skillContext });
|
||||
if (answer) return answer;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'skill',
|
||||
skill,
|
||||
@@ -538,13 +709,14 @@ function resolveSkill({ question, contextId, disabledSkills, customSkills, roles
|
||||
|
||||
export function resolveIntent({
|
||||
question, contextId, disabledSkills = [], customSkills = [], roles = [], skillCategories = [],
|
||||
courses = [], workforce = null,
|
||||
courses = [], workforce = null, skillContext = null,
|
||||
}) {
|
||||
const context = ASSISTANT_CONTEXTS[contextId] ?? null;
|
||||
|
||||
/* 2. Current page skills — specific triggers, ahead of the general reader. */
|
||||
const skill = resolveSkill({
|
||||
question, contextId, disabledSkills, customSkills, roles, skillCategories, courses,
|
||||
skillContext,
|
||||
});
|
||||
if (skill) return skill;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user