feat: Update Ollama configuration and enhance LLM initialization with error handling
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run

This commit is contained in:
Ireneusz Bachanowicz 2025-07-13 19:34:34 +02:00
parent 2763b40b60
commit 0038605b57
5 changed files with 144 additions and 42 deletions

View File

@ -21,11 +21,12 @@ llm:
# Settings for Ollama
ollama:
# Can be overridden by OLLAMA_BASE_URL
base_url: "http://192.168.0.122:11434"
base_url: "http://192.168.0.140:11434"
# base_url: "https://api-amer-sandbox-gbl-mdm-hub.pfizer.com/ollama"
# Can be overridden by OLLAMA_MODEL
model: "phi4-mini:latest"
# model: "qwen3:1.7b"
# model: "mollm:360m"
# model: "smollm:360m"
# model: "qwen3:0.6b"

View File

@ -3,10 +3,17 @@ from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from loguru import logger
import sys
from config import settings
from .models import AnalysisFlags
class LLMInitializationError(Exception):
"""Custom exception for LLM initialization errors"""
def __init__(self, message, details=None):
super().__init__(message)
self.details = details
# Initialize LLM
llm = None
if settings.llm.mode == 'openai':
@ -20,21 +27,60 @@ if settings.llm.mode == 'openai':
)
elif settings.llm.mode == 'ollama':
logger.info(f"Initializing OllamaLLM with model: {settings.llm.ollama_model} at {settings.llm.ollama_base_url}")
llm = OllamaLLM(
model=settings.llm.ollama_model,
base_url=settings.llm.ollama_base_url,
streaming=False
)
try:
# Verify connection parameters
if not settings.llm.ollama_base_url:
raise ValueError("Ollama base URL is not configured")
if not settings.llm.ollama_model:
raise ValueError("Ollama model is not specified")
logger.debug(f"Attempting to connect to Ollama at {settings.llm.ollama_base_url}")
# Append /api/chat to base URL for OpenWebUI compatibility
base_url = f"{settings.llm.ollama_base_url.rstrip('/')}"
llm = OllamaLLM(
model=settings.llm.ollama_model,
base_url=base_url,
streaming=False,
timeout=30, # 30 second timeout
max_retries=3, # Retry up to 3 times
temperature=0.1,
top_p=0.2
)
# Test connection
logger.debug("Testing Ollama connection...")
llm.invoke("test") # Simple test request
logger.info("Ollama connection established successfully")
except Exception as e:
error_msg = f"Failed to initialize Ollama: {str(e)}"
details = {
'model': settings.llm.ollama_model,
'url': settings.llm.ollama_base_url,
'error_type': type(e).__name__
}
logger.error(error_msg)
logger.debug(f"Connection details: {details}")
raise LLMInitializationError(
"Failed to connect to Ollama service. Please check:"
"\n1. Ollama is installed and running"
"\n2. The base URL is correct"
"\n3. The model is available",
details=details
) from e
if llm is None:
logger.error("LLM could not be initialized. Exiting.")
print("\nERROR: Unable to initialize LLM. Check logs for details.", file=sys.stderr)
sys.exit(1)
# Set up Output Parser for structured JSON
parser = JsonOutputParser(pydantic_object=AnalysisFlags)
# Load prompt template from file
def load_prompt_template(version="v1.0.0"):
def load_prompt_template(version="v1.1.0"):
try:
with open(f"llm/prompts/jira_analysis_{version}.txt", "r") as f:
template = f.read()
@ -68,7 +114,30 @@ def create_analysis_chain():
# Initialize analysis chain
analysis_chain = create_analysis_chain()
# Response validation function
# Enhanced response validation function
def validate_response(response: dict) -> bool:
required_fields = ["hasMultipleEscalations", "customerSentiment"]
"""Validate the JSON response structure and content"""
try:
# Check required fields
required_fields = ["hasMultipleEscalations", "customerSentiment"]
if not all(field in response for field in required_fields):
return False
# Validate field types
if not isinstance(response["hasMultipleEscalations"], bool):
return False
if response["customerSentiment"] is not None:
if not isinstance(response["customerSentiment"], str):
return False
# Validate against schema using AnalysisFlags model
try:
AnalysisFlags.model_validate(response)
return True
except Exception:
return False
except Exception:
return False
return all(field in response for field in required_fields)

View File

@ -1,4 +1,4 @@
You are an AI assistant designed to analyze Jira ticket details containe email correspondence and extract key flags and sentiment.
You are an AI assistant designed to analyze Jira ticket details containe email correspondence and extract key flags and sentiment and extracting information into a strict JSON format.
Analyze the following Jira ticket information and provide your analysis in a JSON format.
Ensure the JSON strictly adheres to the specified schema.

View File

@ -0,0 +1,28 @@
SYSTEM INSTRUCTIONS:
You are an AI assistant designed to analyze Jira ticket details containing email correspondence and extract key flags and sentiment, outputting information in a strict JSON format.
Your output MUST be ONLY a valid JSON object. Do NOT include any conversational text, explanations, or markdown outside the JSON.
The JSON structure MUST follow this exact schema. If a field cannot be determined, use `null` for strings/numbers or empty list `[]` for arrays.
Consider the overall context of the ticket and specifically the latest comment if provided.
**Analysis Request:**
- Determine if there are signs of multiple escalation attempts in the descriptions or comments with regards to HUB team. Escalation to other teams are not considered.
-- Usually multiple requests one after another are being called by the same user in span of hours or days asking for immediate help of HUB team. Normal discussion, responses back and forth, are not considered as an escalation.
- Assess if the issue requires urgent attention based on language or context from the summary, description, or latest comment.
-- Usually means that Customer is asking for help due to upcoming deadlines, other high priority issues which are blocked due to our stall.
- Summarize the overall customer sentiment evident in the issue. Analyze tone of responses, happiness, gratefulness, irritation, etc.
USER CONTENT:
Issue Key: {issueKey}
Summary: {summary}
Description: {description}
Status: {status}
Existing Labels: {labels}
Assignee: {assignee}
Last Updated: {updated}
Latest Comment (if applicable): {comment}
{format_instructions}

View File

@ -1,34 +1,38 @@
import pytest
from fastapi.testclient import TestClient
from jira_webhook_llm import app
from llm.models import JiraWebhookPayload
from llm.chains import validate_response
def test_llm_response_format(test_client, mock_jira_payload):
response = test_client.post("/jira-webhook", json=mock_jira_payload)
assert response.status_code == 200
response_data = response.json()
# Validate response structure
assert "response" in response_data
assert "analysis" in response_data["response"]
assert "recommendations" in response_data["response"]
assert "status" in response_data["response"]
def test_validate_response_valid():
"""Test validation with valid response"""
response = {
"hasMultipleEscalations": False,
"customerSentiment": "neutral"
}
assert validate_response(response) is True
def test_llm_response_content_validation(test_client, mock_jira_payload):
response = test_client.post("/jira-webhook", json=mock_jira_payload)
response_data = response.json()
# Validate content types
assert isinstance(response_data["response"]["analysis"], str)
assert isinstance(response_data["response"]["recommendations"], list)
assert isinstance(response_data["response"]["status"], str)
def test_validate_response_missing_field():
"""Test validation with missing required field"""
response = {
"hasMultipleEscalations": False
}
assert validate_response(response) is False
def test_llm_error_handling(test_client):
# Test with invalid payload
invalid_payload = {"invalid": "data"}
response = test_client.post("/jira-webhook", json=invalid_payload)
assert response.status_code == 422
# Test with empty payload
response = test_client.post("/jira-webhook", json={})
assert response.status_code == 422
def test_validate_response_invalid_type():
"""Test validation with invalid field type"""
response = {
"hasMultipleEscalations": "not a boolean",
"customerSentiment": "neutral"
}
assert validate_response(response) is False
def test_validate_response_null_sentiment():
"""Test validation with null sentiment"""
response = {
"hasMultipleEscalations": True,
"customerSentiment": None
}
assert validate_response(response) is True
def test_validate_response_invalid_structure():
"""Test validation with invalid JSON structure"""
response = "not a dictionary"
assert validate_response(response) is False