121 lines
4.9 KiB
Python
121 lines
4.9 KiB
Python
from fastapi import HTTPException
|
|
from loguru import logger
|
|
import json
|
|
from typing import Optional, List, Union
|
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
from datetime import datetime
|
|
|
|
from config import settings
|
|
from langfuse import Langfuse
|
|
from llm.models import JiraWebhookPayload, AnalysisFlags
|
|
from llm.chains import analysis_chain, validate_response
|
|
|
|
class BadRequestError(HTTPException):
|
|
def __init__(self, detail: str):
|
|
super().__init__(status_code=400, detail=detail)
|
|
|
|
class RateLimitError(HTTPException):
|
|
def __init__(self, detail: str):
|
|
super().__init__(status_code=429, detail=detail)
|
|
|
|
class ValidationError(HTTPException):
|
|
def __init__(self, detail: str):
|
|
super().__init__(status_code=422, detail=detail)
|
|
|
|
class JiraWebhookHandler:
|
|
def __init__(self):
|
|
self.analysis_chain = analysis_chain
|
|
|
|
async def handle_webhook(self, payload: JiraWebhookPayload):
|
|
try:
|
|
if not payload.issueKey:
|
|
raise BadRequestError("Missing required field: issueKey")
|
|
|
|
if not payload.summary:
|
|
raise BadRequestError("Missing required field: summary")
|
|
|
|
logger.bind(
|
|
issue_key=payload.issueKey,
|
|
timestamp=datetime.utcnow().isoformat()
|
|
).info("Received webhook")
|
|
|
|
# Create Langfuse trace if enabled
|
|
trace = None
|
|
if settings.langfuse.enabled:
|
|
trace = settings.langfuse_client.trace(
|
|
Langfuse().trace(
|
|
id=f"webhook-{payload.issueKey}",
|
|
name="Jira Webhook",
|
|
input=payload.dict(),
|
|
metadata={
|
|
"issue_key": payload.issueKey,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
}
|
|
)
|
|
)
|
|
|
|
llm_input = {
|
|
"issueKey": payload.issueKey,
|
|
"summary": payload.summary,
|
|
"description": payload.description if payload.description else "No description provided.",
|
|
"status": payload.status if payload.status else "Unknown",
|
|
"labels": ", ".join(payload.labels) if payload.labels else "None",
|
|
"assignee": payload.assignee if payload.assignee else "Unassigned",
|
|
"updated": payload.updated if payload.updated else "Unknown",
|
|
"comment": payload.comment if payload.comment else "No new comment provided."
|
|
}
|
|
|
|
# Create Langfuse span for LLM processing if enabled
|
|
llm_span = None
|
|
if settings.langfuse.enabled and trace:
|
|
llm_span = trace.span(
|
|
name="LLM Processing",
|
|
input=llm_input,
|
|
metadata={
|
|
"model": settings.llm.model if settings.llm.mode == 'openai' else settings.llm.ollama_model
|
|
}
|
|
)
|
|
|
|
try:
|
|
analysis_result = await self.analysis_chain.ainvoke(llm_input)
|
|
|
|
# Update Langfuse span with output if enabled
|
|
if settings.langfuse.enabled and llm_span:
|
|
llm_span.end(output=analysis_result)
|
|
|
|
# Validate LLM response
|
|
if not validate_response(analysis_result):
|
|
logger.warning(f"Invalid LLM response format for {payload.issueKey}")
|
|
analysis_result = {
|
|
"hasMultipleEscalations": False,
|
|
"customerSentiment": "neutral"
|
|
}
|
|
|
|
logger.debug(f"LLM Analysis Result for {payload.issueKey}: {json.dumps(analysis_result, indent=2)}")
|
|
return {"status": "success", "analysis_flags": analysis_result}
|
|
|
|
except Exception as e:
|
|
logger.error(f"LLM processing failed for {payload.issueKey}: {str(e)}")
|
|
|
|
# Log error to Langfuse if enabled
|
|
if settings.langfuse.enabled and llm_span:
|
|
llm_span.end(error=e)
|
|
return {
|
|
"status": "error",
|
|
"analysis_flags": {
|
|
"hasMultipleEscalations": False,
|
|
"customerSentiment": "neutral"
|
|
},
|
|
"error": str(e)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing webhook: {str(e)}")
|
|
import traceback
|
|
logger.error(f"Stack trace: {traceback.format_exc()}")
|
|
|
|
# Log error to Langfuse if enabled
|
|
if settings.langfuse.enabled and trace:
|
|
trace.end(error=e)
|
|
|
|
raise HTTPException(status_code=500, detail=f"Internal Server Error: {str(e)}") |