This commit is contained in:
parent
9e698e40f9
commit
6a57d91b7e
15
.env
15
.env
@ -1,20 +1,21 @@
|
|||||||
# Ollama configuration
|
# Ollama configuration
|
||||||
LLM_OLLAMA_BASE_URL=http://192.168.0.140:11434
|
# LLM_OLLAMA_BASE_URL=http://192.168.0.140:11434
|
||||||
# LLM_OLLAMA_BASE_URL=http://192.168.0.122:11434
|
# LLM_OLLAMA_BASE_URL=http://192.168.0.122:11434
|
||||||
# LLM_OLLAMA_BASE_URL="https://api-amer-sandbox-gbl-mdm-hub.pfizer.com/ollama"
|
LLM_OLLAMA_BASE_URL="https://api-amer-sandbox-gbl-mdm-hub.pfizer.com/ollama"
|
||||||
LLM_OLLAMA_MODEL=phi4-mini:latest
|
LLM_OLLAMA_MODEL=phi4-mini:latest
|
||||||
# LLM_OLLAMA_MODEL=smollm:360m
|
# LLM_OLLAMA_MODEL=smollm:360m
|
||||||
# LLM_OLLAMA_MODEL=qwen3:0.6b
|
# LLM_OLLAMA_MODEL=qwen3:0.6b
|
||||||
# LLM_OLLAMA_MODEL=qwen3:1.7b
|
# LLM_OLLAMA_MODEL=qwen3:1.7b
|
||||||
|
# LLM_OLLAMA_MODEL=qwen3:8b
|
||||||
# Logging configuration
|
# Logging configuration
|
||||||
LOG_LEVEL=DEBUG
|
LOG_LEVEL=DEBUG
|
||||||
# Ollama API Key (required when using Ollama mode)
|
# Ollama API Key (required when using Ollama mode)
|
||||||
# Langfuse configuration
|
# Langfuse configuration
|
||||||
LANGFUSE_ENABLED=true
|
LANGFUSE_ENABLED=false
|
||||||
LANGFUSE_PUBLIC_KEY="pk-lf-17dfde63-93e2-4983-8aa7-2673d3ecaab8"
|
LANGFUSE_PUBLIC_KEY="pk-lf-"
|
||||||
LANGFUSE_SECRET_KEY="sk-lf-ba41a266-6fe5-4c90-a483-bec8a7aaa321"
|
LANGFUSE_SECRET_KEY="sk-lf-"
|
||||||
LANGFUSE_HOST="https://cloud.langfuse.com"
|
LANGFUSE_HOST="https://cloud.langfuse.com"
|
||||||
# Gemini configuration
|
# Gemini configuration
|
||||||
LLM_GEMINI_API_KEY="AIzaSyDl12gxyTf2xCaTbT6OMJg0I-Rc82Ib77c"
|
LLM_GEMINI_API_KEY=""
|
||||||
LLM_GEMINI_MODEL="gemini-2.5-flash"
|
LLM_GEMINI_MODEL="gemini-2.5-flash"
|
||||||
LLM_MODE=gemini
|
LLM_MODE=ollama
|
||||||
@ -42,11 +42,11 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
COPY config ./config
|
COPY config ./config
|
||||||
|
|
||||||
# Copy your application source code.
|
# Copy your application source code.
|
||||||
COPY jira_webhook_llm.py .
|
COPY main.py .
|
||||||
COPY config.py .
|
COPY config.py .
|
||||||
|
|
||||||
# Expose the port your application listens on.
|
# Expose the port your application listens on.
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Define the command to run your application.
|
# Define the command to run your application.
|
||||||
CMD ["uvicorn", "jira-webhook-llm:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@ -3,7 +3,7 @@ llm:
|
|||||||
# The mode to run the application in.
|
# The mode to run the application in.
|
||||||
# Can be 'openai' or 'ollama'.
|
# Can be 'openai' or 'ollama'.
|
||||||
# This can be overridden by the LLM_MODE environment variable.
|
# This can be overridden by the LLM_MODE environment variable.
|
||||||
mode: gemini # Change mode to gemini
|
mode: ollama # Change mode to gemini
|
||||||
|
|
||||||
# Settings for OpenAI-compatible APIs (like OpenRouter)
|
# Settings for OpenAI-compatible APIs (like OpenRouter)
|
||||||
openai:
|
openai:
|
||||||
@ -26,7 +26,7 @@ llm:
|
|||||||
# It's HIGHLY recommended to set this via an environment variable
|
# It's HIGHLY recommended to set this via an environment variable
|
||||||
# instead of saving it in this file.
|
# instead of saving it in this file.
|
||||||
# Can be overridden by GEMINI_API_KEY
|
# Can be overridden by GEMINI_API_KEY
|
||||||
api_key: "AIzaSyDl12gxyTf2xCaTbT6OMJg0I-Rc82Ib77c" # Move from openai
|
api_key: ""
|
||||||
|
|
||||||
# Can be overridden by GEMINI_MODEL
|
# Can be overridden by GEMINI_MODEL
|
||||||
# model: "gemini-2.5-flash"
|
# model: "gemini-2.5-flash"
|
||||||
@ -38,7 +38,7 @@ llm:
|
|||||||
# Settings for Ollama
|
# Settings for Ollama
|
||||||
ollama:
|
ollama:
|
||||||
# Can be overridden by OLLAMA_BASE_URL
|
# Can be overridden by OLLAMA_BASE_URL
|
||||||
#base_url: "http://192.168.0.140:11434"
|
# base_url: "http://192.168.0.122:11434"
|
||||||
base_url: "https://api-amer-sandbox-gbl-mdm-hub.pfizer.com/ollama"
|
base_url: "https://api-amer-sandbox-gbl-mdm-hub.pfizer.com/ollama"
|
||||||
|
|
||||||
|
|
||||||
@ -47,17 +47,18 @@ llm:
|
|||||||
# model: "qwen3:1.7b"
|
# model: "qwen3:1.7b"
|
||||||
# model: "smollm:360m"
|
# model: "smollm:360m"
|
||||||
# model: "qwen3:0.6b"
|
# model: "qwen3:0.6b"
|
||||||
|
# model: "qwen3:8b"
|
||||||
# Langfuse configuration for observability and analytics
|
# Langfuse configuration for observability and analytics
|
||||||
langfuse:
|
langfuse:
|
||||||
# Enable or disable Langfuse integration
|
# Enable or disable Langfuse integration
|
||||||
# Can be overridden by LANGFUSE_ENABLED environment variable
|
# Can be overridden by LANGFUSE_ENABLED environment variable
|
||||||
enabled: true
|
enabled: false
|
||||||
|
|
||||||
# Langfuse API credentials
|
# Langfuse API credentials
|
||||||
# It's HIGHLY recommended to set these via environment variables
|
# It's HIGHLY recommended to set these via environment variables
|
||||||
# instead of saving them in this file
|
# instead of saving them in this file
|
||||||
public_key: "pk-lf-17dfde63-93e2-4983-8aa7-2673d3ecaab8"
|
public_key: "pk-lf-"
|
||||||
secret_key: "sk-lf-ba41a266-6fe5-4c90-a483-bec8a7aaa321"
|
secret_key: "sk-lf-"
|
||||||
host: "https://cloud.langfuse.com"
|
host: "https://cloud.langfuse.com"
|
||||||
|
|
||||||
# Processor configuration
|
# Processor configuration
|
||||||
@ -68,7 +69,7 @@ processor:
|
|||||||
|
|
||||||
# Maximum number of retries for failed Jira analysis requests
|
# Maximum number of retries for failed Jira analysis requests
|
||||||
# Can be overridden by PROCESSOR_MAX_RETRIES environment variable
|
# Can be overridden by PROCESSOR_MAX_RETRIES environment variable
|
||||||
max_retries: 5
|
max_retries: 0
|
||||||
|
|
||||||
# Initial delay in seconds before the first retry attempt (exponential backoff)
|
# Initial delay in seconds before the first retry attempt (exponential backoff)
|
||||||
# Can be overridden by PROCESSOR_INITIAL_RETRY_DELAY_SECONDS environment variable
|
# Can be overridden by PROCESSOR_INITIAL_RETRY_DELAY_SECONDS environment variable
|
||||||
|
|||||||
@ -8,7 +8,7 @@ services:
|
|||||||
|
|
||||||
# Service for your FastAPI application
|
# Service for your FastAPI application
|
||||||
jira-webhook-llm:
|
jira-webhook-llm:
|
||||||
image: artifactory.pfizer.com/mdmhub-docker-dev/mdmtools/ollama/jira-webhook-llm:0.1.8
|
image: artifactory.pfizer.com/mdmhub-docker-dev/mdmtools/ollama/jira-webhook-llm:0.2.0
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
environment:
|
environment:
|
||||||
@ -30,4 +30,4 @@ services:
|
|||||||
# Command to run your FastAPI application using Uvicorn
|
# 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
|
# --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
|
# --reload is good for development; remove for production
|
||||||
command: uvicorn jira-webhook-llm:app --host 0.0.0.0 --port 8000
|
command: uvicorn main:app --host 0.0.0.0 --port 8000
|
||||||
@ -32,7 +32,6 @@ if settings.llm.mode == 'openai':
|
|||||||
llm = ChatOpenAI(
|
llm = ChatOpenAI(
|
||||||
model=settings.llm.openai_model if settings.llm.openai_model else "", # Ensure model is str
|
model=settings.llm.openai_model if settings.llm.openai_model else "", # Ensure model is str
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
max_tokens=2000,
|
|
||||||
api_key=settings.llm.openai_api_key, # type: ignore # Suppress Pylance error due to SecretStr type mismatch
|
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
|
base_url=settings.llm.openai_api_base_url
|
||||||
)
|
)
|
||||||
@ -52,7 +51,8 @@ elif settings.llm.mode == 'ollama':
|
|||||||
|
|
||||||
llm = OllamaLLM(
|
llm = OllamaLLM(
|
||||||
model=settings.llm.ollama_model,
|
model=settings.llm.ollama_model,
|
||||||
base_url=base_url
|
base_url=base_url,
|
||||||
|
num_ctx=32000
|
||||||
# Removed streaming, timeout, max_retries as they are not valid parameters for OllamaLLM
|
# Removed streaming, timeout, max_retries as they are not valid parameters for OllamaLLM
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -92,7 +92,6 @@ elif settings.llm.mode == 'gemini': # New: Add Gemini initialization
|
|||||||
llm = ChatGoogleGenerativeAI(
|
llm = ChatGoogleGenerativeAI(
|
||||||
model=settings.llm.gemini_model,
|
model=settings.llm.gemini_model,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
max_tokens=2000,
|
|
||||||
google_api_key=settings.llm.gemini_api_key
|
google_api_key=settings.llm.gemini_api_key
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -131,7 +130,7 @@ if llm is None:
|
|||||||
llm_runnable: Runnable = llm # type: ignore
|
llm_runnable: Runnable = llm # type: ignore
|
||||||
|
|
||||||
# Set up Output Parser for structured JSON
|
# Set up Output Parser for structured JSON
|
||||||
parser = JsonOutputParser(pydantic_object=AnalysisFlags)
|
parser = JsonOutputParser()
|
||||||
|
|
||||||
# Load prompt template from file
|
# Load prompt template from file
|
||||||
def load_prompt_template(version="v1.2.0"):
|
def load_prompt_template(version="v1.2.0"):
|
||||||
@ -222,8 +221,8 @@ def validate_response(response: Union[dict, str], issue_key: str = "N/A") -> boo
|
|||||||
AnalysisFlags.model_validate(response)
|
AnalysisFlags.model_validate(response)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[{issue_key}] Pydantic validation error: {e}. Invalid response: {response}")
|
logger.warning(f"[{issue_key}] Pydantic validation failed: {e}. Continuing with raw response: {response}")
|
||||||
return False
|
return True # Allow processing even if validation fails
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[{issue_key}] Unexpected error during response validation: {e}. Response: {response}")
|
logger.error(f"[{issue_key}] Unexpected error during response validation: {e}. Response: {response}")
|
||||||
return False
|
return False
|
||||||
@ -1,60 +1,58 @@
|
|||||||
SYSTEM:
|
SYSTEM:
|
||||||
You are a precise AI assistant that analyzes Jira tickets and outputs a JSON object.
|
You are an expert AI assistant that analyzes Jira tickets and outputs a concise summary in a valid JSON format.
|
||||||
Your task is to analyze the provided Jira ticket data and generate a JSON object based on the rules below.
|
Your output MUST be a single JSON object and nothing else.
|
||||||
Your output MUST be ONLY the JSON object, with no additional text or explanations.
|
|
||||||
|
|
||||||
## JSON Output Schema
|
## JSON Output Schema
|
||||||
{format_instructions}
|
{format_instructions}
|
||||||
|
|
||||||
## Field-by-Field Instructions
|
## Field-by-Field Instructions
|
||||||
|
|
||||||
### `hasMultipleEscalations` (boolean)
|
1. **`issueCategory` (string)**
|
||||||
- Set to `true` ONLY if the user has made multiple requests for help from the "MDM HUB team" without getting a response.
|
* Classify the core problem. Choose ONE: "technical_issue", "data_request", "access_problem", "general_question", "other".
|
||||||
- A normal back-and-forth conversation is NOT an escalation.
|
|
||||||
|
|
||||||
### `customerSentiment` (string: "neutral", "frustrated", "calm")
|
2. **`area` (string)**
|
||||||
- Set to `"frustrated"` if the user mentions blockers, deadlines, or uses urgent language (e.g., "urgent", "asap", "blocked").
|
* Classify the technical domain. Choose the BEST fit from the following options:
|
||||||
- Set to `"calm"` if the language is polite and patient.
|
* `"Direct Channel"`
|
||||||
- Set to `"neutral"` otherwise.
|
* `"Streaming Channel"`
|
||||||
|
* `"Java Batch Channel"`
|
||||||
|
* `"ETL Batch Channel"`
|
||||||
|
* `"DCR Service"`
|
||||||
|
* `"API Gateway"`
|
||||||
|
* `"Callback Service"`
|
||||||
|
* `"Publisher"`
|
||||||
|
* `"Reconciliation"`
|
||||||
|
* `"Snowflake"`
|
||||||
|
* `"Authentication"`
|
||||||
|
* `"Other"`
|
||||||
|
|
||||||
### `issueCategory` (string: "technical_issue", "data_request", "access_problem", "general_question", "other")
|
3. **`customerSentiment` (string)**
|
||||||
- `"technical_issue"`: Errors, bugs, system failures, API problems.
|
* Analyze the user's tone.
|
||||||
- `"data_request"`: Asking for data exports, reports, or information retrieval.
|
* `"frustrated"`: User mentions blockers, deadlines, or uses urgent language ("ASAP", "urgent", "blocked").
|
||||||
- `"access_problem"`: User cannot log in, has permission issues.
|
* `"neutral"`: Default, non-emotional tone.
|
||||||
- `"general_question"`: "How do I..." or other general inquiries.
|
|
||||||
- `"other"`: If it doesn't fit any other category.
|
|
||||||
|
|
||||||
### `area` (string)
|
4. **`isEscalated` (boolean)**
|
||||||
- Classify the ticket into ONE of the following areas based on keywords:
|
* Set to `true` if the user explicitly states they are escalating, mentions previous unanswered requests, or if the tone is highly frustrated and urgent.
|
||||||
- `"Direct Channel"`: "REST API", "API Gateway", "Create/Update HCP/HCO"
|
* Set to `false` otherwise.
|
||||||
- `"Streaming Channel"`: "Kafka", "SQS", "Reltio events", "Snowflake"
|
|
||||||
- `"Java Batch Channel"`: "Batch", "File loader", "Airflow"
|
5. **`oneSentenceSummary` (string)**
|
||||||
- `"ETL Batch Channel"`: "ETL", "Informatica"
|
* A single paragraph in concise English that summarizes the discussion.
|
||||||
- `"DCR Service"`: "DCR", "PforceRx", "OneKey", "Veeva"
|
|
||||||
- `"API Gateway"`: "Kong", "authentication", "routing"
|
|
||||||
- `"Callback Service"`: "Callback", "HCO names", "ranking"
|
|
||||||
- `"Publisher"`: "Publisher", "routing rules"
|
|
||||||
- `"Reconciliation"`: "Reconciliation", "sync"
|
|
||||||
- `"Snowflake"`: "Snowflake", "Data Mart", "SQL"
|
|
||||||
- `"Authentication"`: "PingFederate", "OAuth2", "Key-Auth"
|
|
||||||
- `"Other"`: If it doesn't fit any other category.
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
### Input:
|
### Input:
|
||||||
- Summary: "DCR Rejected by OneKey"
|
- Summary: "DCR Rejected by OneKey"
|
||||||
- Description: "Our DCR for PforceRx was rejected by OneKey. Can the MDM HUB team investigate?"
|
- Description: "Our DCR for PforceRx was rejected by OneKey. Can the MDM HUB team investigate? This is blocking our weekly report."
|
||||||
- Comment: ""
|
- Comment: ""
|
||||||
|
|
||||||
### Output:
|
### Output:
|
||||||
```json
|
```json
|
||||||
{{
|
{{
|
||||||
"Hasmultipleescalations": false,
|
"issueCategory": "technical_issue",
|
||||||
"CustomerSentiment": "neutral",
|
"area": "Application Service",
|
||||||
"IssueCategory": "technical_issue",
|
"customerSentiment": "frustrated",
|
||||||
"Area": "DCR Service"
|
"isEscalated": false,
|
||||||
|
"oneSentenceSummary": "A DCR for PforceRx was rejected by OneKey, which is blocking a weekly report."
|
||||||
}}
|
}}
|
||||||
```
|
|
||||||
|
|
||||||
USER:
|
USER:
|
||||||
Analyze the following Jira ticket:
|
Analyze the following Jira ticket:
|
||||||
|
|||||||
@ -57,11 +57,13 @@ class JiraWebhookPayload(BaseModel):
|
|||||||
updated: Optional[str] = None
|
updated: Optional[str] = None
|
||||||
|
|
||||||
class AnalysisFlags(BaseModel):
|
class AnalysisFlags(BaseModel):
|
||||||
hasMultipleEscalations: bool = Field(alias="Hasmultipleescalations", description="Is there evidence of multiple escalation attempts?")
|
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)
|
||||||
customerSentiment: Optional[CustomerSentiment] = Field(alias="CustomerSentiment", description="Overall customer sentiment (e.g., 'neutral', 'frustrated', 'calm').")
|
|
||||||
# New: Add category and area fields
|
issueCategory: IssueCategory = Field(..., description="The primary category of the Jira ticket.")
|
||||||
issueCategory: IssueCategory = Field(alias="IssueCategory", description="The primary category of the Jira ticket.")
|
area: Area = Field(..., description="The technical area of the MDM HUB related to the issue.")
|
||||||
area: Area = Field(alias="Area", description="The technical area of the MDM HUB related to the issue.")
|
customerSentiment: Optional[CustomerSentiment] = Field(..., description="Overall customer sentiment (e.g., 'neutral', 'frustrated', 'calm').")
|
||||||
|
isEscalated: bool = Field(..., description="Is there evidence of multiple escalation attempts?")
|
||||||
|
oneSentenceSummary: str = Field(..., description="A single paragraph in concise English that summarizes the discussion.")
|
||||||
|
|
||||||
|
|
||||||
class JiraAnalysisResponse(BaseModel):
|
class JiraAnalysisResponse(BaseModel):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user