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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ L'idée est ici de répondre à des précises en traitant côté serveur les app
#### Explorer les tables

* [gpf_wfs_list_types()](src/tools/GpfWfsListTypesTool.ts) pour **lister de façon exhaustive les types WFS connus du catalogue de schémas embarqué**. Cet outil est surtout utile pour un inventaire complet ou une exploration globale du catalogue ; pour trouver rapidement un type pertinent, préférer `gpf_wfs_search_types`.
* [gpf_wfs_search_types(keywords,max_results=10)](src/tools/GpfWfsSearchTypesTool.ts) pour **rechercher un type WFS pertinent à partir de mots-clés et obtenir un `typename` valide**. La recherche est textuelle et configurable via `GPF_WFS_MINISEARCH_OPTIONS`.
* [gpf_wfs_search_types(keywords,max_results=10)](src/tools/GpfWfsSearchTypesTool.ts) pour **rechercher un type WFS pertinent à partir de mots-clés et obtenir un `typename` valide**. La recherche est textuelle, configurable via `GPF_WFS_MINISEARCH_OPTIONS`, et renvoie aussi un score de pertinence quand il est disponible.

> - Quels sont les millésimes ADMINEXPRESS disponibles sur la Géoplateforme?
> - Quelle est la table de la BDTOPO correspondant aux bâtiments?
Expand Down
34 changes: 22 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ignfab/geocontext",
"version": "0.9.4",
"version": "0.9.5",
"description": "An experimental MCP server providing access to the services and data of the french Geoplateform",
"type": "module",
"bin": {
Expand Down Expand Up @@ -39,7 +39,7 @@
},
"dependencies": {
"@camptocamp/ogc-client": "^1.3.0",
"@ignfab/gpf-schema-store": "^0.1.2",
"@ignfab/gpf-schema-store": "^0.1.3",
"@rgrove/parse-xml": "^4.2.0",
"https-proxy-agent": "^7.0.6",
"jsts": "^2.12.1",
Expand Down
5 changes: 3 additions & 2 deletions src/gpf/wfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {
Collection,
CollectionSearchResult,
getCollectionCatalog,
MiniSearchCollectionSearchEngine,
MiniSearchCollectionSearchOptions,
Expand Down Expand Up @@ -184,8 +185,8 @@ export class WfsClient {
return this.catalog.list();
}

async searchFeatureTypes(query: string, maxResults: number = 20): Promise<Collection[]> {
return this.catalog.search(query, {
async searchFeatureTypesWithScores(query: string, maxResults: number = 20): Promise<CollectionSearchResult[]> {
return this.catalog.searchWithScores(query, {
limit: maxResults,
});
}
Expand Down
14 changes: 8 additions & 6 deletions src/tools/GpfWfsSearchTypesTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const gpfWfsSearchTypeResultSchema = z.object({
id: z.string().describe("L'identifiant complet du type WFS."),
title: z.string().describe("Le titre lisible du type WFS."),
description: z.string().describe("La description du type WFS."),
score: z.number().describe("Le score de pertinence de la recherche.").optional(),
});

const gpfWfsSearchTypesOutputSchema = z.object({
Expand All @@ -38,7 +39,7 @@ class GpfWfsSearchTypesTool extends MCPTool<GpfWfsSearchTypesInput> {
description = [
"Recherche des types WFS de la Géoplateforme (GPF) à partir de mots-clés afin de trouver un identifiant de type (`typename`) valide.",
"Utiliser ce tool avant `gpf_wfs_describe_type` ou `gpf_wfs_get_features` lorsque le nom exact du type n'est pas connu.",
"La recherche est textuelle (mini-search) et retourne une liste ordonnée de candidats avec leur identifiant, leur titre et leur description.",
"La recherche est textuelle (mini-search) et retourne une liste ordonnée de candidats avec leur identifiant, leur titre, leur description et un score de pertinence éventuel.",
"Le paramètre `max_results` permet d'élargir le nombre de candidats retournés (10 par défaut).",
].join("\r\n");
protected outputSchemaShape = gpfWfsSearchTypesOutputSchema;
Expand All @@ -47,12 +48,13 @@ class GpfWfsSearchTypesTool extends MCPTool<GpfWfsSearchTypesInput> {

async execute(input: GpfWfsSearchTypesInput) {
const maxResults = input.max_results || 10;
const featureTypes = await wfsClient.searchFeatureTypes(input.query, maxResults);
const featureTypes = await wfsClient.searchFeatureTypesWithScores(input.query, maxResults);
return {
results: featureTypes.map((featureType) => ({
id: featureType.id,
title: featureType.title,
description: featureType.description,
results: featureTypes.map(({ collection, score }) => ({
id: collection.id,
title: collection.title,
description: collection.description,
...(score !== undefined ? { score } : {}),
})),
};
}
Expand Down
42 changes: 25 additions & 17 deletions test/gpf/wfs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,20 @@ describe("Test WfsClient",() => {
});
});

describe("searchFeatureTypes",() => {
describe("searchFeatureTypesWithScores",() => {
it("should find BDTOPO_V3:batiment for 'bâtiments bdtopo'", async () => {
const featureTypes = await wfsClient.searchFeatureTypes("bâtiments bdtopo");
expect(featureTypes).toBeDefined();
expect(featureTypes.length).toBeGreaterThan(0);
const featureTypeNames= featureTypes.map((featureType)=>featureType.id);
expect(featureTypeNames).toContain("BDTOPO_V3:batiment");
});
it("should find BDTOPO_V3:departement and ADMINEXPRESS-COG.LATEST:departement for 'départements'", async () => {
const featureTypes = await wfsClient.searchFeatureTypes("départements");
expect(featureTypes).toBeDefined();
expect(featureTypes.length).toBeGreaterThan(0);
const featureTypeNames= featureTypes.map((featureType)=>featureType.id);
expect(featureTypeNames).toContain("BDTOPO_V3:departement");
const featureTypes = await wfsClient.searchFeatureTypesWithScores("bâtiments bdtopo");
expect(featureTypes).toBeDefined();
expect(featureTypes.length).toBeGreaterThan(0);
const featureTypeNames= featureTypes.map((featureType)=>featureType.collection.id);
expect(featureTypeNames).toContain("BDTOPO_V3:batiment");
});
it("should find BDTOPO_V3:departement and ADMINEXPRESS-COG.LATEST:departement for 'départements'", async () => {
const featureTypes = await wfsClient.searchFeatureTypesWithScores("départements");
expect(featureTypes).toBeDefined();
expect(featureTypes.length).toBeGreaterThan(0);
const featureTypeNames= featureTypes.map((featureType)=>featureType.collection.id);
expect(featureTypeNames).toContain("BDTOPO_V3:departement");
expect(featureTypeNames).toContain("ADMINEXPRESS-COG.LATEST:departement");
});

Expand All @@ -118,16 +118,24 @@ describe("Test WfsClient",() => {
boost: { title: 4.0 },
}
});
const featureTypes = await tuned.searchFeatureTypes("bâtiments bdtopo");
const featureTypes = await tuned.searchFeatureTypesWithScores("bâtiments bdtopo");
expect(featureTypes).toBeDefined();
expect(featureTypes.length).toBeGreaterThan(0);
const featureTypeNames= featureTypes.map((featureType)=>featureType.id);
const featureTypeNames= featureTypes.map((featureType)=>featureType.collection.id);
expect(featureTypeNames).toContain("BDTOPO_V3:batiment");
});
it("should return scored results for 'bâtiments bdtopo'", async () => {
const featureTypes = await wfsClient.searchFeatureTypesWithScores("bâtiments bdtopo");
expect(featureTypes).toBeDefined();
expect(featureTypes.length).toBeGreaterThan(0);

const batimentResult = featureTypes.find((featureType) => featureType.collection.id === "BDTOPO_V3:batiment");
expect(batimentResult).toBeDefined();
expect(batimentResult?.score).toEqual(expect.any(Number));
});
});
describe("getFeatureType",() => {

describe("getFeatureType",() => {
it("should return the feature type with BDTOPO_V3:batiment", async () => {
const featureType = await wfsClient.getFeatureType("BDTOPO_V3:batiment");
expect(featureType).toBeDefined();
Expand Down
56 changes: 56 additions & 0 deletions test/tools/wfs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe("Test GpfWfsSearchTypesTool",() => {
id: "BDTOPO_V3:batiment",
title: "Batiment",
description: "Description de test",
score: 12.5,
},
],
};
Expand Down Expand Up @@ -58,6 +59,7 @@ describe("Test GpfWfsSearchTypesTool",() => {
results: [
{
id: "BDTOPO_V3:batiment",
score: 12.5,
},
],
});
Expand All @@ -66,11 +68,65 @@ describe("Test GpfWfsSearchTypesTool",() => {
results: [
{
id: "BDTOPO_V3:batiment",
score: 12.5,
},
],
});
});

it("should omit score when it is undefined", async () => {
class TestableGpfWfsSearchTypesToolWithoutScore extends GpfWfsSearchTypesTool {
async execute() {
return {
results: [
{
id: "BDTOPO_V3:batiment",
title: "Batiment",
description: "Description de test",
},
],
};
}
}

const tool = new TestableGpfWfsSearchTypesToolWithoutScore();
const response = await tool.toolCall({
params: {
name: "gpf_wfs_search_types",
arguments: {
query: "batiment",
max_results: 1,
},
},
});

expect(response.isError).toBeUndefined();
expect(response.content[0]).toMatchObject({
type: "text",
});
const textContent = response.content[0];
if (textContent.type !== "text") {
throw new Error("expected text content");
}

const parsedTextContent = JSON.parse(textContent.text);
expect(parsedTextContent.results[0]).toMatchObject({
id: "BDTOPO_V3:batiment",
});
expect(parsedTextContent.results[0]).not.toHaveProperty("score");

expect(response.structuredContent).toBeDefined();
expect(response.structuredContent).toMatchObject({
results: [
{
id: "BDTOPO_V3:batiment",
},
],
});
const structuredContent = response.structuredContent as { results: Array<Record<string, unknown>> };
expect(structuredContent.results[0]).not.toHaveProperty("score");
});

it("should return isError=true for invalid input", async () => {
const tool = new GpfWfsSearchTypesTool();
const response = await tool.toolCall({
Expand Down
Loading