1352 lines
58 KiB
Python
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}")
|
|
|