feat: Implement Jira Webhook Handler with LLM Integration
- Added FastAPI application to handle Jira webhooks. - Created Pydantic models for Jira payload and LLM output. - Integrated LangChain with OpenAI and Ollama for LLM processing. - Set up Langfuse for tracing and monitoring. - Implemented analysis logic for Jira tickets, including sentiment analysis and label suggestions. - Added test endpoint for LLM integration. - Updated requirements.txt to include necessary dependencies and versions.
This commit is contained in:
parent
9fdea59554
commit
0c468c0a69
49
.gitignore
vendored
Normal file
49
.gitignore
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
# Operating System files
|
||||
.DS_Store
|
||||
.localized
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
*.egg
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Editor files (e.g., Visual Studio Code, Sublime Text, Vim)
|
||||
.vscode/
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs and temporary files
|
||||
log/
|
||||
*.log
|
||||
*.tmp
|
||||
tmp/
|
||||
|
||||
# Package managers
|
||||
node_modules/
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
# Dependencies for compiled languages (e.g., C++, Java)
|
||||
bin/
|
||||
obj/
|
||||
*.exe
|
||||
*.dll
|
||||
*.jar
|
||||
*.class
|
||||
|
||||
# Miscellaneous
|
||||
.env
|
||||
.DS_Store
|
||||
52
Dockerfile
Normal file
52
Dockerfile
Normal file
@ -0,0 +1,52 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
# --- Stage 1: Build Dependencies ---
|
||||
# Using a specific, stable Python version on Alpine for a small final image.
|
||||
FROM python:3.10-alpine3.18 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies for Python packages.
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
build-base \
|
||||
gcc \
|
||||
musl-dev \
|
||||
python3-dev \
|
||||
linux-headers
|
||||
|
||||
# Copy only the requirements file first to leverage Docker's build cache.
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies.
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Remove build dependencies to keep the final image lean.
|
||||
RUN apk del .build-deps
|
||||
|
||||
# --- Stage 2: Runtime Environment ---
|
||||
# Start fresh with a lean Alpine Python image.
|
||||
FROM python:3.10-alpine3.18
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy installed Python packages from the builder stage.
|
||||
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
|
||||
# Set environment variables for Python.
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# Copy the configuration directory first.
|
||||
# If only code changes, this layer remains cached.
|
||||
COPY config ./config
|
||||
|
||||
# Copy your application source code.
|
||||
COPY jira-webhook-llm.py .
|
||||
COPY config.py .
|
||||
|
||||
# Expose the port your application listens on.
|
||||
EXPOSE 8000
|
||||
|
||||
# Define the command to run your application.
|
||||
CMD ["uvicorn", "jira-webhook-llm:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
75
config.py
Normal file
75
config.py
Normal file
@ -0,0 +1,75 @@
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
from loguru import logger
|
||||
from typing import Optional
|
||||
|
||||
# Define a custom exception for configuration errors
|
||||
class AppConfigError(Exception):
|
||||
pass
|
||||
|
||||
class Settings:
|
||||
def __init__(self, config_path: str = "config/application.yml"):
|
||||
"""
|
||||
Loads configuration from a YAML file and overrides with environment variables.
|
||||
"""
|
||||
# --- Load from YAML file ---
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
except FileNotFoundError:
|
||||
raise AppConfigError(f"Configuration file not found at '{config_path}'.")
|
||||
except yaml.YAMLError as e:
|
||||
raise AppConfigError(f"Error parsing YAML file: {e}")
|
||||
|
||||
# --- Read and Combine Settings (Environment variables take precedence) ---
|
||||
llm_config = config.get('llm', {})
|
||||
|
||||
# General settings
|
||||
self.llm_mode: str = os.getenv("LLM_MODE", llm_config.get('mode', 'openai')).lower()
|
||||
|
||||
# OpenAI settings
|
||||
openai_config = llm_config.get('openai', {})
|
||||
self.openai_api_key: Optional[str] = os.getenv("OPENAI_API_KEY", openai_config.get('api_key'))
|
||||
self.openai_api_base_url: Optional[str] = os.getenv("OPENAI_API_BASE_URL", openai_config.get('api_base_url'))
|
||||
self.openai_model: Optional[str] = os.getenv("OPENAI_MODEL", openai_config.get('model'))
|
||||
|
||||
# Ollama settings
|
||||
ollama_config = llm_config.get('ollama', {})
|
||||
self.ollama_base_url: Optional[str] = os.getenv("OLLAMA_BASE_URL", ollama_config.get('base_url'))
|
||||
self.ollama_model: Optional[str] = os.getenv("OLLAMA_MODEL", ollama_config.get('model'))
|
||||
|
||||
self._validate()
|
||||
|
||||
def _validate(self):
|
||||
"""
|
||||
Validates that required configuration variables are set.
|
||||
"""
|
||||
logger.info(f"LLM mode set to: '{self.llm_mode}'")
|
||||
|
||||
if self.llm_mode == 'openai':
|
||||
if not self.openai_api_key:
|
||||
raise AppConfigError("LLM mode is 'openai', but OPENAI_API_KEY is not set.")
|
||||
if not self.openai_api_base_url:
|
||||
raise AppConfigError("LLM mode is 'openai', but OPENAI_API_BASE_URL is not set.")
|
||||
if not self.openai_model:
|
||||
raise AppConfigError("LLM mode is 'openai', but OPENAI_MODEL is not set.")
|
||||
|
||||
elif self.llm_mode == 'ollama':
|
||||
if not self.ollama_base_url:
|
||||
raise AppConfigError("LLM mode is 'ollama', but OLLAMA_BASE_URL is not set.")
|
||||
if not self.ollama_model:
|
||||
raise AppConfigError("LLM mode is 'ollama', but OLLAMA_MODEL is not set.")
|
||||
|
||||
else:
|
||||
raise AppConfigError(f"Invalid LLM_MODE: '{self.llm_mode}'. Must be 'openai' or 'ollama'.")
|
||||
|
||||
logger.info("Configuration validated successfully.")
|
||||
|
||||
# Create a single, validated instance of the settings to be imported by other modules.
|
||||
try:
|
||||
settings = Settings()
|
||||
except AppConfigError as e:
|
||||
logger.error(f"FATAL: {e}")
|
||||
logger.error("Application shutting down due to configuration error.")
|
||||
sys.exit(1) # Exit the application if configuration is invalid
|
||||
30
config/application.yml
Normal file
30
config/application.yml
Normal file
@ -0,0 +1,30 @@
|
||||
# Default application configuration
|
||||
llm:
|
||||
# The mode to run the application in.
|
||||
# Can be 'openai' or 'ollama'.
|
||||
# This can be overridden by the LLM_MODE environment variable.
|
||||
mode: ollama
|
||||
|
||||
# Settings for OpenAI-compatible APIs (like OpenRouter)
|
||||
openai:
|
||||
# It's HIGHLY recommended to set this via an environment variable
|
||||
# instead of saving it in this file.
|
||||
# Can be overridden by OPENAI_API_KEY
|
||||
api_key: "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"
|
||||
|
||||
# Can be overridden by OPENAI_API_BASE_URL
|
||||
api_base_url: "https://openrouter.ai/api/v1"
|
||||
|
||||
# Can be overridden by OPENAI_MODEL
|
||||
model: "deepseek/deepseek-chat:free"
|
||||
|
||||
# Settings for Ollama
|
||||
ollama:
|
||||
# Can be overridden by OLLAMA_BASE_URL
|
||||
base_url: "http://localhost:11434"
|
||||
|
||||
# Can be overridden by OLLAMA_MODEL
|
||||
# model: "phi4-mini:latest"
|
||||
# model: "qwen3:1.7b"
|
||||
model: "smollm:360m"
|
||||
|
||||
16
custom payload JIRA.json
Normal file
16
custom payload JIRA.json
Normal file
File diff suppressed because one or more lines are too long
65
docker-compose.yml
Normal file
65
docker-compose.yml
Normal file
@ -0,0 +1,65 @@
|
||||
name: jira-llm-stack
|
||||
services:
|
||||
# Service for the Ollama server
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
# Map port 11434 from the container to the host machine
|
||||
# This allows you to access Ollama directly from your host if needed (e.g., via curl http://localhost:11434)
|
||||
ports:
|
||||
- "11434:11434"
|
||||
# Mount a volume to persist Ollama models and data
|
||||
# This prevents redownloading models every time the container restarts
|
||||
volumes:
|
||||
- ollama_data:/root/.ollama
|
||||
|
||||
# CORRECTED COMMAND:
|
||||
# We explicitly tell Docker to use 'bash -c' to execute the string.
|
||||
# This ensures that 'ollama pull' and 'ollama serve' are run sequentially.
|
||||
entrypoint: ["sh"]
|
||||
command: ["-c", "ollama serve && ollama pull phi4-mini:latest"]
|
||||
|
||||
# Restart the container if it exits unexpectedly
|
||||
restart: unless-stopped
|
||||
|
||||
# Service for your FastAPI application
|
||||
app:
|
||||
# Build the Docker image for your app from the current directory (where Dockerfile is located)
|
||||
build: .
|
||||
# Map port 8000 from the container to the host machine
|
||||
# This allows you to access your FastAPI app at http://localhost:8000
|
||||
ports:
|
||||
- "8000:8000"
|
||||
# Define environment variables for your FastAPI application
|
||||
# These will be read by pydantic-settings in your app
|
||||
environment:
|
||||
# Set the LLM mode to 'ollama'
|
||||
LLM_MODE: ollama
|
||||
# Point to the Ollama service within the Docker Compose network
|
||||
# 'ollama' is the service name, which acts as a hostname within the network
|
||||
OLLAMA_BASE_URL: http://192.168.0.122:11434
|
||||
# Specify the model to use
|
||||
OLLAMA_MODEL: gemma3:1b
|
||||
# If you have an OpenAI API key in your settings, but want to ensure it's not used
|
||||
# when LLM_MODE is ollama, you can explicitly set it to empty or omit it.
|
||||
# OPENAI_API_KEY: ""
|
||||
# OPENAI_MODEL: ""
|
||||
# Ensure the Ollama service starts and is healthy before starting the app
|
||||
depends_on:
|
||||
- ollama
|
||||
# Restart the container if it exits unexpectedly
|
||||
restart: unless-stopped
|
||||
# Mount your current project directory into the container
|
||||
# This is useful for development, as changes to your code will be reflected
|
||||
# without rebuilding the image (if you're using a hot-reloading server like uvicorn --reload)
|
||||
# For production, you might remove this and rely solely on the Dockerfile copy.
|
||||
volumes:
|
||||
- .:/app
|
||||
# Command to run your FastAPI application using Uvicorn
|
||||
# --host 0.0.0.0 is crucial for the app to be accessible from outside the container
|
||||
# --reload is good for development; remove for production
|
||||
command: uvicorn jira-webhook-llm:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
# Define named volumes for persistent data
|
||||
volumes:
|
||||
ollama_data:
|
||||
driver: local
|
||||
1056
full JIRA payload.json
Normal file
1056
full JIRA payload.json
Normal file
File diff suppressed because one or more lines are too long
279
jira-webhook-llm.py
Normal file
279
jira-webhook-llm.py
Normal file
@ -0,0 +1,279 @@
|
||||
import os
|
||||
import json
|
||||
from typing import Optional, List, Union
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel, Field, ConfigDict, validator
|
||||
from loguru import logger
|
||||
|
||||
# Import your new settings object
|
||||
from config import settings
|
||||
|
||||
|
||||
# LangChain imports
|
||||
from langchain_ollama import OllamaLLM
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
from langchain_core.output_parsers import JsonOutputParser
|
||||
from pydantic import BaseModel as LCBaseModel
|
||||
from pydantic import field_validator
|
||||
|
||||
# Langfuse imports
|
||||
from langfuse import Langfuse, get_client
|
||||
from langfuse.langchain import CallbackHandler
|
||||
|
||||
# LANGFUSE_PUBLIC_KEY="pk_lf_..."
|
||||
# LANGFUSE_SECRET_KEY="sk_lf_..."
|
||||
# LANGFUSE_HOST="https://cloud.langfuse.com" # Or "https://us.cloud.langfuse.com" for US region, or your self-hosted instance
|
||||
|
||||
langfuse = Langfuse(
|
||||
secret_key="sk-lf-55d5fa70-e2d3-44d0-ae76-48181126d7ed",
|
||||
public_key="pk-lf-0f6178ee-e6aa-4cb7-a433-6c00c6512874",
|
||||
host="https://cloud.langfuse.com"
|
||||
)
|
||||
|
||||
# Initialize Langfuse client (optional, get_client() uses environment variables by default)
|
||||
# It's good practice to initialize it early to ensure connection.
|
||||
try:
|
||||
langfuse_client = get_client()
|
||||
if langfuse_client.auth_check():
|
||||
logger.info("Langfuse client authenticated successfully.")
|
||||
else:
|
||||
logger.warning("Langfuse authentication failed. Check your API keys and host.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize Langfuse client: {e}")
|
||||
# Depending on your tolerance, you might want to exit or continue without tracing
|
||||
# For now, we'll just log and continue, but traces won't be sent.
|
||||
|
||||
|
||||
# --- Pydantic Models for Jira Payload and LLM Output ---
|
||||
# Configuration for Pydantic to handle camelCase to snake_case conversion
|
||||
class JiraWebhookPayload(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=lambda x: ''.join(word.capitalize() if i > 0 else word for i, word in enumerate(x.split('_'))), populate_by_name=True)
|
||||
|
||||
issueKey: str
|
||||
summary: str
|
||||
description: Optional[str] = None
|
||||
comment: Optional[str] = None # Assuming this is the *new* comment that triggered the webhook
|
||||
labels: Optional[Union[List[str], str]] = []
|
||||
|
||||
@field_validator('labels', mode='before') # `pre=True` becomes `mode='before'`
|
||||
@classmethod # V2 validators must be classmethods
|
||||
def convert_labels_to_list(cls, v):
|
||||
if isinstance(v, str):
|
||||
return [v]
|
||||
return v or [] # Return an empty list if v is None/empty
|
||||
|
||||
status: Optional[str] = None
|
||||
assignee: Optional[str] = None
|
||||
updated: Optional[str] = None # Timestamp string
|
||||
|
||||
# Define the structure of the LLM's expected JSON output
|
||||
class AnalysisFlags(LCBaseModel):
|
||||
hasMultipleEscalations: bool = Field(description="Is there evidence of multiple escalation attempts or channels?")
|
||||
requiresUrgentAttention: bool = Field(description="Does the issue convey a sense of urgency beyond standard priority?")
|
||||
customerSentiment: Optional[str] = Field(description="Overall customer sentiment (e.g., 'neutral', 'frustrated', 'calm').")
|
||||
suggestedLabels: List[str] = Field(description="List of suggested Jira labels, e.g., ['escalated', 'high-customer-impact'].")
|
||||
summaryOfConcerns: Optional[str] = Field(description="A concise summary of the key concerns or problems in the ticket.")
|
||||
|
||||
|
||||
# --- LLM Setup (Now dynamic based on config) ---
|
||||
llm = None
|
||||
if settings.llm_mode == 'openai':
|
||||
logger.info(f"Initializing ChatOpenAI with model: {settings.openai_model}")
|
||||
llm = ChatOpenAI(
|
||||
model=settings.openai_model,
|
||||
temperature=0.7,
|
||||
max_tokens=2000,
|
||||
api_key=settings.openai_api_key,
|
||||
base_url=settings.openai_api_base_url
|
||||
)
|
||||
elif settings.llm_mode == 'ollama':
|
||||
logger.info(f"Initializing OllamaLLM with model: {settings.ollama_model} at {settings.ollama_base_url}")
|
||||
llm = OllamaLLM(
|
||||
model=settings.ollama_model,
|
||||
base_url=settings.ollama_base_url,
|
||||
streaming=False
|
||||
)
|
||||
|
||||
# This check is now redundant because config.py would have exited, but it's good for clarity.
|
||||
if llm is None:
|
||||
logger.error("LLM could not be initialized. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
# Set up Output Parser for structured JSON
|
||||
parser = JsonOutputParser(pydantic_object=AnalysisFlags)
|
||||
|
||||
# Prompt Template for LLM
|
||||
prompt_template = PromptTemplate(
|
||||
template="""
|
||||
You are an AI assistant designed to analyze Jira ticket details and extract key flags and sentiment.
|
||||
Analyze the following Jira ticket information and provide your analysis in a JSON format.
|
||||
Ensure the JSON strictly adheres to the specified schema.
|
||||
|
||||
Consider the overall context of the ticket and specifically the latest comment if provided.
|
||||
|
||||
Issue Key: {issueKey}
|
||||
Summary: {summary}
|
||||
Description: {description}
|
||||
Status: {status}
|
||||
Existing Labels: {labels}
|
||||
Assignee: {assignee}
|
||||
Last Updated: {updated}
|
||||
Latest Comment (if applicable): {comment}
|
||||
|
||||
**Analysis Request:**
|
||||
- Determine if there are signs of multiple escalation attempts in the descriptions or comments.
|
||||
- Assess if the issue requires urgent attention based on language or context from the summary, description, or latest comment.
|
||||
- Summarize the overall customer sentiment evident in the issue.
|
||||
- Suggest relevant Jira labels that should be applied to this issue.
|
||||
- Provide a concise summary of the key concerns or problems described in the ticket.
|
||||
- Generate a concise, objective comment (max 2-3 sentences) suitable for directly adding to the Jira ticket, summarizing the AI's findings.
|
||||
|
||||
{format_instructions}
|
||||
""",
|
||||
input_variables=[
|
||||
"issueKey", "summary", "description", "status", "labels",
|
||||
"assignee", "updated", "comment"
|
||||
],
|
||||
partial_variables={"format_instructions": parser.get_format_instructions()},
|
||||
)
|
||||
|
||||
# Chain for LLM invocation
|
||||
analysis_chain = prompt_template | llm | parser
|
||||
|
||||
# --- Webhook Endpoint ---
|
||||
@app.post("/jira-webhook")
|
||||
async def jira_webhook_handler(payload: JiraWebhookPayload):
|
||||
# Initialize Langfuse CallbackHandler for this request
|
||||
# This ensures each webhook invocation gets its own trace in Langfuse
|
||||
langfuse_handler = CallbackHandler()
|
||||
|
||||
try:
|
||||
logger.info(f"Received webhook for Jira issue: {payload.issueKey}")
|
||||
|
||||
# Prepare payload for LangChain:
|
||||
# 1. Use the 'comment' field directly if it exists, as it's typically the trigger.
|
||||
# 2. Convert Optional fields to usable strings for the prompt.
|
||||
|
||||
# This mapping handles potential None values in the payload
|
||||
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."
|
||||
}
|
||||
|
||||
# Pass data to LangChain for analysis
|
||||
# Using ainvoke for async execution
|
||||
# Add the Langfuse handler to the config of the ainvoke call
|
||||
analysis_result = await analysis_chain.ainvoke(
|
||||
llm_input,
|
||||
config={
|
||||
"callbacks": [langfuse_handler],
|
||||
"metadata": {
|
||||
"trace_name": f"JiraWebhook-{payload.issueKey}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
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"Error processing webhook: {e}")
|
||||
import traceback
|
||||
traceback.print_exc() # Print full traceback for debugging
|
||||
|
||||
# In case of an error, you might want to log it to Langfuse as well
|
||||
# You can update the trace with an error
|
||||
if langfuse_handler.trace: # Check if the trace was started
|
||||
langfuse_handler.trace.update(
|
||||
status_message=f"Error: {str(e)}",
|
||||
level="ERROR"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=500, detail=f"Internal Server Error: {str(e)}")
|
||||
finally:
|
||||
# It's good practice to flush the Langfuse client to ensure all events are sent
|
||||
# This is especially important in short-lived processes or serverless functions
|
||||
# For a long-running FastAPI app, the client's internal queue usually handles this
|
||||
# but explicit flush can be useful for immediate visibility or during testing.
|
||||
if langfuse_client:
|
||||
langfuse_client.flush()
|
||||
|
||||
|
||||
# To run this:
|
||||
# 1. Set OPENAI_API_KEY, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_HOST environment variables
|
||||
# 2. Start FastAPI: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||
@app.post("/test-llm")
|
||||
async def test_llm():
|
||||
"""Test endpoint for LLM integration"""
|
||||
# Correctly initialize the Langfuse CallbackHandler.
|
||||
# It inherits the client configuration from the global 'langfuse' instance.
|
||||
# If you need to name the trace, you do so in the 'ainvoke' call's metadata.
|
||||
langfuse_handler = CallbackHandler(
|
||||
# The constructor does not take 'trace_name'.
|
||||
# Remove it from here.
|
||||
)
|
||||
|
||||
test_payload = {
|
||||
"issueKey": "TEST-123",
|
||||
"summary": "Test issue",
|
||||
"description": "This is a test issue for LLM integration",
|
||||
"comment": "Testing OpenAI integration with Langfuse",
|
||||
"labels": ["test"],
|
||||
"status": "Open",
|
||||
"assignee": "Tester",
|
||||
"updated": "2025-07-04T21:40:00Z"
|
||||
}
|
||||
|
||||
try:
|
||||
llm_input = {
|
||||
"issueKey": test_payload["issueKey"],
|
||||
"summary": test_payload["summary"],
|
||||
"description": test_payload["description"],
|
||||
"status": test_payload["status"],
|
||||
"labels": ", ".join(test_payload["labels"]),
|
||||
"assignee": test_payload["assignee"],
|
||||
"updated": test_payload["updated"],
|
||||
"comment": test_payload["comment"]
|
||||
}
|
||||
|
||||
# To name the trace, you pass it in the config's metadata
|
||||
result = await analysis_chain.ainvoke(
|
||||
llm_input,
|
||||
config={
|
||||
"callbacks": [langfuse_handler],
|
||||
"metadata": {
|
||||
"trace_name": "TestLLM" # Correct way to name the trace
|
||||
}
|
||||
}
|
||||
)
|
||||
return {
|
||||
"status": "success",
|
||||
"result": result
|
||||
}
|
||||
except Exception as e:
|
||||
if langfuse_handler.trace:
|
||||
langfuse_handler.trace.update(
|
||||
status_message=f"Error in test-llm: {str(e)}",
|
||||
level="ERROR"
|
||||
)
|
||||
logger.error(f"Error in /test-llm: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
finally:
|
||||
if langfuse_client:
|
||||
langfuse_client.flush()
|
||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@ -0,0 +1,10 @@
|
||||
fastapi==0.111.0
|
||||
pydantic==2.9.0 # Changed from 2.7.4 to meet ollama's requirement
|
||||
pydantic-settings==2.0.0
|
||||
langchain-ollama==0.3.3
|
||||
langchain-openai==0.3.27
|
||||
langchain-core==0.3.68
|
||||
uvicorn==0.30.1
|
||||
python-multipart==0.0.9 # Good to include for FastAPI forms
|
||||
loguru==0.7.3
|
||||
langfuse==3.1.3
|
||||
Loading…
x
Reference in New Issue
Block a user