gateway/modules/features/chatbot/mainChatbot.py
2026-01-05 07:34:45 +01:00

1352 lines
58 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Simple chatbot feature - direct AI center implementation.
Bypasses complex workflow engine for fast, simple chatbot responses.
"""
import logging
import json
import uuid
import asyncio
import re
import datetime
from typing import Optional, Dict, Any
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, ChatMessage, WorkflowModeEnum
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum
from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp
from modules.services import getInterface as getServices
from modules.connectors.connectorPreprocessor import PreprocessorConnector
from modules.features.chatbot.eventManager import get_event_manager
from modules.features.chatbot.eventManager import get_event_manager
logger = logging.getLogger(__name__)
def _extractJsonFromResponse(content: str) -> Optional[dict]:
"""Extract JSON from AI response, handling markdown code blocks."""
# Try direct JSON parse first
try:
return json.loads(content.strip())
except json.JSONDecodeError:
pass
# Try to extract JSON from markdown code blocks
json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', content, re.DOTALL)
if json_match:
try:
return json.loads(json_match.group(1))
except json.JSONDecodeError:
pass
# Try to find JSON object in the text
json_match = re.search(r'\{.*\}', content, re.DOTALL)
if json_match:
try:
return json.loads(json_match.group(0))
except json.JSONDecodeError:
pass
return None
async def _generateSqlFromQuestion(services, userQuestion: str, context: str = "") -> str:
"""Generate SQL query directly from user question when JSON parsing fails."""
current_date = datetime.datetime.now().strftime("%d.%m.%Y")
sqlPrompt = f"""Heute ist der {current_date}.
Du bist ein Chatbot der Althaus AG.
Du hast Zugriff auf ein SQL query tool, dass es dir ermöglicht, SQL SELECT Abfragen auf der Althaus AG Datenbank auszuführen.
DATENBANK-INFORMATIONEN:
- Datenbankdatei: /data/database.db (SQLite)
- Tabellen: Artikel, Einkaufspreis, Lagerplatz_Artikel
Die Datenbank besteht aus drei Tabellen, die über Beziehungen verbunden sind:
- **Artikel**: Enthält alle Produktinformationen (I_ID, Artikelbezeichnung, Artikelnummer, etc.)
- **Einkaufspreis**: Enthält Preisdaten (m_Artikel, EP_CHF)
- **Lagerplatz_Artikel**: Enthält Lagerbestands- und Lagerplatzinformationen (R_ARTIKEL, R_LAGERPLATZ, S_IST_BESTAND, etc.)
- **Beziehungen**:
- Artikel.I_ID = Einkaufspreis.m_Artikel
- Artikel.I_ID = Lagerplatz_Artikel.R_ARTIKEL
TABELLEN-SCHEMA (WICHTIG - Spalten mit Leerzeichen/Sonderzeichen IMMER in doppelte Anführungszeichen setzen):
Tabelle 1: Artikel
CREATE TABLE Artikel (
"I_ID" INTEGER PRIMARY KEY,
"Artikelbeschrieb" TEXT,
"Artikelbezeichnung" TEXT,
"Artikelgruppe" TEXT,
"Artikelkategorie" TEXT,
"Artikelkürzel" TEXT,
"Artikelnummer" TEXT,
"Einheit" TEXT,
"Gesperrt" TEXT,
"Keywords" TEXT,
"Lieferant" TEXT,
"Warengruppe" TEXT
)
Tabelle 2: Einkaufspreis
CREATE TABLE Einkaufspreis (
"m_Artikel" INTEGER,
"EP_CHF" FLOAT
)
Tabelle 3: Lagerplatz_Artikel
CREATE TABLE Lagerplatz_Artikel (
"R_ARTIKEL" INTEGER,
"R_LAGERPLATZ" TEXT,
"S_BESTELLTER_BESTAND" INTEGER,
"S_IST_BESTAND" TEXT,
"S_MAXIMALBESTAND" INTEGER,
"S_MINDESTBESTAND" INTEGER,
"S_RESERVIERTER_BESTAND" INTEGER,
"S_SOLL_BESTAND" INTEGER
)
SQL-HINWEISE:
- Verwende IMMER doppelte Anführungszeichen für Spaltennamen: "Artikelkürzel", "Artikelnummer", etc.
- Für Textsuche verwende LIKE mit Wildcards: WHERE a."Artikelbezeichnung" LIKE '%suchbegriff%'
- Für Preisabfragen: Nutze JOINs um auf e."EP_CHF" zuzugreifen
- Für Lagerbestände: Nutze JOINs um auf l."S_IST_BESTAND", l."S_SOLL_BESTAND", etc. zuzugreifen
- WICHTIG bei S_IST_BESTAND: Dieser Wert kann "Unbekannt" sein (TEXT), nicht nur Zahlen! Prüfe mit WHERE l."S_IST_BESTAND" != 'Unbekannt' wenn du nur numerische Werte willst
- Verwende Tabellenaliase (a für Artikel, e für Einkaufspreis, l für Lagerplatz_Artikel) für bessere Lesbarkeit
SQL-AGGREGATIONEN:
Du kannst SQL-Aggregationsfunktionen verwenden, um statistische Auswertungen und Zusammenfassungen zu erstellen:
- COUNT() - Anzahl zählen
- SUM() - Summe berechnen (z.B. SUM(CASE WHEN l."S_IST_BESTAND" != 'Unbekannt' THEN CAST(l."S_IST_BESTAND" AS INTEGER) ELSE 0 END))
- AVG() - Durchschnitt
- MIN() / MAX() - Minimum/Maximum
- GROUP BY - Gruppierung
Beispiel für Lagerbestand-Aggregation:
SELECT SUM(CASE WHEN l."S_IST_BESTAND" != 'Unbekannt' THEN CAST(l."S_IST_BESTAND" AS INTEGER) ELSE 0 END) as "Gesamtbestand"
FROM Artikel a
LEFT JOIN Lagerplatz_Artikel l ON a."I_ID" = l."R_ARTIKEL"
WHERE a."Artikelbezeichnung" LIKE '%LED%'
Generate a SQL SELECT query to answer this question: {userQuestion}{context}
Return ONLY the SQL query, nothing else. Use SELECT queries only. Use double quotes for all column names."""
sqlRequest = AiCallRequest(
prompt=sqlPrompt,
options=AiCallOptions(
resultFormat="txt",
operationType=OperationTypeEnum.DATA_ANALYSE,
processingMode=ProcessingModeEnum.BASIC
)
)
sqlResponse = await services.ai.callAi(sqlRequest)
# Extract SQL from response (might be wrapped in markdown)
sql = sqlResponse.content.strip()
# Remove markdown code blocks if present
if sql.startswith("```"):
sql = re.sub(r'^```(?:sql)?\s*', '', sql)
sql = re.sub(r'\s*```$', '', sql)
return sql.strip()
async def _generate_conversation_name(
services,
userPrompt: str,
userLanguage: str = "en"
) -> str:
"""
Generate a short, descriptive conversation name based on user's prompt.
Args:
services: Services instance with AI access
userPrompt: The user's input prompt
userLanguage: User's preferred language (for prompt localization)
Returns:
Short conversation name (max 60 characters)
"""
try:
# Truncate prompt if too long to avoid excessive token usage
truncated_prompt = userPrompt[:200] if len(userPrompt) > 200 else userPrompt
# Create a prompt that detects the input language and generates title in the same language
name_prompt = f"""You are an expert in professional business communication. Your task is to create a professional conversation title.
CRITICAL: The title MUST be in the SAME LANGUAGE as the user's question. If the question is in German, respond in German. If in French, respond in French. If in English, respond in English.
USER QUESTION: "{truncated_prompt}"
TASK:
1. Identify the language of the user's question
2. Identify the main topic of the question
3. Create an elegant, professional title in THE SAME LANGUAGE as the question (max 60 characters, no punctuation)
RULES:
- Title MUST match the language of the input question exactly
- DO NOT translate to another language
- DO NOT just copy words from the question
- Use professional language appropriate to the detected language
- No punctuation (?, !, .)
EXAMPLES:
- Input (German): "wie viele leds haben wir auf lager?" → Title (German): "LED Lagerbestand"
- Input (German): "wie viel kosten die leds ungefähr? was ist die preisrange?" → Title (German): "LED Preisübersicht"
- Input (French): "combien de leds avons-nous en stock?" → Title (French): "Stock de LED"
- Input (French): "combien coûtent les leds environ?" → Title (French): "Aperçu des Prix LED"
- Input (English): "how many leds do we have in stock?" → Title (English): "LED Inventory Inquiry"
- Input (English): "how much do the leds cost approximately?" → Title (English): "LED Price Overview"
Respond ONLY with the title in the same language as the question, nothing else:"""
# Ensure AI services are initialized before calling
await services.ai.ensureAiObjectsInitialized()
nameRequest = AiCallRequest(
prompt=name_prompt,
options=AiCallOptions(
resultFormat="txt",
operationType=OperationTypeEnum.DATA_GENERATE,
processingMode=ProcessingModeEnum.DETAILED,
temperature=0.7 # Balanced temperature for creativity and consistency
)
)
nameResponse = await services.ai.callAi(nameRequest)
raw_response = nameResponse.content.strip()
logger.info(f"AI name generation raw response (full): {raw_response}")
# Extract title from response - look for "TITEL:" or "TITLE:" marker first
generated_name = raw_response
# Try to extract title after "TITEL:" or "TITLE:" marker (case insensitive)
title_markers = ["TITEL:", "TITLE:", "Titre:", "Titel:", "TITRE:", "titel:", "title:"]
for marker in title_markers:
if marker.lower() in raw_response.lower():
# Find the marker (case insensitive)
marker_pos = raw_response.lower().find(marker.lower())
if marker_pos >= 0:
generated_name = raw_response[marker_pos + len(marker):].strip()
# Take only the first line after the marker
generated_name = generated_name.split('\n')[0].strip()
logger.info(f"Extracted title after marker '{marker}': {generated_name}")
break
# If no marker found, try to find the last meaningful line
if generated_name == raw_response or len(generated_name) > 60:
lines = [line.strip() for line in raw_response.split("\n") if line.strip()]
# Look for a line that looks like a title (not too long, no colons except at start)
for line in reversed(lines):
# Skip lines that are clearly explanations or steps
if any(skip in line.lower() for skip in ["schritt", "step", "étape", "beispiel", "example", "wichtig", "important"]):
continue
# Take lines that are reasonable title length and don't have colons in the middle
if 3 <= len(line) <= 60 and (":" not in line or line.startswith(":")):
generated_name = line
logger.info(f"Extracted title from last meaningful line: {generated_name}")
break
# Remove common prefixes/suffixes and clean up
generated_name = re.sub(r'^(Title|Titel|Titre|Name|Name:):\s*', '', generated_name, flags=re.IGNORECASE)
generated_name = re.sub(r'^["\']|["\']$', '', generated_name) # Remove surrounding quotes
generated_name = re.sub(r'^#+\s*', '', generated_name) # Remove markdown headers
generated_name = re.sub(r'^```.*?\n', '', generated_name, flags=re.DOTALL) # Remove code blocks start
generated_name = re.sub(r'\n.*?```$', '', generated_name, flags=re.DOTALL) # Remove code blocks end
# Take only the first line (in case AI added explanations)
generated_name = generated_name.split('\n')[0].strip()
# Remove ALL question marks, exclamation marks, and trailing periods
generated_name = re.sub(r'[?!]+', '', generated_name) # Remove all ? and !
generated_name = re.sub(r'\.+$', '', generated_name) # Remove trailing periods
generated_name = generated_name.strip()
# Ensure proper title case (capitalize first letter of each word, but handle acronyms)
if generated_name:
# Split into words and capitalize properly
words = generated_name.split()
capitalized_words = []
for word in words:
# Keep acronyms (all caps) as-is, otherwise capitalize first letter
if word.isupper() and len(word) > 1:
capitalized_words.append(word)
else:
# Capitalize first letter, lowercase the rest
capitalized_words.append(word.capitalize())
generated_name = " ".join(capitalized_words)
# Validate: should be a proper title, not just copied words
# Check if it's too similar to the original (just first words)
prompt_words = set(userPrompt.lower().split()[:6])
name_words = set(generated_name.lower().split())
similarity = len(prompt_words.intersection(name_words)) / max(len(prompt_words), 1)
# If too similar (more than 80% same words), it's probably just copied
if similarity > 0.8 and len(generated_name.split()) <= len(userPrompt.split()[:5]):
logger.warning(f"Generated name too similar to input, regenerating: {generated_name}")
# Try once more with a more explicit prompt
retry_prompt = f"""CRITICAL: Create a professional title in THE SAME LANGUAGE as the user's question.
Question: {truncated_prompt}
The title MUST be in the exact same language as the question above.
- If the question is in German, respond in German
- If the question is in French, respond in French
- If the question is in English, respond in English
Create a professional title (max 60 characters, no punctuation) that summarizes the topic.
Title (in the same language as the question):"""
# Ensure AI services are initialized (should already be, but be safe)
await services.ai.ensureAiObjectsInitialized()
retryRequest = AiCallRequest(
prompt=retry_prompt,
options=AiCallOptions(
resultFormat="txt",
operationType=OperationTypeEnum.DATA_GENERATE,
processingMode=ProcessingModeEnum.DETAILED,
temperature=0.8
)
)
retryResponse = await services.ai.callAi(retryRequest)
generated_name = retryResponse.content.strip()
generated_name = re.sub(r'^(Title|Titel|Titre|Name|Name:):\s*', '', generated_name, flags=re.IGNORECASE)
generated_name = re.sub(r'^["\']|["\']$', '', generated_name)
generated_name = generated_name.split('\n')[0].strip()
generated_name = re.sub(r'[?!]+', '', generated_name) # Remove all ? and !
generated_name = re.sub(r'\.+$', '', generated_name) # Remove trailing periods
# Apply title case
if generated_name:
words = generated_name.split()
capitalized_words = []
for word in words:
if word.isupper() and len(word) > 1:
capitalized_words.append(word)
else:
capitalized_words.append(word.capitalize())
generated_name = " ".join(capitalized_words).strip()
# Final validation and length check
if not generated_name or len(generated_name) < 3:
logger.error(f"Generated name too short or empty: '{generated_name}'")
# Use default title based on language
if userLanguage == "de":
generated_name = "Chatbot Konversation"
elif userLanguage == "fr":
generated_name = "Conversation Chatbot"
else:
generated_name = "Chatbot Conversation"
# Final cleanup: ensure no question marks or trailing punctuation
generated_name = re.sub(r'[?!]+', '', generated_name)
generated_name = re.sub(r'\.+$', '', generated_name).strip()
# Final length check and truncation
if len(generated_name) > 60:
# Try to truncate at word boundary
truncated = generated_name[:57]
last_space = truncated.rfind(' ')
if last_space > 30: # Only truncate at word boundary if reasonable
generated_name = truncated[:last_space] + "..."
else:
generated_name = truncated + "..."
logger.info(f"Generated conversation name: '{generated_name}' from prompt: '{userPrompt[:50]}...'")
return generated_name
except Exception as e:
logger.error(f"Error generating conversation name: {e}", exc_info=True)
# Use default title based on language
if userLanguage == "de":
return "Chatbot Konversation"
elif userLanguage == "fr":
return "Conversation Chatbot"
else:
return "Chatbot Conversation"
async def chatProcess(
currentUser: User,
userInput: UserInputRequest,
workflowId: Optional[str] = None
) -> ChatWorkflow:
"""
Simple chatbot processing - direct AI center implementation.
Flow:
1. Create or load workflow
2. Store user message
3. AI analyzes: determine if DB query/web research needed
4. Execute database query if needed
5. Execute web research if needed
6. Generate final answer
7. Store assistant message
8. Return workflow
Args:
currentUser: Current user
userInput: User input request
workflowId: Optional workflow ID to continue existing conversation
Returns:
ChatWorkflow instance
"""
try:
# Get services
services = getServices(currentUser, None)
interfaceDbChat = services.interfaceDbChat
# Get event manager and create queue if needed
event_manager = get_event_manager()
# Create or load workflow
if workflowId:
workflow = interfaceDbChat.getWorkflow(workflowId)
if not workflow:
raise ValueError(f"Workflow {workflowId} not found")
# Resume workflow: increment round number
new_round = workflow.currentRound + 1
interfaceDbChat.updateWorkflow(workflowId, {
"status": "running",
"currentRound": new_round,
"lastActivity": getUtcTimestamp()
})
workflow = interfaceDbChat.getWorkflow(workflowId)
logger.info(f"Resumed workflow {workflowId}, round incremented to {new_round}")
# Create event queue if it doesn't exist (for streaming)
if not event_manager.has_queue(workflowId):
event_manager.create_queue(workflowId)
else:
# Generate conversation name based on user's prompt
conversation_name = await _generate_conversation_name(
services,
userInput.prompt,
userInput.userLanguage
)
# Create new workflow
workflowData = {
"id": str(uuid.uuid4()),
"mandateId": currentUser.mandateId,
"status": "running",
"name": conversation_name,
"currentRound": 1,
"currentTask": 0,
"currentAction": 0,
"totalTasks": 0,
"totalActions": 0,
"workflowMode": WorkflowModeEnum.WORKFLOW_CHATBOT.value, # Use Chatbot mode for chatbot conversations
"startedAt": getUtcTimestamp(),
"lastActivity": getUtcTimestamp()
}
workflow = interfaceDbChat.createWorkflow(workflowData)
logger.info(f"Created new chatbot workflow: {workflow.id} with name: {conversation_name}")
# Create event queue for new workflow (for streaming)
event_manager.create_queue(workflow.id)
# Reload workflow to get current message count
workflow = interfaceDbChat.getWorkflow(workflow.id)
# Store user message
userMessageData = {
"id": f"msg_{uuid.uuid4()}",
"workflowId": workflow.id,
"message": userInput.prompt,
"role": "user",
"status": "first" if workflowId is None else "step",
"sequenceNr": len(workflow.messages) + 1,
"publishedAt": getUtcTimestamp(),
"roundNumber": workflow.currentRound,
"taskNumber": 0,
"actionNumber": 0
}
userMessage = interfaceDbChat.createMessage(userMessageData)
logger.info(f"Stored user message: {userMessage.id}")
# Emit message event for streaming (exact chatData format)
event_manager = get_event_manager()
message_timestamp = parseTimestamp(userMessage.publishedAt, default=getUtcTimestamp())
await event_manager.emit_event(
workflow.id,
"chatdata",
"New message",
"message",
{
"type": "message",
"createdAt": message_timestamp,
"item": userMessage.dict()
}
)
# Update workflow status
interfaceDbChat.updateWorkflow(workflow.id, {
"status": "running",
"lastActivity": getUtcTimestamp()
})
# Process in background (async)
asyncio.create_task(_processChatbotMessage(
services,
workflow.id,
userInput,
userMessage.id
))
# Reload workflow to include new message
workflow = interfaceDbChat.getWorkflow(workflow.id)
return workflow
except Exception as e:
logger.error(f"Error in chatProcess: {str(e)}", exc_info=True)
raise
async def _check_workflow_status(interfaceDbChat, workflowId: str, event_manager) -> bool:
"""
Check if workflow is stopped. If stopped, emit stopped event and return True.
Returns:
True if workflow is stopped, False otherwise
"""
workflow = interfaceDbChat.getWorkflow(workflowId)
if workflow and workflow.status == "stopped":
await event_manager.emit_event(
workflowId,
"stopped",
"Workflow stopped",
"stopped"
)
logger.info(f"Workflow {workflowId} was stopped, exiting processing")
return True
return False
async def _processChatbotMessage(
services,
workflowId: str,
userInput: UserInputRequest,
userMessageId: str
):
"""
Process chatbot message in background.
Executes the actual chatbot logic.
"""
event_manager = get_event_manager()
try:
interfaceDbChat = services.interfaceDbChat
# Reload workflow to get current messages
workflow = interfaceDbChat.getWorkflow(workflowId)
if not workflow:
logger.error(f"Workflow {workflowId} not found during processing")
await event_manager.emit_event(
workflowId,
"error",
f"Workflow {workflowId} nicht gefunden",
"error"
)
return
# Check if workflow was stopped
if await _check_workflow_status(interfaceDbChat, workflowId, event_manager):
return
# Helper function to process streaming messages from AI responses and create logs
def process_streaming_messages(ai_response_data: Dict[str, Any], progress: Optional[float] = None):
"""Process streaming messages from AI response and create logs."""
if not ai_response_data:
return
streaming_messages = ai_response_data.get("streamingMessages", [])
if isinstance(streaming_messages, list):
workflow = interfaceDbChat.getWorkflow(workflowId)
if workflow:
for msg in streaming_messages:
if isinstance(msg, str) and msg.strip():
try:
services.chat.storeLog(workflow, {
"message": msg.strip(),
"type": "info",
"status": "running",
"progress": progress if progress is not None else 0.5
})
except Exception as e:
logger.warning(f"Error creating log from streaming message: {e}")
# Build conversation context from history
context = ""
if workflow.messages:
recent_messages = workflow.messages[-5:] # Last 5 messages
context = "\n\nPrevious conversation:\n"
for msg in recent_messages:
if msg.role == "user":
context += f"User: {msg.message}\n"
elif msg.role == "assistant":
context += f"Assistant: {msg.message}\n"
# Ensure AI service is initialized
await services.ai.ensureAiObjectsInitialized()
# Get current date for prompts
current_date = datetime.datetime.now().strftime("%d.%m.%Y")
# Check if workflow was stopped before starting analysis
if await _check_workflow_status(interfaceDbChat, workflowId, event_manager):
return
# Step 1: AI Analysis - create detailed plan for all database queries needed
logger.info("Step 1: Analyzing user input and creating query plan...")
await event_manager.emit_event(
workflowId,
"status",
"Analysiere Benutzeranfrage und erstelle Abfrageplan...",
"analysis"
)
analysisPrompt = f"""Heute ist der {current_date}.
Du bist ein Chatbot der Althaus AG.
Du hast Zugriff auf ein SQL query tool, dass es dir ermöglicht, SQL SELECT Abfragen auf der Althaus AG Datenbank auszuführen.
Du kannst dem Nutzer bei allen Aufgaben helfen, die du mit SQL Abfragen erledigen kannst.
DATENBANK-INFORMATIONEN:
- Datenbankdatei: /data/database.db (SQLite)
- Tabellen: Artikel, Einkaufspreis, Lagerplatz_Artikel
Die Datenbank besteht aus drei Tabellen, die über Beziehungen verbunden sind:
- **Artikel**: Enthält alle Produktinformationen (I_ID, Artikelbezeichnung, Artikelnummer, etc.)
- **Einkaufspreis**: Enthält Preisdaten (m_Artikel, EP_CHF)
- **Lagerplatz_Artikel**: Enthält Lagerbestands- und Lagerplatzinformationen (R_ARTIKEL, R_LAGERPLATZ, S_IST_BESTAND, etc.)
- **Beziehungen**:
- Artikel.I_ID = Einkaufspreis.m_Artikel
- Artikel.I_ID = Lagerplatz_Artikel.R_ARTIKEL
TABELLEN-SCHEMA (WICHTIG - Spalten mit Leerzeichen/Sonderzeichen IMMER in doppelte Anführungszeichen setzen):
Tabelle 1: Artikel
CREATE TABLE Artikel (
"I_ID" INTEGER PRIMARY KEY,
"Artikelbeschrieb" TEXT,
"Artikelbezeichnung" TEXT,
"Artikelgruppe" TEXT,
"Artikelkategorie" TEXT,
"Artikelkürzel" TEXT,
"Artikelnummer" TEXT,
"Einheit" TEXT,
"Gesperrt" TEXT,
"Keywords" TEXT,
"Lieferant" TEXT,
"Warengruppe" TEXT
)
Tabelle 2: Einkaufspreis
CREATE TABLE Einkaufspreis (
"m_Artikel" INTEGER,
"EP_CHF" FLOAT
)
Tabelle 3: Lagerplatz_Artikel
CREATE TABLE Lagerplatz_Artikel (
"R_ARTIKEL" INTEGER,
"R_LAGERPLATZ" TEXT,
"S_BESTELLTER_BESTAND" INTEGER,
"S_IST_BESTAND" TEXT,
"S_MAXIMALBESTAND" INTEGER,
"S_MINDESTBESTAND" INTEGER,
"S_RESERVIERTER_BESTAND" INTEGER,
"S_SOLL_BESTAND" INTEGER
)
SQL-HINWEISE:
- Verwende IMMER doppelte Anführungszeichen für Spaltennamen: "Artikelkürzel", "Artikelnummer", etc.
- Für Textsuche verwende LIKE mit Wildcards: WHERE a."Artikelbezeichnung" LIKE '%suchbegriff%'
- Für Preisabfragen: Nutze JOINs um auf e."EP_CHF" zuzugreifen
- Für Lagerbestände: Nutze JOINs um auf l."S_IST_BESTAND", l."S_SOLL_BESTAND", etc. zuzugreifen
- WICHTIG bei S_IST_BESTAND: Dieser Wert kann "Unbekannt" sein (TEXT), nicht nur Zahlen! Prüfe mit WHERE l."S_IST_BESTAND" != 'Unbekannt' wenn du nur numerische Werte willst
- Verwende Tabellenaliase (a für Artikel, e für Einkaufspreis, l für Lagerplatz_Artikel) für bessere Lesbarkeit
SQL-AGGREGATIONEN:
Du kannst SQL-Aggregationsfunktionen verwenden, um statistische Auswertungen und Zusammenfassungen zu erstellen:
- COUNT() - Anzahl zählen
- SUM() - Summe berechnen (z.B. SUM(CASE WHEN l."S_IST_BESTAND" != 'Unbekannt' THEN CAST(l."S_IST_BESTAND" AS INTEGER) ELSE 0 END))
- AVG() - Durchschnitt
- MIN() / MAX() - Minimum/Maximum
- GROUP BY - Gruppierung
STREAMING-UPDATES:
WICHTIG: Du kannst mehrere Tools parallel aufrufen! Wenn es sinnvoll ist, kannst du:
- Mehrere SQL-Abfragen gleichzeitig ausführen (z.B. verschiedene Suchkriterien parallel abfragen)
- SQL-Abfragen und Tavily-Suchen kombinieren (z.B. Artikel in der DB finden UND gleichzeitig im Internet nach Produktinformationen suchen)
- Verschiedene Analysen parallel durchführen
Nutze diese Parallelisierung, um effizienter zu arbeiten und dem Nutzer schneller umfassende Antworten zu geben.
Du hast Zugriff auf das Tool "send_streaming_message", mit dem du dem Nutzer kurze Status-Updates senden kannst, während du an seiner Anfrage arbeitest. Nutze dieses Tool, um den Nutzer über deine aktuellen Aktivitäten zu informieren. Du kannst es parallel zu anderen Tools aufrufen.
Beispiele für Status-Updates:
- "Analysiere Benutzeranfrage..."
- "Bestimme benötigte Datenbankabfragen..."
- "Identifiziere relevante Suchbegriffe..."
- "Erstelle Abfrageplan mit allen benötigten Queries..."
- "Durchsuche Datenbank nach Lampen, LED, Leuchten, und Ähnlichem.."
- "Suche im Internet nach Produktinformationen zu [Produktname].."
- "Analysiere Suchergebnisse und bereite Antwort vor.."
- "Führe erweiterte Datenbankabfrage durch.."
Sende diese Updates sehr sehr häufig, damit der Nutzer weiss, was du gerade machst. Es ist ganz wichtig, dass du den Nutzer so oft es geht auf dem Laufenden hältst.
Die Beispiele oben sind nur Beispiele. Wenn möglich, sei spezifischer und kreativer, damit der Nutzer genau weiss, was du gerade tust.
Falls es möglich ist, gib in den Status-Updates auch schon Zwischenergebnisse an, z.B. "Habe 20 Artikel gefunden, suche weiter nach ähnlichen Begriffen".
Du kannst auch gerne deinen Denkenprozess in den Status-Updates beschreiben, z.B. "Überlege, welche Suchbegriffe ich noch verwenden könnte".
Es ist super wichtig, dass wir dem Nutzer laufend Updates geben, damit er nicht das Gefühl hat, dass er zu lange warten muss.
Wichtig: Sende auch eine Status-Update, wenn du die Zusammenfassende Antwort an den Nutzer schreibst, z.B. "Formuliere finale Antwort mit übersichtlicher Tabelle..".
Analyze the user's question: {userInput.prompt}{context}
Determine what actions are needed to answer this question. Return ONLY a valid JSON object:
{{
"needsDatabaseQuery": boolean,
"needsWebResearch": boolean,
"sqlQuery": string (if needsDatabaseQuery is true, generate the SQL query with correct column names using double quotes),
"reasoning": string,
"streamingMessages": array of strings (status updates to send to user via send_streaming_message tool, e.g. ["Analysiere Benutzeranfrage...", "Bestimme benötigte Datenbankabfragen..."])
}}
WICHTIG für SQL-Queries:
- Verwende IMMER doppelte Anführungszeichen für Spaltennamen
- Für Lagerbestand: Verwende l."S_IST_BESTAND" (NICHT "Bestände"!)
- Bei Aggregationen mit S_IST_BESTAND: SUM(CASE WHEN l."S_IST_BESTAND" != 'Unbekannt' THEN CAST(l."S_IST_BESTAND" AS INTEGER) ELSE 0 END)
Only use SELECT queries for database. Return valid JSON only."""
analysisRequest = AiCallRequest(
prompt=analysisPrompt,
options=AiCallOptions(
resultFormat="json",
operationType=OperationTypeEnum.DATA_ANALYSE,
processingMode=ProcessingModeEnum.BASIC
)
)
analysisResponse = await services.ai.callAi(analysisRequest)
# Log raw response for debugging
logger.debug(f"Raw AI analysis response: {analysisResponse.content[:500]}")
# Parse analysis result with improved JSON extraction
analysis = _extractJsonFromResponse(analysisResponse.content)
# Process streaming messages from AI (create logs)
process_streaming_messages(analysis, progress=0.1)
if analysis:
needsDatabaseQuery = analysis.get("needsDatabaseQuery", False)
needsWebResearch = analysis.get("needsWebResearch", False)
queryPlan = analysis.get("queryPlan", {})
reasoning = analysis.get("reasoning", "")
else:
# JSON parsing failed - fallback
logger.warning("Failed to parse analysis JSON, using fallback")
question_lower = userInput.prompt.lower()
db_keywords = ["stock", "lager", "bestand", "artikel", "preis", "price", "wie viele", "how many"]
needsDatabaseQuery = any(keyword in question_lower for keyword in db_keywords)
needsWebResearch = False
queryPlan = {}
reasoning = "Failed to parse analysis"
logger.info(f"Analysis result: DB={needsDatabaseQuery}, Web={needsWebResearch}, Plan keys={list(queryPlan.keys()) if queryPlan else 'None'}...")
await event_manager.emit_event(
workflowId,
"progress",
f"Analyse abgeschlossen: {len(queryPlan.keys()) if queryPlan else 0} Abfrage-Kategorien identifiziert",
"analysis",
{"needsDatabaseQuery": needsDatabaseQuery, "needsWebResearch": needsWebResearch}
)
# Check if workflow was stopped after analysis
if await _check_workflow_status(interfaceDbChat, workflowId, event_manager):
return
# Step 2: Generate all SQL queries based on the plan
allQueries = {} # Store all queries to execute
queryResults = {} # Store results from all queries
if needsDatabaseQuery and queryPlan:
logger.info("Step 2: Generating all SQL queries from plan...")
# Store log: Query generation started
services.chat.storeLog(workflow, {
"message": "Generiere alle SQL-Abfragen basierend auf dem Plan...",
"type": "info",
"status": "running",
"progress": 0.3
})
await event_manager.emit_event(
workflowId,
"status",
"Generiere alle SQL-Abfragen basierend auf dem Plan...",
"query_generation"
)
# Build query generation prompt
query_generation_prompt = f"""Based on this query plan: {json.dumps(queryPlan, indent=2, ensure_ascii=False)}
And the user question: {userInput.prompt}{context}
Generate ALL SQL queries needed. Return ONLY a valid JSON object with all queries:
{{
"mainQuery": "SQL query for main query",
"statisticsQueries": {{
"query1": "SQL query description and SQL",
"query2": "SQL query description and SQL"
}},
"supplierQueries": {{
"query1": "SQL query description and SQL"
}},
"articleQueries": {{
"query1": "SQL query description and SQL"
}},
"additionalQueries": {{
"query1": "SQL query description and SQL"
}},
"streamingMessages": ["array of status updates via send_streaming_message tool"]
}}
STREAMING-UPDATES:
Während du die Queries generierst, denke an hilfreiche Status-Updates:
- "Generiere Hauptabfrage..."
- "Erstelle Statistik-Abfragen..."
- "Vorbereite Lieferanten-Analyse-Abfrage..."
- "Erstelle Top-20-Artikel-Abfrage..."
- "Generiere zusätzliche relevante Abfragen..."
IMPORTANT:
- Use double quotes for all column names
- For stock calculations: Use SUM(CASE WHEN l."S_IST_BESTAND" != 'Unbekannt' THEN CAST(l."S_IST_BESTAND" AS INTEGER) ELSE 0 END)
- For top articles: ORDER BY CAST(l."S_IST_BESTAND" AS INTEGER) DESC LIMIT 20
- Extract search terms from user question (e.g., "LED", "Lampe", etc.) and use in WHERE clauses
- All queries should use consistent search criteria
Return ONLY valid JSON, no other text."""
query_generation_request = AiCallRequest(
prompt=query_generation_prompt,
options=AiCallOptions(
resultFormat="json",
operationType=OperationTypeEnum.DATA_ANALYSE,
processingMode=ProcessingModeEnum.BASIC
)
)
query_generation_response = await services.ai.callAi(query_generation_request)
queries_data = _extractJsonFromResponse(query_generation_response.content)
# Process streaming messages from AI (create logs)
process_streaming_messages(queries_data, progress=0.3)
if queries_data:
# Collect all queries
if queries_data.get("mainQuery"):
allQueries["main"] = queries_data["mainQuery"]
# Collect statistics queries
if queries_data.get("statisticsQueries"):
for key, value in queries_data["statisticsQueries"].items():
if isinstance(value, str):
allQueries[f"stat_{key}"] = value
elif isinstance(value, dict) and "sql" in value:
allQueries[f"stat_{key}"] = value["sql"]
# Collect supplier queries
if queries_data.get("supplierQueries"):
for key, value in queries_data["supplierQueries"].items():
if isinstance(value, str):
allQueries[f"supplier_{key}"] = value
elif isinstance(value, dict) and "sql" in value:
allQueries[f"supplier_{key}"] = value["sql"]
# Collect article queries
if queries_data.get("articleQueries"):
for key, value in queries_data["articleQueries"].items():
if isinstance(value, str):
allQueries[f"article_{key}"] = value
elif isinstance(value, dict) and "sql" in value:
allQueries[f"article_{key}"] = value["sql"]
# Collect additional queries
if queries_data.get("additionalQueries"):
for key, value in queries_data["additionalQueries"].items():
if isinstance(value, str):
allQueries[f"additional_{key}"] = value
elif isinstance(value, dict) and "sql" in value:
allQueries[f"additional_{key}"] = value["sql"]
logger.info(f"Generated {len(allQueries)} queries from plan")
await event_manager.emit_event(
workflowId,
"progress",
f"{len(allQueries)} SQL-Abfragen erfolgreich generiert",
"query_generation",
{"queryCount": len(allQueries), "queries": list(allQueries.keys())}
)
else:
logger.warning("Failed to generate queries from plan")
await event_manager.emit_event(
workflowId,
"error",
"Fehler beim Generieren der SQL-Abfragen",
"query_generation"
)
# Check if workflow was stopped before query execution
if await _check_workflow_status(interfaceDbChat, workflowId, event_manager):
return
# Step 3: Execute all queries in parallel
if allQueries:
logger.info(f"Step 3: Executing {len(allQueries)} queries in parallel...")
await event_manager.emit_event(
workflowId,
"status",
f"Führe {len(allQueries)} Datenbankabfragen parallel aus...",
"query_execution"
)
try:
connector = PreprocessorConnector()
async def execute_query(query_key, query_sql):
"""Execute a single query and return result."""
try:
result = await connector.executeQuery(query_sql)
if not result.startswith(("Error:", "Query failed:", "Network error:", "API error:")):
return (query_key, result)
else:
logger.warning(f"{query_key} query returned error: {result[:100]}")
return (query_key, None)
except Exception as e:
logger.warning(f"{query_key} query failed: {e}")
return (query_key, None)
# Execute all queries in parallel
tasks = [execute_query(key, sql) for key, sql in allQueries.items()]
results = await asyncio.gather(*tasks)
# Process results
successful_queries = 0
for query_key, result in results:
if result is not None:
queryResults[query_key] = result
successful_queries += 1
logger.info(f"{query_key} query executed successfully")
await event_manager.emit_event(
workflowId,
"progress",
f"Abfrage '{query_key}' erfolgreich ausgeführt",
"query_execution",
{"queryKey": query_key}
)
await event_manager.emit_event(
workflowId,
"progress",
f"{successful_queries} von {len(allQueries)} Abfragen erfolgreich ausgeführt",
"query_execution",
{"successful": successful_queries, "total": len(allQueries)}
)
await connector.close()
except Exception as e:
logger.error(f"Error executing queries: {e}")
queryResults["error"] = f"Error executing queries: {str(e)}"
await event_manager.emit_event(
workflowId,
"error",
f"Fehler beim Ausführen der Abfragen: {str(e)}",
"query_execution"
)
# Check if workflow was stopped after query execution
if await _check_workflow_status(interfaceDbChat, workflowId, event_manager):
return
# Step 3: Execute web research if needed
webResearchResults = ""
if needsWebResearch:
logger.info("Step 3: Performing web research...")
try:
researchResult = await services.web.performWebResearch(
prompt=userInput.prompt,
urls=[],
country=None,
language=userInput.userLanguage or "de",
researchDepth="general",
operationId=None
)
# Extract text from research result
if isinstance(researchResult, dict):
webResearchResults = json.dumps(researchResult, ensure_ascii=False, indent=2)
else:
webResearchResults = str(researchResult)
logger.info("Web research completed successfully")
await event_manager.emit_event(
workflowId,
"progress",
"Internet-Recherche abgeschlossen",
"web_research"
)
except Exception as e:
logger.error(f"Web research failed: {e}")
webResearchResults = f"Web research error: {str(e)}"
await event_manager.emit_event(
workflowId,
"error",
f"Fehler bei Internet-Recherche: {str(e)}",
"web_research"
)
# Check if workflow was stopped before answer generation
if await _check_workflow_status(interfaceDbChat, workflowId, event_manager):
return
# Step 4: Generate final answer
logger.info("Step 4: Generating final answer from all query results...")
await event_manager.emit_event(
workflowId,
"status",
"Formuliere finale Antwort mit allen Ergebnissen...",
"answer_generation"
)
# Build context for final answer with all query results
answerContext = f"User question: {userInput.prompt}{context}\n\n"
if queryResults:
answerContext += "Database query results (all queries executed in parallel):\n\n"
for query_key, result in queryResults.items():
if query_key != "error":
answerContext += f"{query_key} results:\n{result}\n\n"
if "error" in queryResults:
answerContext += f"Errors: {queryResults['error']}\n\n"
if webResearchResults:
answerContext += f"Web research results:\n{webResearchResults}\n\n"
answerPrompt = f"""Heute ist der {current_date}.
Du bist ein Chatbot der Althaus AG.
Du hast Zugriff auf ein SQL query tool, dass es dir ermöglicht, SQL SELECT Abfragen auf der Althaus AG Datenbank auszuführen.
KRITISCH - AUSFÜHRLICHE ANTWORTEN ERFORDERLICH:
Du MUSST immer sehr ausführliche, detaillierte und strukturierte Antworten geben. Einfache, kurze Antworten sind NICHT ausreichend!
ANTWORT-STRUKTUR FÜR DATENBANK-ABFRAGEN:
Wenn du Datenbank-Ergebnisse präsentierst, MUSS deine Antwort folgende Struktur haben:
1. EINLEITUNG MIT QUELLENANGABE:
- Beginne IMMER mit: "Aus der Datenbank habe ich eine umfassende Analyse [des Themas] durchgeführt:"
- Oder: "Aus der Datenbank habe ich folgende Informationen gefunden:"
2. ÜBERSCHRIFT MIT THEMA:
- Gib eine klare Überschrift, z.B. "LED-Lagerbestand Übersicht" oder "Artikel-Übersicht: [Suchbegriff]"
3. ZUSAMMENFASSUNG MIT STATISTIKEN:
- Gib IMMER eine detaillierte Zusammenfassung mit wichtigen Kennzahlen
- Beispiele:
* "Gesamtbestand LEDs: 7'411 Stück verteilt auf 157 verschiedene Artikel"
* "1'027 LED-Artikel insgesamt in der Datenbank"
* "157 Artikel haben aktuell Lagerbestand > 0"
* "273 Artikel ohne Lagerinformationen"
- Formatiere Zahlen mit Tausender-Trennzeichen (z.B. 7'411 statt 7411)
- Verwende Aufzählungen für bessere Lesbarkeit
4. TOP-LIEFERANTEN TABELLE (wenn relevant):
- Wenn mehrere Lieferanten vorhanden sind, erstelle IMMER eine Tabelle mit Top-Lieferanten
- Spalten: Lieferant, Anzahl Artikel, Gesamtbestand (oder andere relevante Metriken)
- Sortiere nach Gesamtbestand oder Anzahl Artikel (absteigend)
- Zeige mindestens Top 10 Lieferanten
5. TOP-ARTIKEL TABELLE:
- Erstelle IMMER eine Tabelle mit den Top 20 Artikeln
- Spalten sollten enthalten: Artikelnummer (als Link), Artikelbezeichnung, Lieferant, Lagerplatz, Ist-Bestand, Soll-Bestand (wenn verfügbar)
- Sortiere nach Ist-Bestand (absteigend) oder nach Relevanz
- Formatiere Zahlen mit Tausender-Trennzeichen
6. ZUSÄTZLICHE ANALYSEN:
- Füge immer eine kurze Analyse hinzu, z.B.:
* "Phoenix Contact AG dominiert klar mit über 68% des gesamten LED-Lagerbestands"
* "Die meisten Artikel sind Beschriftungsmarker und -schilder"
- Erkläre interessante Muster oder Auffälligkeiten
7. HINWEIS BEI MEHR ERGEBNISSEN:
- Wenn mehr als 20 Artikel existieren, füge IMMER hinzu:
* "_Es existieren weitere X Artikel. Die Tabelle zeigt die 20 Artikel mit dem höchsten Bestand._"
- Oder: "_Es wurden insgesamt X Artikel gefunden. Die Tabelle zeigt die ersten 20._"
8. NÄCHSTE SCHRITTE - MEHRERE VORSCHLÄGE:
- Gib IMMER mindestens 5 verschiedene Vorschläge für nächste Schritte
- Formatiere als Aufzählung mit Bullet Points
- Beispiele:
* "Details zu einem bestimmten LED-Artikel erfahren?"
* "LED-Artikel mit niedrigem Lagerbestand oder unter Mindestbestand anzeigen?"
* "Nach spezifischen LED-Typen suchen (z.B. Signalleuchten, Anzeige-LEDs, LED-Strips)?"
* "Preisinformationen zu den LED-Artikeln abrufen?"
* "LED-Artikel eines bestimmten Lieferanten detailliert anzeigen?"
- Passe die Vorschläge an den Kontext an
TABELLEN-FORMATIERUNG:
- Verwende IMMER Markdown-Tabellen für strukturierte Daten
- Spalten sollten klar getrennt sein mit |
- Header-Zeile sollte deutlich hervorgehoben sein
- Zahlen sollten rechtsbündig sein (in Markdown mit Leerzeichen)
- Verwende Tausender-Trennzeichen (z.B. 1'090 statt 1090)
QUELLENANGABE - DATENBANK:
WICHTIG: Wenn du Informationen aus der Datenbank präsentierst, kennzeichne dies IMMER klar für den Nutzer.
- Beginne deine Antwort mit einer klaren Kennzeichnung, z.B.: "Aus der Datenbank habe ich eine umfassende Analyse durchgeführt:"
- Bei kombinierten Informationen (Datenbank + Internet): Trenne klar zwischen beiden Quellen
QUELLENANGABE - INTERNET:
WICHTIG: Wenn du Informationen aus dem Internet präsentierst, kennzeichne dies IMMER klar für den Nutzer.
- Beginne Internet-Recherchen mit: "Aus meiner Internet-Recherche:" oder "Laut Online-Quellen:"
- Gib IMMER die konkreten Quellen an (Website-Namen und Links)
- Bei mehreren Quellen: Liste die Quellen auf und verweise darauf
- Trenne klar zwischen Datenbank-Informationen und Internet-Recherchen
TABELLENLÄNGE UND ARTIKELANZAHL - KRITISCH:
WICHTIG: Zeige MAXIMAL 20 Artikel in Tabellen. Du darfst und sollst aber ausführliche Erklärungen liefern!
ZAHLEN-PRÜFUNG - ABSOLUT KRITISCH:
BEVOR du deine finale Antwort zurückgibst, MUSST du diese Schritte befolgen:
1. ZÄHLE die TATSÄCHLICHEN Zeilen in deiner finalen Tabelle
2. Diese Zahl ist die EINZIGE korrekte Anzahl für deine Antwort
3. Verwende diese Zahl KONSISTENT überall in deiner Antwort
4. Formatiere Zahlen mit Tausender-Trennzeichen (z.B. 7'411 statt 7411)
Wenn immer du eine Artikelnummer innerhalb einer Tabelle zurückgibst bitte markiere diese als Markdownlink:
[ARTIKELNUMMER](/details/ARTIKELNUMMER). ARTIKELNUMMER ist hierbei der Platzhalter, den du ersetzen musst.
WICHTIG! Du musst im Link die ARTIKELNUMMER sicher URL-encodieren. Encodiere aber NICHT die Artikelnummer in eckigen Klammern. Also encodiere den Ankertext nicht!
Ausserhalb einer Tabelle musst du keine Links auf Artikelnummern setzen.
Du antwortest ausschliesslich auf Deutsch. Nutze kein sz(ß) sondern immer ss.
STREAMING-UPDATES:
Während du die Antwort erstellst, denke daran, dass der Nutzer Updates erhalten sollte:
- "Kompliliere Statistiken..."
- "Erstelle Lieferanten-Tabelle..."
- "Formatiere Top-20-Artikel..."
- "Formuliere finale Antwort mit übersichtlicher Tabelle..."
- "Füge zusätzliche Analysen hinzu..."
Sende diese Updates sehr häufig, damit der Nutzer weiss, was du gerade machst. Es ist ganz wichtig, dass du den Nutzer so oft es geht auf dem Laufenden hältst.
Die Beispiele oben sind nur Beispiele. Wenn möglich, sei spezifischer und kreativer, damit der Nutzer genau weiss, was du gerade tust.
Falls es möglich ist, gib in den Status-Updates auch schon Zwischenergebnisse an, z.B. "Habe 20 Artikel gefunden, suche weiter nach ähnlichen Begriffen".
Du kannst auch gerne deinen Denkenprozess in den Status-Updates beschreiben, z.B. "Überlege, welche Suchbegriffe ich noch verwenden könnte".
Es ist super wichtig, dass wir dem Nutzer laufend Updates geben, damit er nicht das Gefühl hat, dass er zu lange warten muss.
Wichtig: Sende auch eine Status-Update, wenn du die Zusammenfassende Antwort an den Nutzer schreibst, z.B. "Formuliere finale Antwort mit übersichtlicher Tabelle..".
Answer the user's question in German: {userInput.prompt}{context}
{answerContext}
KRITISCH: Du hast jetzt ZUSÄTZLICHE Datenbankabfragen erhalten, die dir detaillierte Informationen liefern:
- "Total count query results" - Gesamtanzahl der Artikel
- "Top suppliers query results" - Top-Lieferanten mit Artikelanzahl und Gesamtbestand
- "Top 20 articles query results" - Top 20 Artikel mit allen Details
VERWENDE ALLE DIESE DATEN für deine Antwort! Erstelle eine umfassende Antwort mit:
1. EINLEITUNG: "Aus der Datenbank habe ich eine umfassende Analyse [des Themas] durchgeführt:"
2. ÜBERSCHRIFT: z.B. "LED-Lagerbestand Übersicht"
3. ZUSAMMENFASSUNG MIT STATISTIKEN:
- Verwende die Daten aus "Total count query results" für Gesamtanzahl
- Verwende die Daten aus "Initial database query results" für Gesamtbestand
- Berechne weitere Kennzahlen basierend auf den Daten
4. TOP-LIEFERANTEN TABELLE:
- Verwende IMMER die Daten aus "Top suppliers query results"
- Erstelle eine Tabelle mit: Lieferant | Anzahl Artikel | Gesamtbestand
- Sortiere nach Gesamtbestand (absteigend)
- Zeige mindestens Top 10
5. TOP 20 ARTIKEL TABELLE:
- Verwende IMMER die Daten aus "Top 20 articles query results"
- Erstelle eine Tabelle mit: Artikelnummer (als Link) | Artikelbezeichnung | Lieferant | Lagerplatz | Ist-Bestand | Soll-Bestand
- Formatiere Zahlen mit Tausender-Trennzeichen
6. ZUSÄTZLICHE ANALYSEN:
- Analysiere die Daten und gib interessante Erkenntnisse
- z.B. "Phoenix Contact AG dominiert klar mit über 68% des gesamten Lagerbestands"
7. HINWEIS BEI MEHR ERGEBNISSEN:
- Wenn mehr als 20 Artikel existieren: "_Es existieren weitere X Artikel. Die Tabelle zeigt die 20 Artikel mit dem höchsten Bestand._"
8. NÄCHSTE SCHRITTE:
- Gib mindestens 5 verschiedene Vorschläge als Aufzählung
WICHTIG:
- Verwende ALLE verfügbaren Daten aus den zusätzlichen Abfragen
- Erstelle Tabellen aus den Daten - nicht nur Text!
- Formatiere Zahlen mit Tausender-Trennzeichen (z.B. 7'411 statt 7411)
- Sei sehr ausführlich und detailliert - kurze Antworten sind NICHT ausreichend!"""
answerRequest = AiCallRequest(
prompt=answerPrompt,
context=answerContext if (queryResults or webResearchResults) else None,
options=AiCallOptions(
resultFormat="txt",
operationType=OperationTypeEnum.DATA_ANALYSE,
processingMode=ProcessingModeEnum.BASIC
)
)
answerResponse = await services.ai.callAi(answerRequest)
finalAnswer = answerResponse.content
logger.info("Final answer generated successfully")
# Store log: Answer generation completed
services.chat.storeLog(workflow, {
"message": "Antwort erfolgreich generiert",
"type": "info",
"status": "running",
"progress": 0.9
})
await event_manager.emit_event(
workflowId,
"progress",
"Antwort erfolgreich generiert",
"answer_generation"
)
# Check if workflow was stopped after answer generation
if await _check_workflow_status(interfaceDbChat, workflowId, event_manager):
return
# Step 5: Store assistant message
# Reload workflow to get updated message count
workflow = interfaceDbChat.getWorkflow(workflowId)
assistantMessageData = {
"id": f"msg_{uuid.uuid4()}",
"workflowId": workflowId,
"parentMessageId": userMessageId,
"message": finalAnswer,
"role": "assistant",
"status": "last",
"sequenceNr": len(workflow.messages) + 1,
"publishedAt": getUtcTimestamp(),
"success": True,
"roundNumber": workflow.currentRound,
"taskNumber": 0,
"actionNumber": 0
}
assistantMessage = interfaceDbChat.createMessage(assistantMessageData)
logger.info(f"Stored assistant message: {assistantMessage.id}")
# Emit message event for streaming (exact chatData format)
message_timestamp = parseTimestamp(assistantMessage.publishedAt, default=getUtcTimestamp())
await event_manager.emit_event(
workflowId,
"chatdata",
"New message",
"message",
{
"type": "message",
"createdAt": message_timestamp,
"item": assistantMessage.dict()
}
)
# Update workflow status to completed
interfaceDbChat.updateWorkflow(workflowId, {
"status": "completed",
"lastActivity": getUtcTimestamp()
})
logger.info(f"Chatbot processing completed for workflow {workflowId}")
# Emit completion event
await event_manager.emit_event(
workflowId,
"complete",
"Chatbot-Verarbeitung abgeschlossen",
"complete",
{"workflowId": workflowId}
)
# Schedule cleanup
await event_manager.cleanup(workflowId)
except Exception as e:
logger.error(f"Error processing chatbot message: {str(e)}", exc_info=True)
# Store error message
try:
# Reload workflow to get current message count
workflow = interfaceDbChat.getWorkflow(workflowId)
errorMessageData = {
"id": f"msg_{uuid.uuid4()}",
"workflowId": workflowId,
"parentMessageId": userMessageId,
"message": f"Sorry, I encountered an error: {str(e)}",
"role": "assistant",
"status": "last",
"sequenceNr": len(workflow.messages) + 1,
"publishedAt": getUtcTimestamp(),
"success": False,
"roundNumber": workflow.currentRound if workflow else 1,
"taskNumber": 0,
"actionNumber": 0
}
errorMessage = interfaceDbChat.createMessage(errorMessageData)
# Emit message event for streaming (exact chatData format)
message_timestamp = parseTimestamp(errorMessage.publishedAt, default=getUtcTimestamp())
await event_manager.emit_event(
workflowId,
"chatdata",
"New message",
"message",
{
"type": "message",
"createdAt": message_timestamp,
"item": errorMessage.dict()
}
)
# Update workflow status to error
interfaceDbChat.updateWorkflow(workflowId, {
"status": "error",
"lastActivity": getUtcTimestamp()
})
# Schedule cleanup
await event_manager.cleanup(workflowId)
except Exception as storeError:
logger.error(f"Error storing error message: {storeError}")