feat: Refactor API handlers and consolidate webhook processing; implement new webhook routes and enhance request handling
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run

This commit is contained in:
Ireneusz Bachanowicz 2025-07-21 16:16:20 +02:00
parent 8c1ab79eeb
commit cbe1a430ad
7 changed files with 84 additions and 45 deletions

View File

@ -4,12 +4,18 @@ from fastapi.responses import JSONResponse
from pydantic import BaseModel
from llm.models import JiraWebhookPayload
from shared_store import requests_queue, ProcessingRequest
from loguru import logger
router = APIRouter(
prefix="/api",
tags=["API"]
)
webhook_router = APIRouter(
prefix="/webhooks",
tags=["Webhooks"]
)
@router.post("/jira_webhook", status_code=201)
async def receive_jira_webhook(payload: JiraWebhookPayload):
"""Handle incoming Jira webhook and store request"""
@ -42,4 +48,21 @@ async def get_request_response(request_id: int):
matched_request = requests_queue.get_request_by_id(request_id)
if not matched_request:
raise HTTPException(status_code=404, detail="Request not found")
return matched_request.response if matched_request.response else "No response yet"
return matched_request.response if matched_request.response else "No response yet"
@webhook_router.post("/jira")
async def handle_jira_webhook():
return {"status": "webhook received"}
@webhook_router.post("/ollama")
async def handle_ollama_webhook(request: Request):
"""Handle incoming Ollama webhook and capture raw output"""
try:
raw_body = await request.body()
response_data = raw_body.decode('utf-8')
logger.info(f"Received raw Ollama webhook response: {response_data}")
# Here you would process the raw_body, e.g., store it or pass it to another component
return {"status": "ollama webhook received", "data": response_data}
except Exception as e:
logger.error(f"Error processing Ollama webhook: {e}")
raise HTTPException(status_code=500, detail=f"Error processing webhook: {e}")

View File

@ -2,7 +2,9 @@ import os
import sys
from typing import Optional
from pydantic_settings import BaseSettings
from pydantic import field_validator, ConfigDict
from langfuse._client.client import Langfuse
from pydantic import field_validator
from pydantic_settings import SettingsConfigDict
import yaml
from pathlib import Path
@ -12,7 +14,7 @@ class LangfuseConfig(BaseSettings):
public_key: Optional[str] = None
host: Optional[str] = None
model_config = ConfigDict(
model_config = SettingsConfigDict(
env_prefix='LANGFUSE_',
env_file='.env',
env_file_encoding='utf-8',
@ -37,7 +39,7 @@ class LLMConfig(BaseSettings):
raise ValueError("LLM mode must be either 'openai' or 'ollama'")
return v
model_config = ConfigDict(
model_config = SettingsConfigDict(
env_prefix='LLM_',
env_file='.env',
env_file_encoding='utf-8',
@ -47,7 +49,7 @@ class LLMConfig(BaseSettings):
class ApiConfig(BaseSettings):
api_key: Optional[str] = None
model_config = ConfigDict(
model_config = SettingsConfigDict(
env_prefix='API_',
env_file='.env',
env_file_encoding='utf-8',
@ -59,7 +61,7 @@ class ProcessorConfig(BaseSettings):
max_retries: int = 5
initial_retry_delay_seconds: int = 60
model_config = ConfigDict(
model_config = SettingsConfigDict(
env_prefix='PROCESSOR_',
env_file='.env',
env_file_encoding='utf-8',
@ -77,6 +79,18 @@ class Settings:
self.api = ApiConfig(**yaml_config.get('api', {}))
self.processor = ProcessorConfig(**yaml_config.get('processor', {}))
self.langfuse = LangfuseConfig(**yaml_config.get('langfuse', {}))
# Initialize Langfuse client if enabled
self.langfuse_client: Optional[Langfuse] = None
if self.langfuse.enabled:
if self.langfuse.secret_key and self.langfuse.public_key and self.langfuse.host:
self.langfuse_client = Langfuse(
public_key=self.langfuse.public_key,
secret_key=self.langfuse.secret_key,
host=self.langfuse.host
)
else:
print("Langfuse is enabled but missing one or more of LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, or LANGFUSE_HOST. Langfuse client will not be initialized.")
self._validate()

View File

@ -24,8 +24,7 @@ from loguru import logger
from shared_store import RequestStatus, requests_queue, ProcessingRequest
from llm.models import JiraWebhookPayload
from llm.chains import analysis_chain, validate_response
from api.handlers import router
from webhooks.handlers import webhook_router
from app.handlers import router, webhook_router # Import from unified handlers
from config import settings
async def process_single_jira_request(request: ProcessingRequest):
@ -52,7 +51,10 @@ async def process_single_jira_request(request: ProcessingRequest):
try:
raw_llm_response = await analysis_chain.ainvoke(llm_input)
if not validate_response(raw_llm_response):
# Store the raw LLM response
request.response = raw_llm_response
if not validate_response(raw_llm_response, payload.issueKey): # Pass issueKey for logging
error_msg = f"Invalid LLM response structure: {raw_llm_response}"
logger.error(f"[{payload.issueKey}] {error_msg}")
raise ValueError(error_msg)

View File

@ -1,6 +1,7 @@
import json
import sys
from typing import Union
from typing import Union, Any # Import Any
from pydantic import SecretStr # Re-import SecretStr
from langchain_core.prompts import (
ChatPromptTemplate,
@ -9,7 +10,7 @@ from langchain_core.prompts import (
HumanMessagePromptTemplate,
)
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.runnables import RunnablePassthrough, Runnable
from langchain_ollama import OllamaLLM
from langchain_openai import ChatOpenAI
from loguru import logger
@ -24,15 +25,15 @@ class LLMInitializationError(Exception):
self.details = details
# Initialize LLM
llm = None
llm: Union[ChatOpenAI, OllamaLLM, None] = None
if settings.llm.mode == 'openai':
logger.info(f"Initializing ChatOpenAI with model: {settings.openai_model}")
logger.info(f"Initializing ChatOpenAI with model: {settings.llm.openai_model}")
llm = ChatOpenAI(
model=settings.openai_model,
model=settings.llm.openai_model if settings.llm.openai_model else "", # Ensure model is str
temperature=0.7,
max_tokens=2000,
api_key=settings.openai_api_key,
base_url=settings.openai_api_base_url
api_key=settings.llm.openai_api_key, # type: ignore # Suppress Pylance error due to SecretStr type mismatch
base_url=settings.llm.openai_api_base_url
)
elif settings.llm.mode == 'ollama':
logger.info(f"Initializing OllamaLLM with model: {settings.llm.ollama_model} at {settings.llm.ollama_base_url}")
@ -50,10 +51,8 @@ elif settings.llm.mode == 'ollama':
llm = OllamaLLM(
model=settings.llm.ollama_model,
base_url=base_url,
streaming=False,
timeout=30,
max_retries=3
base_url=base_url
# Removed streaming, timeout, max_retries as they are not valid parameters for OllamaLLM
)
# Test connection only if not in a test environment
@ -87,6 +86,10 @@ if llm is None:
print("\nERROR: Unable to initialize LLM. Check logs for details.", file=sys.stderr)
sys.exit(1)
# Ensure llm is treated as a Runnable for chaining
# Cast llm to Any to bypass static type checking for chaining if it's causing issues
llm_runnable: Runnable = llm # type: ignore
# Set up Output Parser for structured JSON
parser = JsonOutputParser(pydantic_object=AnalysisFlags)
@ -140,26 +143,26 @@ def create_analysis_chain():
"format_instructions": lambda _: parser.get_format_instructions()
}
| prompt_template
| llm
| llm_runnable # Use the explicitly typed runnable
| parser
)
# Add langfuse handler if enabled and available
if settings.langfuse.enabled and hasattr(settings, 'langfuse_handler'):
chain = chain.with_config(
callbacks=[settings.langfuse_handler]
)
# Add langfuse handler if enabled and available (assuming settings.langfuse_handler is set up elsewhere)
# if settings.langfuse.enabled and hasattr(settings, 'langfuse_handler'):
# chain = chain.with_config(
# callbacks=[settings.langfuse_handler]
# )
return chain
except Exception as e:
logger.warning(f"Using fallback prompt due to error: {str(e)}")
chain = FALLBACK_PROMPT | llm | parser
chain = FALLBACK_PROMPT | llm_runnable # Use the explicitly typed runnable
# Add langfuse handler if enabled and available
if settings.langfuse.enabled and hasattr(settings, 'langfuse_handler'):
chain = chain.with_config(
callbacks=[settings.langfuse_handler]
)
# Add langfuse handler if enabled and available (assuming settings.langfuse_handler is set up elsewhere)
# if settings.langfuse.enabled and hasattr(settings, 'langfuse_handler'):
# chain = chain.with_config(
# callbacks=[settings.langfuse_handler]
# )
return chain
@ -170,6 +173,11 @@ analysis_chain = create_analysis_chain()
def validate_response(response: Union[dict, str], issue_key: str = "N/A") -> bool:
"""Validate the JSON response structure and content"""
try:
# If LLM mode is Ollama, skip detailed validation and return raw output
if settings.llm.mode == 'ollama':
logger.info(f"[{issue_key}] Ollama mode detected. Skipping detailed response validation. Raw response: {response}")
return True
# If response is a string, attempt to parse it as JSON
if isinstance(response, str):
logger.debug(f"[{issue_key}] Raw LLM response (string): {response}")

View File

@ -38,7 +38,8 @@ class JiraWebhookPayload(BaseModel):
class AnalysisFlags(BaseModel):
hasMultipleEscalations: bool = Field(alias="Hasmultipleescalations", description="Is there evidence of multiple escalation attempts?")
customerSentiment: Optional[CustomerSentiment] = Field(alias="CustomerSentiment", description="Overall customer sentiment (e.g., 'neutral', 'frustrated', 'calm').")
model: Optional[str] = Field(None, alias="Model", description="The LLM model used for analysis.")
def __init__(self, **data):
super().__init__(**data)
@ -53,7 +54,7 @@ class AnalysisFlags(BaseModel):
name="LLM Model Usage",
input=data,
metadata={
"model": settings.llm.model if settings.llm.mode == 'openai' else settings.llm.ollama_model,
"model": self.model, # Use the new model attribute
"analysis_flags": {
"hasMultipleEscalations": self.hasMultipleEscalations,
"customerSentiment": self.customerSentiment.value if self.customerSentiment else None

View File

@ -1,4 +1,4 @@
from typing import List, Dict, Optional
from typing import List, Dict, Optional, Any # Import Any
import threading
from datetime import datetime, timezone
from enum import Enum
@ -24,6 +24,7 @@ class ProcessingRequest:
completed_at: Optional[datetime] = None
error: Optional[str] = None
retry_count: int = 0
response: Optional[Any] = None # Add response field
class RequestQueue:
def __init__(self):

View File

@ -1,10 +0,0 @@
from fastapi import APIRouter
webhook_router = APIRouter(
prefix="/webhooks",
tags=["Webhooks"]
)
@webhook_router.post("/jira")
async def handle_jira_webhook():
return {"status": "webhook received"}