2026-05-26 06:31:02 +02:00

145 lines
5.6 KiB
Python

"""Manages the chat history and communication with the AI model API."""
import os
from dotenv import load_dotenv
import requests
import json
from backend.managers.debug_logger import get_logger
logger = get_logger(__name__)
load_dotenv()
class ChatManager:
"""Handles sending messages and maintaining conversation history.
Connects to an OpenAI-compatible REST endpoint configured via environment
variables. All messages (user, assistant, system) are kept in memory so
the full conversation is sent with every request.
"""
def __init__(self):
self.api_host = os.getenv("HOST")
self.api_port = os.getenv("PORT")
self.api_key = os.getenv("API_KEY")
self.model = os.getenv("MODEL")
self.max_tokens = 2000
# API endpoint URL (OpenAI-compatible format)
self.api_url = f"http://{self.api_host}:{self.api_port}/v1/chat/completions"
# Chat history stored in memory
self.chat_history = []
def add_message(self, role: str, content: str) -> None:
"""Append a single message to the conversation history."""
self.chat_history.append({"role": role, "content": content})
def get_history(self) -> list:
"""Return a copy of the conversation history."""
return list(self.chat_history)
# REVIEW: dead code — clear_history() is never called anywhere in the codebase.
def clear_history(self) -> None:
"""Wipe the conversation history (starts a fresh chat)."""
logger.info("Chat history was cleared")
self.chat_history = []
def send_message(self, user_message: str) -> str:
"""Send a user message to the AI and return its reply.
Adds the user message to history, calls the API with the full history
as context, and appends the AI reply to history before returning it.
"""
# Add user message to history
self.add_message("user", user_message)
logger.info("Sending message to LLM API")
# Prepare request to OpenAI-compatible API
headers = {
"Content-Type": "application/json",
}
# Add API key if available
if self.api_key and self.api_key != "EMPTY":
headers["Authorization"] = f"Bearer {self.api_key}"
# Full history is sent so the model has multi-turn conversation context
payload = {
"model": self.model,
"messages": self.chat_history,
"temperature": 0.7,
"max_tokens": 2000,
"stream": False,
}
try:
# Make API request
response = requests.post(
self.api_url, headers=headers, json=payload, timeout=30
)
# Check if request was successful
if response.status_code != 200:
logger.warning("API HTTP status error %s: %s", response.status_code, response.text)
raise Exception(f"API Error {response.status_code}")
logger.info("Response recieved from API")
except requests.exceptions.Timeout as e:
error_msg = f"Timeout Error: {str(e)}"
self.add_message("assistant", f"Error: {error_msg}")
logger.exception("LLM API timeout: %s", e)
raise RuntimeError("LLM API timeout") from e
except requests.exceptions.RequestException as e:
error_msg = f"Connection Error: {str(e)}"
self.add_message("assistant", f"Error: {error_msg}")
logger.exception("LLM API connection failed: %s", e)
raise RuntimeError("Connection Error: LLM API connection failed") from e
return self.receive_response(response)
def receive_response(self, response) -> str:
"""Parse an API response object and return the AI reply text.
Extracts the message content from the JSON body, appends it to history,
and returns it. Raises on malformed JSON or unexpected response shape.
"""
try:
response_data = response.json()
except json.JSONDecodeError as e:
error_msg = f"JSON Decode Error: {str(e)}"
self.add_message("assistant", f"Error: {error_msg}")
logger.exception("JSON Decode Error: %s", e)
raise Exception(error_msg)
if "choices" not in response_data or not response_data["choices"]:
logger.warning("Invalid API response format: %s", response_data)
self.add_message("assistant", "Error: Invalid API response format")
raise Exception("Invalid API response format")
try:
ai_message = response_data["choices"][0]["message"]["content"]
self.add_message("assistant", ai_message)
logger.info("Assistant response generated")
return ai_message
except Exception as e:
error_msg = f"Error: {str(e)}"
self.add_message("assistant", f"Error: {error_msg}")
logger.exception("JSON parsing and message formatting failed: %s", e)
raise RuntimeError("JSON parsing and message formatting failed") from e
# REVIEW: dead code — get_chat_display() is never called anywhere in the codebase.
# The UI renders st.session_state.chat_history directly. This method also does the
# same thing as get_history() (returns a copy of chat_history with the same fields),
# making it redundant even if it were used.
def get_chat_display(self) -> list:
"""Return a copy of the history suitable for display in the UI."""
return [
{"role": msg["role"], "content": msg["content"]}
for msg in self.chat_history
]