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)}")