feat: Update Ollama configuration and enhance LLM initialization with error handling
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
This commit is contained in:
parent
2763b40b60
commit
0038605b57
@ -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"
|
||||
@ -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)
|
||||
@ -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.
|
||||
|
||||
|
||||
28
llm/prompts/jira_analysis_v1.1.0.txt
Normal file
28
llm/prompts/jira_analysis_v1.1.0.txt
Normal 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}
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user