Skip to content

Commit 0f1499b

Browse files
committed
add gemini query flow
1 parent 491ca16 commit 0f1499b

2 files changed

Lines changed: 108 additions & 3 deletions

File tree

LOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- **Web GUI**: Replaced landing with MCP-backed chat UI (vanilla + Tailwind)
77
- **MCP**: Added JSON-RPC search flow with dataset cards
88
- **Fix**: Added `Accept` header for MCP 406 requirement
9+
- **Fix**: Normalize natural-language queries before search
10+
- **Gemini**: Added API key input and NL→Solr query call
911

1012
### Web GUI landing + Pages deploy
1113
- **Web GUI**: Added static landing page in `web-gui/public`

web-gui/public/index.html

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
2121
<span id="status-text" class="text-slate-500">Verifica MCP...</span>
2222
</div>
2323
</div>
24-
<div class="grid gap-4 md:grid-cols-2">
24+
<div class="grid gap-4 md:grid-cols-3">
2525
<label class="flex flex-col gap-2 text-sm font-medium">
2626
Endpoint MCP
2727
<input
@@ -38,6 +38,15 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
3838
placeholder="https://www.dati.gov.it/opendata"
3939
/>
4040
</label>
41+
<label class="flex flex-col gap-2 text-sm font-medium">
42+
API key Gemini
43+
<input
44+
id="gemini-key"
45+
type="password"
46+
class="rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
47+
placeholder="AIza..."
48+
/>
49+
</label>
4150
</div>
4251
<div class="flex flex-wrap items-center justify-between gap-4 text-sm">
4352
<button
@@ -47,7 +56,7 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
4756
Applica impostazioni
4857
</button>
4958
<p class="text-slate-500">
50-
Nessuna integrazione Gemini: solo MCP JSON-RPC.
59+
Query in linguaggio naturale via Gemini 2.5 Flash.
5160
</p>
5261
</div>
5362
</header>
@@ -87,6 +96,7 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
8796
const elements = {
8897
endpoint: document.getElementById("mcp-endpoint"),
8998
ckan: document.getElementById("ckan-server"),
99+
geminiKey: document.getElementById("gemini-key"),
90100
save: document.getElementById("save-settings"),
91101
statusDot: document.getElementById("status-dot"),
92102
statusText: document.getElementById("status-text"),
@@ -99,11 +109,13 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
99109
const state = {
100110
endpoint: localStorage.getItem("mcpEndpoint") || DEFAULT_ENDPOINT,
101111
ckanServer: localStorage.getItem("ckanServer") || DEFAULT_CKAN,
112+
geminiKey: localStorage.getItem("geminiApiKey") || "",
102113
busy: false
103114
};
104115

105116
elements.endpoint.value = state.endpoint;
106117
elements.ckan.value = state.ckanServer;
118+
elements.geminiKey.value = state.geminiKey;
107119

108120
const setStatus = (status, text) => {
109121
elements.statusText.textContent = text;
@@ -223,13 +235,102 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
223235
}
224236
};
225237

238+
const stopwords = new Set([
239+
"a",
240+
"al",
241+
"allo",
242+
"alla",
243+
"alle",
244+
"ai",
245+
"agli",
246+
"da",
247+
"dal",
248+
"dallo",
249+
"dalla",
250+
"delle",
251+
"dei",
252+
"del",
253+
"della",
254+
"di",
255+
"e",
256+
"ed",
257+
"il",
258+
"lo",
259+
"la",
260+
"le",
261+
"i",
262+
"gli",
263+
"in",
264+
"su",
265+
"per",
266+
"con",
267+
"che",
268+
"quali",
269+
"quale",
270+
"quanti",
271+
"quanto",
272+
"tema",
273+
"dataset"
274+
]);
275+
276+
const normalizeQuery = (query) => {
277+
if (/["*:]|\bAND\b|\bOR\b|\bNOT\b/i.test(query)) {
278+
return query;
279+
}
280+
const tokens = query
281+
.toLowerCase()
282+
.match(/[\p{L}\p{N}]+/gu)
283+
?.filter((token) => token.length > 1 && !stopwords.has(token));
284+
if (!tokens || tokens.length === 0) return "*:*";
285+
return tokens.join(" ");
286+
};
287+
288+
const callGemini = async (userQuery) => {
289+
if (!state.geminiKey) {
290+
throw new Error("API key Gemini mancante");
291+
}
292+
const prompt = `Converti la richiesta in una query Solr per CKAN. Rispondi solo con JSON: {"q":"..."}.
293+
Richiesta: ${userQuery}`;
294+
const response = await fetch(
295+
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${state.geminiKey}`,
296+
{
297+
method: "POST",
298+
headers: {
299+
"Content-Type": "application/json",
300+
Accept: "application/json"
301+
},
302+
body: JSON.stringify({
303+
contents: [{ role: "user", parts: [{ text: prompt }] }],
304+
generationConfig: { temperature: 0.2, maxOutputTokens: 200 }
305+
})
306+
}
307+
);
308+
if (!response.ok) {
309+
throw new Error(`Gemini HTTP ${response.status}`);
310+
}
311+
const data = await response.json();
312+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
313+
const jsonStart = text.indexOf("{");
314+
const jsonEnd = text.lastIndexOf("}");
315+
if (jsonStart === -1 || jsonEnd === -1) {
316+
throw new Error("Risposta Gemini non valida");
317+
}
318+
const parsed = JSON.parse(text.slice(jsonStart, jsonEnd + 1));
319+
if (!parsed.q) {
320+
throw new Error("Gemini non ha restituito q");
321+
}
322+
return parsed.q;
323+
};
324+
226325
const runSearch = async (query) => {
326+
const normalizedQuery = normalizeQuery(query);
327+
const solrQuery = await callGemini(query).catch(() => normalizedQuery);
227328
setToolStatus("Uso tool: ckan_package_search");
228329
const result = await callMcp("tools/call", {
229330
name: "ckan_package_search",
230331
arguments: {
231332
server_url: state.ckanServer,
232-
q: query,
333+
q: solrQuery,
233334
rows: 5,
234335
response_format: "json"
235336
}
@@ -278,8 +379,10 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
278379
elements.save.addEventListener("click", async () => {
279380
state.endpoint = elements.endpoint.value.trim() || DEFAULT_ENDPOINT;
280381
state.ckanServer = elements.ckan.value.trim() || DEFAULT_CKAN;
382+
state.geminiKey = elements.geminiKey.value.trim();
281383
localStorage.setItem("mcpEndpoint", state.endpoint);
282384
localStorage.setItem("ckanServer", state.ckanServer);
385+
localStorage.setItem("geminiApiKey", state.geminiKey);
283386
await checkMcp();
284387
});
285388

0 commit comments

Comments
 (0)