màj wiki avec retour MES lot-5 AD
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { SECTIONS } from '../config/constants.js';
|
||||
|
||||
export const browseToolDefinition = {
|
||||
name: 'list_wiki_sections',
|
||||
description:
|
||||
'Return the complete EasyWMS wiki structure for navigation. Optionally filter by section.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
section: {
|
||||
type: 'string',
|
||||
enum: ['concepts', 'modules', 'architecture', 'operations', 'limagrain'],
|
||||
description: 'Filter by section (omit to return all sections)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function handleListWikiSections(args, { pages }) {
|
||||
const { section } = args || {};
|
||||
|
||||
const sectionsToShow = section ? [section] : SECTIONS;
|
||||
const lines = [];
|
||||
|
||||
for (const sec of sectionsToShow) {
|
||||
const sectionPages = pages.filter(p => p.path.startsWith(sec + '/'));
|
||||
if (sectionPages.length === 0) continue;
|
||||
|
||||
lines.push(`## ${sec.charAt(0).toUpperCase() + sec.slice(1)} (${sectionPages.length} pages)\n`);
|
||||
|
||||
for (const page of sectionPages) {
|
||||
lines.push(`### ${page.title}`);
|
||||
lines.push(`- **Path:** ${page.path}`);
|
||||
lines.push(`- **Type:** ${page.type}`);
|
||||
if (page.summary) lines.push(`- ${page.summary}`);
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: section
|
||||
? `No pages found in section "${section}".`
|
||||
: 'No wiki pages found. Check that the wiki folder is properly configured.',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const total = sectionsToShow.reduce(
|
||||
(sum, sec) => sum + pages.filter(p => p.path.startsWith(sec + '/')).length,
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `# EasyWMS Wiki — ${section ? section : 'All Sections'} (${total} pages)\n\n` + lines.join('\n'),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Simple Levenshtein distance for fuzzy matching of glossary terms.
|
||||
*/
|
||||
function levenshtein(a, b) {
|
||||
const m = a.length;
|
||||
const n = b.length;
|
||||
const dp = Array.from({ length: m + 1 }, (_, i) => [i]);
|
||||
for (let j = 1; j <= n; j++) dp[0][j] = j;
|
||||
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
dp[i][j] =
|
||||
a[i - 1] === b[j - 1]
|
||||
? dp[i - 1][j - 1]
|
||||
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
||||
}
|
||||
}
|
||||
return dp[m][n];
|
||||
}
|
||||
|
||||
export const glossaryToolDefinition = {
|
||||
name: 'get_glossary_term',
|
||||
description:
|
||||
'Look up a WMS-specific term, abbreviation, or acronym in the EasyWMS glossary (e.g. "LPN", "PDL", "ASN").',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
term: {
|
||||
type: 'string',
|
||||
description: 'Term or abbreviation to look up (case-insensitive)',
|
||||
},
|
||||
},
|
||||
required: ['term'],
|
||||
},
|
||||
};
|
||||
|
||||
export function handleGetGlossaryTerm(args, { glossary }) {
|
||||
const { term } = args;
|
||||
|
||||
if (!term || term.trim().length === 0) {
|
||||
return { content: [{ type: 'text', text: 'Error: term parameter is required.' }] };
|
||||
}
|
||||
|
||||
const query = term.trim().toLowerCase();
|
||||
|
||||
// 1. Exact match
|
||||
if (glossary.has(query)) {
|
||||
const entry = glossary.get(query);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `**${entry.term}**\n\n${entry.definition}\n\n*Source: [Glossary](glossary.md)*`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Partial match — prefer entries whose term STARTS WITH the query
|
||||
// (handles "pdl" → "pdl (picking dedicated location)")
|
||||
const startsWithMatches = [];
|
||||
const containsMatches = [];
|
||||
for (const [key, entry] of glossary) {
|
||||
if (key.startsWith(query + ' ') || key.startsWith(query + '(') || key === query) {
|
||||
startsWithMatches.push(entry);
|
||||
} else if (key.includes(query)) {
|
||||
containsMatches.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Single strong match
|
||||
if (startsWithMatches.length === 1) {
|
||||
const entry = startsWithMatches[0];
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `**${entry.term}**\n\n${entry.definition}\n\n*Source: [Glossary](glossary.md)*`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const partialMatches = startsWithMatches.length > 0 ? startsWithMatches : containsMatches;
|
||||
|
||||
if (partialMatches.length === 1) {
|
||||
const entry = partialMatches[0];
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `**${entry.term}** *(partial match)*\n\n${entry.definition}\n\n*Source: [Glossary](glossary.md)*`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (partialMatches.length > 1) {
|
||||
const list = partialMatches
|
||||
.slice(0, 5)
|
||||
.map(e => `- **${e.term}**: ${e.definition.slice(0, 100)}...`)
|
||||
.join('\n');
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Multiple matches for "${term}":\n\n${list}\n\n*Refine your query for an exact match.*`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Fuzzy match (Levenshtein)
|
||||
const candidates = [];
|
||||
for (const [key, entry] of glossary) {
|
||||
const dist = levenshtein(query, key);
|
||||
if (dist <= 3) candidates.push({ entry, dist });
|
||||
}
|
||||
candidates.sort((a, b) => a.dist - b.dist);
|
||||
|
||||
if (candidates.length > 0) {
|
||||
const suggestions = candidates
|
||||
.slice(0, 3)
|
||||
.map(c => `- **${c.entry.term}**`)
|
||||
.join('\n');
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Term "${term}" not found in glossary. Did you mean:\n\n${suggestions}\n\n*See [Glossary](glossary.md) for the full list.*`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Term "${term}" not found in the EasyWMS glossary. See [Glossary](glossary.md) for the full list.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { getPage, findSimilarPages } from '../services/page.js';
|
||||
|
||||
export const pageToolDefinition = {
|
||||
name: 'get_wiki_page',
|
||||
description:
|
||||
'Load the full content of a specific EasyWMS wiki page by its relative path (e.g. "concepts/container.md").',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Page path relative to wiki/ folder (e.g. "concepts/container.md")',
|
||||
},
|
||||
},
|
||||
required: ['path'],
|
||||
},
|
||||
};
|
||||
|
||||
export function handleGetWikiPage(args, { wikiPath, pages }) {
|
||||
const { path: relPath } = args;
|
||||
|
||||
if (!relPath || relPath.trim().length === 0) {
|
||||
return { content: [{ type: 'text', text: 'Error: path parameter is required.' }] };
|
||||
}
|
||||
|
||||
// Security: prevent path traversal
|
||||
const normalized = relPath.replace(/\\/g, '/').replace(/\.\.\//g, '');
|
||||
|
||||
const content = getPage(wikiPath, normalized, pages);
|
||||
|
||||
if (!content) {
|
||||
const similar = findSimilarPages(normalized, pages);
|
||||
const suggestions =
|
||||
similar.length > 0
|
||||
? '\n\nDid you mean one of these?\n' + similar.map(p => `- ${p.path} — ${p.title}`).join('\n')
|
||||
: '\n\nUse list_wiki_sections to browse available pages.';
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Page not found: "${relPath}"${suggestions}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: content }] };
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
export const relatedToolDefinition = {
|
||||
name: 'get_related_pages',
|
||||
description:
|
||||
'Find all EasyWMS wiki pages that reference a given entity, command, or concept by name (e.g. "Container", "putaway", "ASN").',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Entity name, command, or concept to find references for',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
};
|
||||
|
||||
export function handleGetRelatedPages(args, { pages }) {
|
||||
const { name } = args;
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
return { content: [{ type: 'text', text: 'Error: name parameter is required.' }] };
|
||||
}
|
||||
|
||||
const query = name.trim().toLowerCase();
|
||||
const results = [];
|
||||
|
||||
for (const page of pages) {
|
||||
const mentions = [];
|
||||
|
||||
// 1. Check `related` frontmatter
|
||||
for (const rel of page.related) {
|
||||
const relName = rel.replace(/^.*\//, '').replace('.md', '').toLowerCase();
|
||||
if (relName.includes(query) || query.includes(relName)) {
|
||||
mentions.push({ type: 'related', context: `related: ${rel}` });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check page title
|
||||
if (page.title.toLowerCase().includes(query)) {
|
||||
mentions.push({ type: 'title', context: page.title });
|
||||
}
|
||||
|
||||
// 3. Check body — extract sentences containing the query
|
||||
const sentences = extractMentionContext(page.body, query, 3);
|
||||
for (const sentence of sentences) {
|
||||
mentions.push({ type: 'body', context: sentence });
|
||||
}
|
||||
|
||||
if (mentions.length > 0) {
|
||||
results.push({ page, mentions });
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No pages found referencing "${name}". Try search_wiki for broader results.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const lines = [`**Pages referencing "${name}"** (${results.length} found)\n`];
|
||||
|
||||
for (const { page, mentions } of results) {
|
||||
lines.push(`### ${page.title}`);
|
||||
lines.push(`- **Path:** ${page.path}`);
|
||||
|
||||
const relatedMentions = mentions.filter(m => m.type === 'related');
|
||||
const bodyMentions = mentions.filter(m => m.type === 'body');
|
||||
|
||||
if (relatedMentions.length > 0) {
|
||||
lines.push(`- **In related:** ${relatedMentions.map(m => m.context).join(', ')}`);
|
||||
}
|
||||
|
||||
for (const m of bodyMentions.slice(0, 2)) {
|
||||
lines.push(`- *"${m.context}"*`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract up to `limit` sentence snippets from text that contain the query.
|
||||
*/
|
||||
function extractMentionContext(body, query, limit) {
|
||||
const sentences = body.split(/(?<=[.!?])\s+|\n\n/);
|
||||
const results = [];
|
||||
|
||||
for (const sentence of sentences) {
|
||||
if (results.length >= limit) break;
|
||||
const clean = sentence.trim().replace(/\n/g, ' ');
|
||||
if (clean.toLowerCase().includes(query) && clean.length > 10) {
|
||||
results.push(clean.slice(0, 150));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { searchPages } from '../services/search.js';
|
||||
|
||||
export const searchToolDefinition = {
|
||||
name: 'search_wiki',
|
||||
description:
|
||||
'Search the EasyWMS wiki by keyword, tag, or entity name. Returns relevant pages sorted by relevance.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search terms (keywords, entity name, concept)',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Filter by frontmatter tags',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['concept', 'module', 'architecture', 'operation', 'limagrain'],
|
||||
description: 'Filter by page type',
|
||||
},
|
||||
max_results: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results to return (default: 5)',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
};
|
||||
|
||||
export function handleSearchWiki(args, { pages, invertedIndex }) {
|
||||
const { query, tags, type, max_results } = args;
|
||||
|
||||
if (!query || query.trim().length === 0) {
|
||||
return { content: [{ type: 'text', text: 'Error: query parameter is required.' }] };
|
||||
}
|
||||
|
||||
const results = searchPages(pages, invertedIndex, query, {
|
||||
tags,
|
||||
type,
|
||||
maxResults: max_results || 5,
|
||||
});
|
||||
|
||||
if (results.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No results found for "${query}". Try different keywords or use list_wiki_sections to browse.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const lines = [`**Search results for "${query}"** (${results.length} found)\n`];
|
||||
for (const r of results) {
|
||||
lines.push(`### ${r.title}`);
|
||||
lines.push(`- **Path:** ${r.path}`);
|
||||
lines.push(`- **Type:** ${r.type}`);
|
||||
lines.push(`- **Score:** ${r.score}`);
|
||||
if (r.summary) lines.push(`- **Summary:** ${r.summary}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
||||
}
|
||||
Reference in New Issue
Block a user