KI-Chat: Web-Recherche ueber Ollama Web Search

Der Assistent kannte bisher nur die Datenbank. Fragen wie "Was muss ich
ueber die Firma wissen?" konnte er damit nicht beantworten - Groesse,
Produkte, News, Kultur stehen nirgends in der Bewerbung.

Zwei neue Werkzeuge (web_suche, web_seite_lesen) ueber die Web-Search-API
von ollama.com. Sie authentifizieren sich mit demselben OLLAMA_API_KEY,
den der Benutzer fuer Chat und Dokumente ohnehin hinterlegt hat: keine
zusaetzliche Konfiguration. Anders als /api/chat haengt die Websuche
nicht an OLLAMA_HOST - wer rein lokal chattet, hat schlicht keine
Websuche, der Rest laeuft unveraendert weiter.

- Tool-Runden 4 -> 8: Bewerbung holen, suchen, Seiten lesen, antworten
  lief vorher ins Limit, statt zu antworten.
- Antworten der Websuche hart gedeckelt, sie wandern sonst in jeder
  weiteren Runde erneut ins Kontextfenster.
- Status-Label zeigt, wonach gesucht wird - sonst sieht der Nutzer nicht,
  ob die richtige Firma erwischt wurde.
- Nackte URLs im Chat sind klickbar: Belege nennt das Modell meist als
  reine URL, nicht als Markdown-Link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 12:02:29 +02:00
co-authored by Claude Opus 4.8
parent 9fc4225204
commit 4a036fe215
4 changed files with 188 additions and 6 deletions
+55 -1
View File
@@ -34,6 +34,7 @@ const {
DOKUMENT_TYPEN, normalizeDokumente,
} = require('./lib/documents');
const chat = require('./lib/chat');
const websuche = require('./lib/websuche');
const promptStore = require('./lib/prompts');
const designStore = require('./lib/design');
const mailer = require('./lib/mailer');
@@ -3932,6 +3933,35 @@ initializeDatabase().then(async () => {
parameters: { type: 'object', properties: {} },
},
},
{
type: 'function',
function: {
name: 'web_suche',
description: 'Sucht im Internet und liefert Treffer mit Titel, URL und Auszug. Nutze dies für alles, was NICHT in der Bewerbungsdatenbank steht: Hintergrund zu einer Firma (Größe, Produkte, Standorte, Kultur, aktuelle News, Geschäftsberichte, Bewertungen), Gehaltsspannen, Branchen- oder Technologiefragen, Vorbereitung auf ein Vorstellungsgespräch. Hole dir Firmenname/Stelle zuerst per bewerbung_detail aus der Datenbank und suche dann gezielt danach.',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Suchanfrage in natürlicher Sprache, möglichst konkret (z. B. „Muster GmbH München Mitarbeiterzahl Produkte 2026").' },
max_results: { type: 'integer', description: 'Anzahl Treffer (Standard 5, max 10).' },
},
required: ['query'],
},
},
},
{
type: 'function',
function: {
name: 'web_seite_lesen',
description: 'Lädt eine konkrete Webseite und liefert ihren Text. Nutze dies, wenn ein Treffer aus web_suche vertieft werden soll (z. B. die „Über uns"-/Karriereseite der Firma) oder wenn der Nutzer bzw. die Bewerbung eine URL nennt (quelle_url der Stellenanzeige).',
parameters: {
type: 'object',
properties: {
url: { type: 'string', description: 'Vollständige URL inkl. https://' },
},
required: ['url'],
},
},
},
];
// Tool labels shown in the UI while a tool call is in flight.
@@ -3940,8 +3970,23 @@ initializeDatabase().then(async () => {
list_bewerbungen: 'Bewerbungen werden geladen…',
bewerbung_detail: 'Bewerbungsdetails werden geladen…',
kommende_termine: 'Termine werden geladen…',
web_suche: 'Im Internet wird recherchiert…',
web_seite_lesen: 'Webseite wird gelesen…',
};
// Bei der Web-Recherche zeigt das Label mit an, wonach gesucht bzw. was geladen
// wird — sonst steht der Nutzer vor einem stummen „recherchiert…", ohne zu
// sehen, ob der Assistent die richtige Firma erwischt hat.
function chatToolLabel(name, args) {
const basis = CHAT_TOOL_LABELS[name] || name;
const a = args || {};
if (name === 'web_suche' && a.query) return `Im Internet wird recherchiert: „${String(a.query).slice(0, 80)}"`;
if (name === 'web_seite_lesen' && a.url) {
try { return `Webseite wird gelesen: ${new URL(String(a.url)).hostname}`; } catch (e) { /* Label ohne Host */ }
}
return basis;
}
// Execute one tool call against the database. Returns a JSON-serialisable
// value that is fed back to the model as the tool result.
async function executeChatTool(name, args) {
@@ -4015,6 +4060,15 @@ initializeDatabase().then(async () => {
})),
};
}
// Web-Recherche (Ollama Web Search) — Fehler werden als Tool-Ergebnis
// zurückgegeben (runChat fängt sie ohnehin ab), damit das Modell einen
// Ausfall der Websuche dem Nutzer sagen kann, statt den Chat abzubrechen.
if (name === 'web_suche') {
return websuche.suche(a.query, a.max_results);
}
if (name === 'web_seite_lesen') {
return websuche.seiteLesen(a.url);
}
return { error: 'unbekanntes Werkzeug: ' + name };
}
@@ -4156,7 +4210,7 @@ initializeDatabase().then(async () => {
tools: CHAT_TOOLS,
signal: controller.signal,
onToken: (delta) => send({ type: 'token', content: delta }),
onToolCall: (name) => send({ type: 'tool', name, label: CHAT_TOOL_LABELS[name] || name }),
onToolCall: (name, args) => send({ type: 'tool', name, label: chatToolLabel(name, args) }),
executeTool: executeChatTool,
});
} catch (err) {