Multi-Modell + Vision: OpenRouter (GLM 5.3 Flash), Bild-Upload, Tour-Feature, Dashboard-Fixes
- Modell-Registry (DeepSeek direkt + OpenRouter z-ai/glm-5.3-flash), Modellwahl pro Session, Reasoning-Effort max (env-konfigurierbar) - Bild-Upload für Vision-Modelle (User) + Agent-screenshot()-Tool (nur Vision) - Tour-Planung: geocode + save_tour mit GPX-Export und Karten-Sektion - Fix: System-Prompt wird jetzt wirklich an die API gesendet, inkl. aktuellem Dashboard-Dateinamen für Folge-Updates - Fix: History-Sanitizer gegen 400-Fehler (nie mitten in Tool-Gruppen kürzen) - Fix: reasoning_content-Roundtrip (Pflicht bei DeepSeek mit tools) - Fix: Dashboards ohne Browser-Cache ausliefern
This commit is contained in:
1 parent
eee59363d4
commit
87eb7029f1
10 files changed
+843
-70
No files matched your search
+229
-28
@@ -1,4 +1,5 @@
|
||||
const tools = require('./tools');
|
||||
const { getModel } = require('./config');
|
||||
|
||||
const SYSTEM_PROMPT = `Du bist ein professioneller Web-Researcher und Wissensberater. Deine Aufgabe: Nimm eine Recherche-Frage an, recherchiere gründlich mit den bereitgestellten Tools, verifiziere ALLE Links und erstelle ein schönes HTML-Dashboard. Du bist KEIN Link-Sammler, sondern Analyst, Berater und Horizonterweiterer.
|
||||
|
||||
@@ -18,6 +19,29 @@ const SYSTEM_PROMPT = `Du bist ein professioneller Web-Researcher und Wissensber
|
||||
6. ANALYSIERE: Schreibe eine ausführliche Einleitung mit 6 Absätzen: (1) Überblick, (2) Horizonterweiterung - was du außerdem herausgefunden hast, (3) Einschätzung der Optionen mit Vor-/Nachteilen, (4) konkrete Empfehlung, (5) Fahrplan / erste Schritte, (6) Fallstricke und Hinweise.
|
||||
7. GENERIERE das Dashboard mit dem save_dashboard-Tool (Detailstruktur unten) und fasse dann in deiner finalen Antwort das Wichtigste zusammen.
|
||||
|
||||
## Tour-Planung (PFLICHT bei Tour-Anfragen!)
|
||||
Erkennst du den Wunsch nach einer Route/Tour/Städtebesichtigung (z.B. "plane eine Tour", "Route durch", "Städtebummel", "was sollte ich in X sehen"), gilt:
|
||||
- **Du MUSST die Tools geocode und save_tour verwenden.** Erfinde NIEMALS Koordinaten, Distanzen oder Gehzeiten selbst!
|
||||
- Ablauf: (1) Orte wie gewohnt recherchieren (verifizierte Quellen), (2) sinnvolle Besuchsreihenfolge festlegen, (3) JEDEN Stopp mit geocode geokodieren, (4) save_tour mit allen Stopps aufrufen (Name, lat, lon, Beschreibung), (5) Distanz/Gehzeit AUS DEM TOOL-ERGEBNIS übernehmen, (6) Karten-Sektion ins Dashboard bauen, (7) in der finalen Antwort GPX-Download und Google-Maps-Link nennen.
|
||||
|
||||
### "🗺️ Tour-Karte"-Sektion im Dashboard (nur wenn save_tour gelaufen ist)
|
||||
- Leaflet via CDN einbinden: <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"> und <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> (zusätzlich zu Tailwind CDN)
|
||||
- Karten-Div mit fester Höhe (z.B. style="height: 450px")
|
||||
- Tiles: KEIN API-KEY NÖTIG! Nutze AUSSCHLIESSLICH exakt diese CARTO-Konfiguration (kostenlos, ohne Key, kein Referer-Zwang):
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 20,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>'
|
||||
}).addTo(map);
|
||||
- VERBOTEN sind Anbieter mit API-Key (Mapbox, MapTiler, Thunderforest, Google Maps, Here) und tile.openstreetmap.org – CARTO ist die einzige erlaubte Kartenquelle. Falls du irgendwo einen "API key" sehen solltest, ist es falsch!
|
||||
- Die Stopps (mit lat/lon) INLINE als JavaScript-Objekt ins HTML schreiben
|
||||
- Die Routen-Geometrie per fetch() aus der tour.json laden (URL hast du vom save_tour-Ergebnis). Fallback, wenn fetch fehlschlägt: Polylinie direkt durch die Stopps zeichnen
|
||||
- Nummerierte Marker (CircleMarker mit Zahl), Polylinie (blau), map.fitBounds(...) über alle Punkte
|
||||
- Buttons über der Karte: "📍 Route in Google Maps öffnen" (googleMapsUrl aus dem save_tour-Ergebnis, target=_blank) und "🗺️ GPX für OsmAnd herunterladen" (gpxUrl, mit download-Attribut)
|
||||
- Unter der Karte die Stopp-Liste: Nummer, Name, Beschreibung, "Hier hin navigieren"-Link (navUrl pro Stopp)
|
||||
- Header-Zeile mit Distanz (km) und Gehzeit (min), wenn vorhanden
|
||||
- Hinweis: "GPX in OsmAnd importieren: Datei herunterladen → Menü → Meine Orte → Importieren"
|
||||
|
||||
## Dashboard-Struktur (HTML, komplette Standalone-Datei)
|
||||
- <html lang="de">, <script src="https://cdn.tailwindcss.com"></script> im Head, responsives Design mit Dark-Mode-Unterstützung (dark: Klassen)
|
||||
- Header: Titel + Datum + Anzahl verifizierter Links
|
||||
@@ -28,6 +52,7 @@ const SYSTEM_PROMPT = `Du bist ein professioneller Web-Researcher und Wissensber
|
||||
|
||||
## Regeln
|
||||
- JEDER Link im Dashboard MUSS mit verify() verifiziert sein - füge bei den Karten einen "✓ Verifiziert"-Badge hinzu
|
||||
- Erfinde NIEMALS Koordinaten, Distanzen oder Gehzeiten – bei Touren kommen die ausschließlich aus geocode() und save_tour()
|
||||
- Ziele auf 8-15 verifizierte Ressourcen pro Thema
|
||||
- Bei Login-Walls: old.reddit.com statt reddit.com, nitter.net statt twitter.com; wenn eine Seite blockiert, notiere das und nimm einen anderen Link
|
||||
- Die finale Antwort ist eine deutsche Zusammenfassung (max. 300 Wörter): wichtigste Erkenntnisse + Link zum Dashboard. Details gehören ins Dashboard, nicht in die Antwort.
|
||||
@@ -97,8 +122,74 @@ const TOOL_DEFINITIONS = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'geocode',
|
||||
description:
|
||||
'Wandelt einen Ortsnamen in Koordinaten um (Nominatim/OpenStreetMap, kostenlos). Nutze das für jeden Stopp einer Tour, z.B. "Brandenburger Tor Berlin". Liefert lat/lon. Maximal 1 Request pro Sekunde.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Ortsangabe, z.B. "Brandenburger Tor Berlin" oder bereits Koordinaten "52.5163, 13.3777"' },
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'save_tour',
|
||||
description:
|
||||
'Erstellt aus einer Liste von Stopps (Name + Koordinaten + Beschreibung) eine komplette Tour: GPX-Datei für OsmAnd, tour.json für die Karte im Dashboard, Google-Maps-Routen-URL mit allen Stopps und Fuß-Navigation pro Stopp. Die Route wird über OSRM als echte Geh-Route berechnet (Fallback: Luftlinie). IMMER vorher jeden Stopp mit geocode() geokodieren!',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Titel der Tour, z.B. "Tagestour Prag"' },
|
||||
filename: { type: 'string', description: 'Dateiname ohne Endung, z.B. "prag-tagestour"' },
|
||||
stops: {
|
||||
type: 'array',
|
||||
description: 'Stopps in Besuchsreihenfolge (Start bis Ende)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Name des Ortes' },
|
||||
lat: { type: 'number', description: 'Breitengrad' },
|
||||
lon: { type: 'number', description: 'Längengrad' },
|
||||
description: { type: 'string', description: 'Kurze Beschreibung (2-3 Sätze)' },
|
||||
},
|
||||
required: ['name', 'lat', 'lon'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['title', 'filename', 'stops'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Nur für Vision-Modelle: Seite fotografieren und visuell beurteilen. */
|
||||
const SCREENSHOT_TOOL = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'screenshot',
|
||||
description:
|
||||
'Macht einen Screenshot einer Seite (JPEG) und liefert das Bild in den Kontext. Nutze das bei bildlastigen Seiten, wenn der Text nicht ausreicht (z.B. Anzeigen-Listen, Produktfotos, Infografiken). Maximal 2-3 Screenshots pro Session.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'Die exakte URL der Seite' },
|
||||
full_page: {
|
||||
type: 'boolean',
|
||||
description: 'Volle Seitenlänge erfassen (Standard: nur sichtbarer Bereich). Bei langen Seiten zu groß – dort besser weglassen.',
|
||||
},
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** Menschlich lesbare Zusammenfassung eines Tool-Ergebnisses für den Chat-Verlauf. */
|
||||
function summarizeTool(toolName, output) {
|
||||
let data;
|
||||
@@ -118,6 +209,18 @@ function summarizeTool(toolName, output) {
|
||||
return `Link ${data.ok ? 'VERIFIZIERT ✓' : 'FEHLGESCHLAGEN ✗'}: ${data.url}${data.reason ? ' – ' + data.reason : ''}`;
|
||||
case 'save_dashboard':
|
||||
return `Dashboard gespeichert: ${data.url || ''}`;
|
||||
case 'geocode':
|
||||
return data.error
|
||||
? `Geocoding FEHLGESCHLAGEN: ${data.error}`
|
||||
: `Geocodiert: ${data.name} -> ${data.lat}, ${data.lon}`;
|
||||
case 'save_tour':
|
||||
return data.error
|
||||
? `Tour-Fehler: ${data.error}`
|
||||
: `Tour gespeichert: ${data.title || ''} (${data.stops?.length || 0} Stopps, ${data.distanceKm} km, ${data.durationMin} min). GPX: ${data.gpxUrl || ''}`;
|
||||
case 'screenshot':
|
||||
return data.error
|
||||
? `Screenshot FEHLGESCHLAGEN: ${data.error}`
|
||||
: `Screenshot erstellt: ${data.title || data.url || ''} (Bild liegt im Kontext des Modells, nicht in der History)`;
|
||||
default:
|
||||
return String(output).slice(0, 1000);
|
||||
}
|
||||
@@ -125,32 +228,80 @@ function summarizeTool(toolName, output) {
|
||||
|
||||
const MAX_ITERATIONS = 40;
|
||||
|
||||
async function chatCompletion(config, messages) {
|
||||
/** Baut den System-Prompt inkl. aktuellem Dashboard-Stand und Vision-Hinweisen. */
|
||||
function buildSystemPrompt(session, model) {
|
||||
let prompt = SYSTEM_PROMPT;
|
||||
if (session.dashboard) {
|
||||
const file = session.dashboard.split('/').pop().replace(/\.html$/, '');
|
||||
prompt += `
|
||||
|
||||
## Aktueller Stand dieser Session
|
||||
Für diese Session existiert bereits ein Dashboard: ${session.dashboard}
|
||||
Wenn der Nutzer Änderungen, Ergänzungen oder Erweiterungen wünscht, MUSST du das save_dashboard-Tool aufrufen und das KOMPLETTE aktualisierte HTML mit EXAKT diesem Dateinamen übergeben: "${file}" (ohne .html).
|
||||
Eine bloße Erklärung oder Beschreibung der Änderungen im Chat ist KEINE Umsetzung – erst wenn save_dashboard erfolgreich gelaufen ist, fasst du die Änderungen im Chat zusammen.`;
|
||||
}
|
||||
if (model.vision) {
|
||||
prompt += `
|
||||
|
||||
## Bilderkennung (Vision)
|
||||
- Wenn der Nutzer ein Bild hochgeladen hat (z.B. ein Foto eines Schuhs): Analysiere es SORGFÄLTIG (Marke, Farbe, Material, Form, Zustand, erkennbare Details) und suche gezielt nach ähnlichen Produkten und Angeboten. Beschreibe im Dashboard, worauf du beim Vergleich geachtet hast.
|
||||
- Das hochgeladene Bild liegt dir als Data-URL im Verlauf vor – du kannst es bei Bedarf direkt ins Dashboard einbetten.
|
||||
- Nutze das screenshot-Tool, um bildlastige Seiten (z.B. Anzeigen-Listen, Produktfotos) visuell zu beurteilen, wenn die Text-Extraktion nicht ausreicht. Maximal 2-3 Screenshots pro Session.`;
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/** Erkennt Änderungswünsche am Dashboard in der Nutzernachricht. */
|
||||
function isChangeRequest(text) {
|
||||
return /dashboard|anpassen|anpass|ändern|änder|ergänz|erweiter|aktualisier|update|umbau|hinzufüg|hinzufueg|modifizier|vervollständig|vervollstaendig/i.test(
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
async function chatCompletion(config, messages, systemPrompt, model, tools) {
|
||||
const body = {
|
||||
model: config.model,
|
||||
messages,
|
||||
tools: TOOL_DEFINITIONS,
|
||||
model: model.id,
|
||||
messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
||||
tools,
|
||||
stream: true,
|
||||
max_tokens: 32000,
|
||||
max_tokens: model.maxTokens || config.maxTokens || 64000,
|
||||
stream_options: { include_usage: true },
|
||||
};
|
||||
if (config.thinking !== 'disabled') {
|
||||
|
||||
const isOpenRouter = model.provider === 'openrouter';
|
||||
const baseUrl = isOpenRouter
|
||||
? 'https://openrouter.ai/api/v1'
|
||||
: config.baseUrl || 'https://api.deepseek.com';
|
||||
|
||||
if (isOpenRouter) {
|
||||
// GLM: Reasoning ist Pflicht; Effort über OpenRouter-Param "reasoning"
|
||||
body.reasoning = { effort: model.reasoningEffort?.() || 'max' };
|
||||
} else if (config.thinking !== 'disabled') {
|
||||
body.thinking = { type: 'enabled' };
|
||||
body.reasoning_effort = 'medium';
|
||||
body.reasoning_effort = model.reasoningEffort?.() || 'high';
|
||||
}
|
||||
|
||||
const resp = await fetch(`${config.baseUrl || 'https://api.deepseek.com'}/chat/completions`, {
|
||||
const headers = isOpenRouter
|
||||
? {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${config.openrouterApiKey}`,
|
||||
'X-Title': 'Research Chat',
|
||||
'HTTP-Referer': 'https://github.com/nico/researchchat',
|
||||
}
|
||||
: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
};
|
||||
|
||||
const resp = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.text().catch(() => '');
|
||||
throw new Error(`DeepSeek API Fehler ${resp.status}: ${err.slice(0, 500)}`);
|
||||
throw new Error(`Modell-API Fehler ${resp.status} (${model.id}): ${err.slice(0, 500)}`);
|
||||
}
|
||||
|
||||
const reader = resp.body.getReader();
|
||||
@@ -173,7 +324,9 @@ async function chatCompletion(config, messages) {
|
||||
const choice = json.choices?.[0];
|
||||
if (!choice) return;
|
||||
const d = choice.delta || {};
|
||||
// DeepSeek liefert reasoning_content, OpenRouter/GLM reasoning
|
||||
if (d.reasoning_content) delta.reasoning += d.reasoning_content;
|
||||
else if (d.reasoning) delta.reasoning += d.reasoning;
|
||||
if (d.content) delta.content += d.content;
|
||||
if (d.tool_calls) {
|
||||
for (const tc of d.tool_calls) {
|
||||
@@ -224,7 +377,11 @@ async function chatCompletion(config, messages) {
|
||||
*/
|
||||
async function runResearchJob(config, session, userMessageText, emit, db) {
|
||||
let dashboardUrl = session.dashboard || null;
|
||||
let dashboardFile = dashboardUrl ? dashboardUrl.split('/').pop().replace('.html', '') : null;
|
||||
let dashboardSaved = false;
|
||||
|
||||
const model = getModel(session.model || config.model);
|
||||
const tools = model.vision ? [...TOOL_DEFINITIONS, SCREENSHOT_TOOL] : TOOL_DEFINITIONS;
|
||||
const prices = model.prices;
|
||||
|
||||
const handlers = {
|
||||
async search({ query }) {
|
||||
@@ -238,6 +395,10 @@ async function runResearchJob(config, session, userMessageText, emit, db) {
|
||||
const result = await tools.browse(url, (m) => emit.step(m));
|
||||
return JSON.stringify(result);
|
||||
},
|
||||
async screenshot({ url, full_page }) {
|
||||
emit.progress('📷', `Screenshot: ${url}`);
|
||||
return tools.screenshot(url, !!full_page, (m) => emit.step(m));
|
||||
},
|
||||
async verify({ url }) {
|
||||
emit.progress('✅', `Verifiziere: ${url}`);
|
||||
const result = await tools.verify(url, (m) => emit.step(m));
|
||||
@@ -247,17 +408,27 @@ async function runResearchJob(config, session, userMessageText, emit, db) {
|
||||
emit.progress('📊', 'Dashboard wird gespeichert…');
|
||||
const saved = tools.saveDashboard(session.id, filename || title, html, (m) => emit.step(m));
|
||||
dashboardUrl = saved.url;
|
||||
dashboardFile = saved.file.replace('.html', '');
|
||||
dashboardSaved = true;
|
||||
db.setSessionDashboard(session.id, saved.url);
|
||||
emit.progress('📊', `Dashboard gespeichert: ${saved.file}`);
|
||||
return JSON.stringify(saved);
|
||||
},
|
||||
async geocode({ query }) {
|
||||
emit.progress('📍', `Geocode: ${query}`);
|
||||
const result = await tools.geocode(query, config, (m) => emit.step(m));
|
||||
return JSON.stringify(result);
|
||||
},
|
||||
async save_tour({ title, filename, stops }) {
|
||||
emit.progress('🗺️', `Tour wird erstellt: ${title || ''}`);
|
||||
const result = await tools.saveTour({ stops, title, filename, sessionId: session.id }, config, (m) => emit.step(m));
|
||||
return JSON.stringify(result);
|
||||
},
|
||||
};
|
||||
|
||||
const apiMessages = db.toApiMessages(session.id);
|
||||
const apiMessages = db.toApiMessages(session.id, { provider: model.provider, vision: model.vision });
|
||||
const text = userMessageText;
|
||||
const usage = { promptTokens: 0, completionTokens: 0, cacheHitTokens: 0, cacheMissTokens: 0 };
|
||||
emit.progress('🤖', 'Research startet…');
|
||||
emit.progress('🤖', `Research startet mit ${model.label}…`);
|
||||
|
||||
const trackUsage = (u) => {
|
||||
if (!u) return;
|
||||
@@ -271,17 +442,17 @@ async function runResearchJob(config, session, userMessageText, emit, db) {
|
||||
};
|
||||
|
||||
const finishJob = (finalText, dashboardUrl) => {
|
||||
db.setSessionUsage(session.id, usage);
|
||||
db.setSessionUsage(session.id, usage, prices);
|
||||
db.setSessionStatus(session.id, 'done');
|
||||
if (dashboardUrl) db.setSessionDashboard(session.id, dashboardUrl);
|
||||
const costEstimate = db.computeCost(usage);
|
||||
emit.done({ text: finalText, dashboardUrl, usage, costEstimate });
|
||||
return { text: finalText, dashboardUrl, usage, costEstimate };
|
||||
const costEstimate = db.computeCost(usage, prices);
|
||||
emit.done({ text: finalText, dashboardUrl, usage, costEstimate, model: model.id });
|
||||
return { text: finalText, dashboardUrl, usage, costEstimate, model: model.id };
|
||||
};
|
||||
|
||||
for (let iter = 0; iter < MAX_ITERATIONS; iter++) {
|
||||
emit.progress('🤖', iter === 0 ? 'Analysiere Anfrage und plane Suche…' : 'Weitere Analyse…');
|
||||
const result = await chatCompletion(config, apiMessages);
|
||||
const result = await chatCompletion(config, apiMessages, buildSystemPrompt(session, model), model, tools);
|
||||
trackUsage(result.usage);
|
||||
|
||||
if (result.reasoning) emit.thinking(result.reasoning);
|
||||
@@ -296,7 +467,14 @@ async function runResearchJob(config, session, userMessageText, emit, db) {
|
||||
function: { name: tc.function.name, arguments: tc.function.arguments },
|
||||
})),
|
||||
};
|
||||
db.createMessage(session.id, { role: 'assistant', content: assistantMsg.content, toolCalls: assistantMsg.tool_calls });
|
||||
// DeepSeek: Reasoning-Text muss bei tools-Requests mit zurückgegeben werden
|
||||
if (model.provider === 'deepseek' && result.reasoning) assistantMsg.reasoning_content = result.reasoning;
|
||||
db.createMessage(session.id, {
|
||||
role: 'assistant',
|
||||
content: assistantMsg.content,
|
||||
toolCalls: assistantMsg.tool_calls,
|
||||
reasoning: result.reasoning || null,
|
||||
});
|
||||
apiMessages.push(assistantMsg);
|
||||
|
||||
for (const tc of result.toolCalls) {
|
||||
@@ -312,19 +490,42 @@ async function runResearchJob(config, session, userMessageText, emit, db) {
|
||||
output = JSON.stringify({ error: `Tool-Fehler: ${e.message.slice(0, 300)}` });
|
||||
}
|
||||
}
|
||||
const outStr = typeof output === 'string' ? output : JSON.stringify(output);
|
||||
db.createMessage(session.id, {
|
||||
role: 'tool',
|
||||
toolName: tc.function.name,
|
||||
toolCallId: tc.id,
|
||||
content: summarizeTool(tc.function.name, output).slice(0, 8000),
|
||||
content: summarizeTool(tc.function.name, outStr).slice(0, 8000),
|
||||
});
|
||||
apiMessages.push({ role: 'tool', tool_call_id: tc.id, content: output.slice(0, 8000) });
|
||||
if (output && typeof output === 'object' && output.image) {
|
||||
// Screenshot: Bild nur im laufenden Verlauf, DB bekommt die Zusammenfassung
|
||||
apiMessages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: tc.id,
|
||||
content: [
|
||||
{ type: 'text', text: (output.text || output.title || '').slice(0, 2000) },
|
||||
{ type: 'image_url', image_url: { url: output.image } },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
apiMessages.push({ role: 'tool', tool_call_id: tc.id, content: outStr.slice(0, 8000) });
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const finalText = result.content || (dashboardUrl ? 'Recherche abgeschlossen – siehe Dashboard.' : '');
|
||||
db.createMessage(session.id, { role: 'assistant', content: finalText });
|
||||
if (!dashboardSaved && dashboardUrl && isChangeRequest(userMessageText)) {
|
||||
emit.progress(
|
||||
'⚠️',
|
||||
'Achtung: Das Dashboard wurde nicht aktualisiert – der Agent hat save_dashboard nicht aufgerufen.'
|
||||
);
|
||||
}
|
||||
db.createMessage(session.id, {
|
||||
role: 'assistant',
|
||||
content: finalText,
|
||||
reasoning: result.reasoning || null,
|
||||
});
|
||||
return finishJob(finalText, dashboardUrl);
|
||||
}
|
||||
|
||||
@@ -332,10 +533,10 @@ async function runResearchJob(config, session, userMessageText, emit, db) {
|
||||
role: 'assistant',
|
||||
content: 'Maximale Iterationen erreicht – Recherche abgebrochen. Bitte neue Session starten.',
|
||||
});
|
||||
db.setSessionUsage(session.id, usage);
|
||||
db.setSessionUsage(session.id, usage, prices);
|
||||
db.setSessionStatus(session.id, 'done');
|
||||
emit.error('Maximale Iterationen erreicht – Recherche abgebrochen.');
|
||||
return { text: '', dashboardUrl, usage, costEstimate: db.computeCost(usage) };
|
||||
return { text: '', dashboardUrl, usage, costEstimate: db.computeCost(usage, prices), model: model.id };
|
||||
}
|
||||
|
||||
module.exports = { runResearchJob, SYSTEM_PROMPT, TOOL_DEFINITIONS };
|
||||
Reference in new issue
Block a user