Skip to content
Merged
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
36 changes: 36 additions & 0 deletions srv/lib/concept-definition-extract.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,30 @@ const MAX_DEFINITION_LEN = 1200; // hard ceiling; column is String(5000)
const MIN_DEFINITION_LEN = 40; // reject stubs / empty tool calls
const MAX_GROUNDING_ITEMS = 8; // cap the prompt payload

// #2440: refusal signatures. When the concept name reached the model blank
// (a DB key-casing defect fed "CONCEPT: undefined"), the model returned a
// fluent refusal instead of a definition. Those paragraphs are non-null,
// in-range and clean-termed, so they slipped past length+terminology checks and
// were bulk-approved to prod. Reject any output matching these phrasings.
const REFUSAL_PATTERNS = Object.freeze([
/concept name was not provided/i,
/\bno concept name\b/i,
/no valid concept name/i,
/concept name (?:provided )?is\s+\**["“]?undefined/i,
/concept provided is\s+\**["“]?undefined/i,
/sources do not contain sufficient information/i,
/do not contain (?:enough|any|sufficient) information/i,
/does not contain (?:enough|any|sufficient) information/i,
/cannot be determined/i,
/cannot be documented/i,
/cannot be written/i,
/insufficient information/i,
/do not (?:define|describe) a single/i,
/(?:for|define) an undefined concept/i,
/concept ["“]?undefined["”]?/i,
/(?:please )?(?:supply|provide) a valid concept name/i,
]);

// Bare `{type, required, properties}` — defaultCallModel wraps it into the
// OpenAI function-tool shape. No nullable fields (Claude refuses the tool
// call on `type: ['string','null']` — see help-doc-extract.js Bug 5 note).
Expand Down Expand Up @@ -83,6 +107,9 @@ export function applyPostValidation(raw) {
if (!term.ok) {
return { definition: null, reason: 'stale-terminology', violations: term.violations };
}
if (REFUSAL_PATTERNS.some((re) => re.test(def))) {
return { definition: null, reason: 'refusal', violations: [] };
}
return { definition: def, reason: null, violations: [] };
}

Expand All @@ -98,6 +125,15 @@ export function applyPostValidation(raw) {
* violations: Array, promptTokens: number, completionTokens: number }>}
*/
export async function generateConceptDefinition({ callModel, concept, grounding = [] }) {
// #2440: never send the model a blank name. The prod incident was a fluent
// refusal ("the concept name was not provided…") triggered by CONCEPT:
// undefined. Fail loud here rather than emit a garbage prompt — the caller
// counts this as a rejection, never writes it, and the concept is retried
// once its name is populated correctly.
const name = typeof concept?.name === 'string' ? concept.name.trim() : '';
if (!name) {
return { definition: null, reason: 'no-name', violations: [], promptTokens: 0, completionTokens: 0 };
}
const user = buildUserPrompt({ concept, grounding });
const { verdict, tokenUsage } = await extractConceptsCore({
system: SYSTEM_PROMPT,
Expand Down
46 changes: 46 additions & 0 deletions test/unit/concept-definition-extract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,38 @@ describe('applyPostValidation (#2426)', () => {
it('rejects over-long output', () => {
expect(applyPostValidation('x'.repeat(1300)).reason).toBe('too-long');
});

// #2440: fluent LLM refusals ("the concept name was not provided…") are
// non-null, in-range and clean-termed, so length+terminology checks pass them
// through. They were bulk-approved and shipped to prod. Reject them explicitly.
it('rejects refusal signatures as reason="refusal" (#2440)', () => {
const refusals = [
'The concept name was not provided, so a precise definition cannot be determined. Based on the available sources, SAP BTP is a platform.',
'The sources do not contain sufficient information to define a specific SAP developer concept, as the concept name is missing.',
'The concept name was not provided and the supplied source snippets do not define a single identifiable concept for this entry.',
'The sources do not contain sufficient information to define the concept "undefined." The sources cover unrelated topics.',
'A precise definition cannot be determined from the available source snippets provided for this concept entry here.',
// Real prod phrasings observed across the live batch-2440 rows (#2440):
'The concept provided is undefined and no valid concept name was supplied. The available source snippets cover SAP HANA development topics.',
'The provided sources do not contain enough information to define a specific concept, as the concept name is listed as "undefined." The sources cover registering SAP systems.',
'The concept provided is **undefined** and cannot be documented. The supplied sources cover a range of SAP HANA Cloud connectivity tutorials.',
'The provided sources do not contain enough information to write a grounded definition for an undefined concept. Please supply a valid concept name.',
'The concept name provided is "undefined," which does not correspond to a identifiable SAP developer concept supported by the given source snippets.',
// The #2426 bulk-run also swept in "valid name, thin grounding" refusals:
'The provided sources do not contain sufficient information about "Placeholder Property Handling" in UI5 to write a grounded definition.',
'The provided sources do not contain any information about UI5 Semantic Colors and States. The only source available describes a unit testing tutorial step for UI5.',
];
for (const def of refusals) {
const r = applyPostValidation(def);
expect(r.definition, def).toBeNull();
expect(r.reason, def).toBe('refusal');
}
});

it('does not misclassify a legitimate definition as a refusal (#2440)', () => {
const def = 'The SAP Destination service lets applications retrieve connection and configuration details for remote systems, so developers avoid hardcoding endpoints and credentials.';
expect(applyPostValidation(def).reason).toBeNull();
});
});

describe('generateConceptDefinition (#2426)', () => {
Expand All @@ -52,4 +84,18 @@ describe('generateConceptDefinition (#2426)', () => {
expect(r.definition).toBeNull();
expect(r.reason).toBe('stale-terminology');
});

// #2440 root cause: concept.name arrived as undefined (HANA uppercase key
// read as lowercase), so the prompt said "CONCEPT: undefined" and the model
// refused. Never send the model a blank/undefined name — fail loud instead.
it('refuses to generate when concept.name is missing (#2440)', async () => {
let called = false;
const callModel = async () => { called = true; return { verdict: { definition: 'x'.repeat(60) } }; };
for (const bad of [{ slug: 'abap-sql' }, { slug: 'abap-sql', name: '' }, { slug: 'abap-sql', name: ' ' }, { slug: 'abap-sql', name: undefined }]) {
const r = await generateConceptDefinition({ callModel, concept: bad, grounding });
expect(r.definition, JSON.stringify(bad)).toBeNull();
expect(r.reason, JSON.stringify(bad)).toBe('no-name');
}
expect(called, 'model must not be called with a missing name').toBe(false);
});
});
Loading