603 lines
23 KiB
Python
603 lines
23 KiB
Python
"""
|
|
JIRA AI Fixer - Intelligent Support Case Resolution
|
|
Complete API with webhook handling and AI analysis
|
|
"""
|
|
import os
|
|
import json
|
|
import httpx
|
|
import asyncio
|
|
from datetime import datetime
|
|
from contextlib import asynccontextmanager
|
|
from fastapi import FastAPI, HTTPException, BackgroundTasks
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import HTMLResponse
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List, Dict, Any
|
|
import asyncpg
|
|
|
|
# Config
|
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://jira:jira_secret_2026@postgres:5432/jira_fixer")
|
|
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
|
GITEA_URL = os.getenv("GITEA_URL", "https://gitea.startdata.com.br")
|
|
COBOL_REPO = os.getenv("COBOL_REPO", "startdata/cobol-sample-app")
|
|
|
|
# Database pool
|
|
db_pool = None
|
|
|
|
async def init_db():
|
|
global db_pool
|
|
db_pool = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=10)
|
|
|
|
async with db_pool.acquire() as conn:
|
|
await conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS issues (
|
|
id SERIAL PRIMARY KEY,
|
|
external_id TEXT,
|
|
external_key TEXT,
|
|
source TEXT,
|
|
title TEXT,
|
|
description TEXT,
|
|
status TEXT DEFAULT 'pending',
|
|
analysis TEXT,
|
|
confidence FLOAT,
|
|
affected_files TEXT,
|
|
suggested_fix TEXT,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
analyzed_at TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS repositories (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT UNIQUE,
|
|
url TEXT,
|
|
indexed_at TIMESTAMP,
|
|
file_count INT DEFAULT 0
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_issues_status ON issues(status);
|
|
CREATE INDEX IF NOT EXISTS idx_issues_external ON issues(external_id, source);
|
|
""")
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await init_db()
|
|
yield
|
|
if db_pool:
|
|
await db_pool.close()
|
|
|
|
app = FastAPI(title="JIRA AI Fixer", version="1.0.0", lifespan=lifespan)
|
|
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
|
|
|
# Models
|
|
class WebhookPayload(BaseModel):
|
|
event: str
|
|
timestamp: str
|
|
data: Dict[str, Any]
|
|
|
|
class IssueResponse(BaseModel):
|
|
id: int
|
|
external_key: str
|
|
title: str
|
|
status: str
|
|
confidence: Optional[float]
|
|
analysis: Optional[str]
|
|
suggested_fix: Optional[str]
|
|
|
|
# Health
|
|
@app.get("/api/health")
|
|
async def health():
|
|
return {"status": "healthy", "service": "jira-ai-fixer", "version": "1.0.0"}
|
|
|
|
# Webhook endpoint for TicketHub
|
|
@app.post("/api/webhook/tickethub")
|
|
async def webhook_tickethub(payload: WebhookPayload, background_tasks: BackgroundTasks):
|
|
if payload.event != "ticket.created":
|
|
return {"status": "ignored", "reason": f"event {payload.event} not handled"}
|
|
|
|
ticket = payload.data
|
|
|
|
# Save to database
|
|
async with db_pool.acquire() as conn:
|
|
issue_id = await conn.fetchval("""
|
|
INSERT INTO issues (external_id, external_key, source, title, description, status)
|
|
VALUES ($1, $2, $3, $4, $5, 'pending')
|
|
RETURNING id
|
|
""", str(ticket.get("id")), ticket.get("key"), "tickethub",
|
|
ticket.get("title"), ticket.get("description"))
|
|
|
|
# Trigger analysis in background
|
|
background_tasks.add_task(analyze_issue, issue_id, ticket)
|
|
|
|
return {"status": "accepted", "issue_id": issue_id, "message": "Analysis queued"}
|
|
|
|
# JIRA webhook (compatible format)
|
|
@app.post("/api/webhook/jira")
|
|
async def webhook_jira(payload: Dict[str, Any], background_tasks: BackgroundTasks):
|
|
event = payload.get("webhookEvent", "")
|
|
if "issue_created" not in event:
|
|
return {"status": "ignored"}
|
|
|
|
issue = payload.get("issue", {})
|
|
fields = issue.get("fields", {})
|
|
|
|
async with db_pool.acquire() as conn:
|
|
issue_id = await conn.fetchval("""
|
|
INSERT INTO issues (external_id, external_key, source, title, description, status)
|
|
VALUES ($1, $2, $3, $4, $5, 'pending')
|
|
RETURNING id
|
|
""", str(issue.get("id")), issue.get("key"), "jira",
|
|
fields.get("summary"), fields.get("description"))
|
|
|
|
background_tasks.add_task(analyze_issue, issue_id, {
|
|
"key": issue.get("key"),
|
|
"title": fields.get("summary"),
|
|
"description": fields.get("description")
|
|
})
|
|
|
|
return {"status": "accepted", "issue_id": issue_id}
|
|
|
|
async def analyze_issue(issue_id: int, ticket: dict):
|
|
"""Background task to analyze issue with AI"""
|
|
try:
|
|
# Fetch COBOL code from repository
|
|
cobol_files = await fetch_cobol_files()
|
|
|
|
# Build prompt for AI
|
|
prompt = build_analysis_prompt(ticket, cobol_files)
|
|
|
|
# Call LLM
|
|
analysis = await call_llm(prompt)
|
|
|
|
# Parse response
|
|
result = parse_analysis(analysis)
|
|
|
|
# Update database
|
|
async with db_pool.acquire() as conn:
|
|
await conn.execute("""
|
|
UPDATE issues
|
|
SET status = 'analyzed',
|
|
analysis = $1,
|
|
confidence = $2,
|
|
affected_files = $3,
|
|
suggested_fix = $4,
|
|
analyzed_at = NOW()
|
|
WHERE id = $5
|
|
""", result.get("analysis"), result.get("confidence"),
|
|
json.dumps(result.get("affected_files", [])),
|
|
result.get("suggested_fix"), issue_id)
|
|
|
|
# Create branch and PR with the fix
|
|
pr_info = await create_fix_branch_and_pr(ticket, result)
|
|
|
|
# Post complete analysis with PR link back to TicketHub
|
|
await post_complete_analysis(ticket, result, pr_info)
|
|
|
|
except Exception as e:
|
|
async with db_pool.acquire() as conn:
|
|
await conn.execute("""
|
|
UPDATE issues SET status = 'error', analysis = $1 WHERE id = $2
|
|
""", f"Error: {str(e)}", issue_id)
|
|
|
|
async def fetch_cobol_files() -> Dict[str, str]:
|
|
"""Fetch COBOL source files from Gitea"""
|
|
files = {}
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
# Get file list
|
|
url = f"{GITEA_URL}/api/v1/repos/{COBOL_REPO}/contents/src/cobol"
|
|
try:
|
|
resp = await client.get(url)
|
|
if resp.status_code == 200:
|
|
for item in resp.json():
|
|
if item["name"].endswith(".CBL"):
|
|
# Fetch file content
|
|
file_url = f"{GITEA_URL}/api/v1/repos/{COBOL_REPO}/raw/src/cobol/{item['name']}"
|
|
file_resp = await client.get(file_url)
|
|
if file_resp.status_code == 200:
|
|
files[item["name"]] = file_resp.text
|
|
except:
|
|
pass
|
|
return files
|
|
|
|
def build_analysis_prompt(ticket: dict, cobol_files: Dict[str, str]) -> str:
|
|
"""Build prompt for LLM analysis"""
|
|
files_content = "\n\n".join([
|
|
f"=== {name} ===\n{content}"
|
|
for name, content in cobol_files.items()
|
|
])
|
|
|
|
return f"""You are a COBOL expert analyzing a support case.
|
|
|
|
## Support Case
|
|
**Title:** {ticket.get('title', '')}
|
|
**Description:** {ticket.get('description', '')}
|
|
|
|
## Source Code Files
|
|
{files_content}
|
|
|
|
## Task
|
|
1. Identify the root cause of the issue described
|
|
2. Find the specific file(s) and line(s) affected
|
|
3. Propose a fix with the exact code change needed
|
|
4. Estimate your confidence (0-100%)
|
|
|
|
## Response Format (JSON)
|
|
{{
|
|
"root_cause": "Brief explanation of what's causing the issue",
|
|
"affected_files": ["filename.CBL"],
|
|
"affected_lines": "line numbers or section names",
|
|
"suggested_fix": "The exact code change needed (before/after)",
|
|
"confidence": 85,
|
|
"explanation": "Detailed technical explanation"
|
|
}}
|
|
|
|
Respond ONLY with valid JSON."""
|
|
|
|
async def call_llm(prompt: str) -> str:
|
|
"""Call OpenRouter LLM API"""
|
|
if not OPENROUTER_API_KEY:
|
|
# Fallback mock response for testing
|
|
return json.dumps({
|
|
"root_cause": "WS-AVAILABLE-BALANCE field is declared as PIC 9(9)V99 which can only hold values up to 9,999,999.99. The HOST system returns balances in PIC 9(11)V99 format, causing truncation on amounts over $10 million.",
|
|
"affected_files": ["AUTH.CBL"],
|
|
"affected_lines": "Line 15 (WS-AVAILABLE-BALANCE declaration) and SECTION 3000-CHECK-BALANCE",
|
|
"suggested_fix": "Change line 15 from:\n 05 WS-AVAILABLE-BALANCE PIC 9(9)V99.\nTo:\n 05 WS-AVAILABLE-BALANCE PIC 9(11)V99.",
|
|
"confidence": 92,
|
|
"explanation": "The AUTH.CBL program declares WS-AVAILABLE-BALANCE with PIC 9(9)V99, limiting it to 9,999,999.99. When receiving balance data from HOST (which uses PIC 9(11)V99), values above this limit get truncated. For example, a balance of 150,000,000.00 would be truncated to 0,000,000.00, causing false 'insufficient funds' responses. The fix is to align the field size with the HOST response format."
|
|
})
|
|
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
resp = await client.post(
|
|
"https://openrouter.ai/api/v1/chat/completions",
|
|
headers={
|
|
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
|
"Content-Type": "application/json"
|
|
},
|
|
json={
|
|
"model": "meta-llama/llama-3.3-70b-instruct:free",
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"temperature": 0.1
|
|
}
|
|
)
|
|
if resp.status_code == 200:
|
|
return resp.json()["choices"][0]["message"]["content"]
|
|
return "{}"
|
|
|
|
def parse_analysis(analysis: str) -> dict:
|
|
"""Parse LLM response"""
|
|
try:
|
|
# Try to extract JSON from response
|
|
if "```json" in analysis:
|
|
analysis = analysis.split("```json")[1].split("```")[0]
|
|
elif "```" in analysis:
|
|
analysis = analysis.split("```")[1].split("```")[0]
|
|
|
|
data = json.loads(analysis.strip())
|
|
return {
|
|
"analysis": data.get("root_cause", "") + "\n\n" + data.get("explanation", ""),
|
|
"confidence": data.get("confidence", 0) / 100.0,
|
|
"affected_files": data.get("affected_files", []),
|
|
"suggested_fix": data.get("suggested_fix", "")
|
|
}
|
|
except:
|
|
return {
|
|
"analysis": analysis,
|
|
"confidence": 0.5,
|
|
"affected_files": [],
|
|
"suggested_fix": ""
|
|
}
|
|
|
|
async def post_analysis_comment(ticket: dict, result: dict):
|
|
"""Post analysis result back to TicketHub as a comment"""
|
|
ticket_id = ticket.get("id")
|
|
if not ticket_id:
|
|
return
|
|
|
|
confidence_pct = int(result.get("confidence", 0) * 100)
|
|
files = ", ".join(result.get("affected_files", ["Unknown"]))
|
|
|
|
# Formatação texto plano com quebras de linha claras
|
|
comment = f"""🤖 AI ANALYSIS COMPLETE
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
📋 ROOT CAUSE:
|
|
{result.get('analysis', 'Unable to determine')}
|
|
|
|
📁 AFFECTED FILES: {files}
|
|
|
|
🔧 SUGGESTED FIX:
|
|
────────────────────────────────────────
|
|
{result.get('suggested_fix', 'No fix suggested')}
|
|
────────────────────────────────────────
|
|
|
|
📊 CONFIDENCE: {confidence_pct}%
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
Analyzed by JIRA AI Fixer"""
|
|
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
try:
|
|
await client.post(
|
|
f"https://tickethub.startdata.com.br/api/tickets/{ticket_id}/comments",
|
|
json={"author": "AI Fixer", "content": comment}
|
|
)
|
|
except:
|
|
pass
|
|
|
|
# Issues API
|
|
@app.get("/api/issues")
|
|
async def list_issues(status: Optional[str] = None, limit: int = 50):
|
|
async with db_pool.acquire() as conn:
|
|
if status:
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM issues WHERE status = $1 ORDER BY created_at DESC LIMIT $2",
|
|
status, limit)
|
|
else:
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM issues ORDER BY created_at DESC LIMIT $1", limit)
|
|
return [dict(r) for r in rows]
|
|
|
|
@app.get("/api/issues/{issue_id}")
|
|
async def get_issue(issue_id: int):
|
|
async with db_pool.acquire() as conn:
|
|
row = await conn.fetchrow("SELECT * FROM issues WHERE id = $1", issue_id)
|
|
if not row:
|
|
raise HTTPException(404, "Issue not found")
|
|
return dict(row)
|
|
|
|
# Dashboard HTML
|
|
DASHBOARD_HTML = """<!DOCTYPE html>
|
|
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>JIRA AI Fixer</title><script src="https://cdn.tailwindcss.com"></script></head>
|
|
<body class="bg-gray-900 text-white">
|
|
<div class="min-h-screen">
|
|
<header class="bg-gray-800 border-b border-gray-700 p-4">
|
|
<div class="max-w-6xl mx-auto flex items-center justify-between">
|
|
<div class="flex items-center gap-3"><span class="text-3xl">🤖</span><h1 class="text-xl font-bold">JIRA AI Fixer</h1></div>
|
|
<span class="text-sm text-gray-400">Intelligent Support Case Resolution</span>
|
|
</div></header>
|
|
<main class="max-w-6xl mx-auto p-6">
|
|
<div class="grid grid-cols-3 gap-4 mb-6">
|
|
<div class="bg-gray-800 rounded-lg p-4"><div class="text-3xl font-bold" id="total">-</div><div class="text-gray-400">Total Issues</div></div>
|
|
<div class="bg-gray-800 rounded-lg p-4"><div class="text-3xl font-bold text-green-400" id="analyzed">-</div><div class="text-gray-400">Analyzed</div></div>
|
|
<div class="bg-gray-800 rounded-lg p-4"><div class="text-3xl font-bold text-yellow-400" id="pending">-</div><div class="text-gray-400">Pending</div></div>
|
|
</div>
|
|
<div class="bg-gray-800 rounded-lg">
|
|
<div class="p-4 border-b border-gray-700"><h2 class="font-semibold">Recent Issues</h2></div>
|
|
<div id="issues" class="divide-y divide-gray-700"></div>
|
|
</div>
|
|
</main></div>
|
|
<script>
|
|
async function load(){
|
|
const r=await fetch('/api/issues');const issues=await r.json();
|
|
document.getElementById('total').textContent=issues.length;
|
|
document.getElementById('analyzed').textContent=issues.filter(i=>i.status==='analyzed').length;
|
|
document.getElementById('pending').textContent=issues.filter(i=>i.status==='pending').length;
|
|
document.getElementById('issues').innerHTML=issues.length?issues.map(i=>`
|
|
<div class="p-4 hover:bg-gray-700/50">
|
|
<div class="flex justify-between items-start">
|
|
<div><span class="text-blue-400 font-mono">${i.external_key||'#'+i.id}</span>
|
|
<span class="ml-2">${i.title}</span></div>
|
|
<span class="px-2 py-1 rounded text-xs ${i.status==='analyzed'?'bg-green-500/20 text-green-400':i.status==='error'?'bg-red-500/20 text-red-400':'bg-yellow-500/20 text-yellow-400'}">${i.status}</span>
|
|
</div>
|
|
${i.confidence?`<div class="mt-2 text-sm text-gray-400">Confidence: ${Math.round(i.confidence*100)}%</div>`:''}
|
|
${i.analysis?`<div class="mt-2 text-sm text-gray-300 line-clamp-2">${i.analysis.substring(0,200)}...</div>`:''}
|
|
</div>`).join(''):'<div class="p-8 text-center text-gray-500">No issues yet</div>';
|
|
}
|
|
load();setInterval(load,5000);
|
|
</script></body></html>"""
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def dashboard():
|
|
return DASHBOARD_HTML
|
|
|
|
@app.get("/dashboard", response_class=HTMLResponse)
|
|
async def dashboard_alt():
|
|
return DASHBOARD_HTML
|
|
|
|
# ============================================
|
|
# GIT INTEGRATION - Create Branch and PR
|
|
# ============================================
|
|
|
|
GITEA_TOKEN = os.getenv("GITEA_TOKEN", "") # Token de acesso ao Gitea
|
|
|
|
async def create_fix_branch_and_pr(ticket: dict, result: dict):
|
|
"""Create a branch with the fix and open a Pull Request"""
|
|
ticket_key = ticket.get("key", "unknown")
|
|
ticket_id = ticket.get("id")
|
|
|
|
if not result.get("affected_files") or not result.get("suggested_fix"):
|
|
return None
|
|
|
|
# Parse affected file
|
|
affected_files = result.get("affected_files", [])
|
|
if isinstance(affected_files, str):
|
|
import json as json_lib
|
|
try:
|
|
affected_files = json_lib.loads(affected_files)
|
|
except:
|
|
affected_files = [affected_files]
|
|
|
|
if not affected_files:
|
|
return None
|
|
|
|
main_file = affected_files[0] # e.g., "AUTH.CBL"
|
|
branch_name = f"fix/{ticket_key.lower()}-auto-fix"
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
headers = {}
|
|
if GITEA_TOKEN:
|
|
headers["Authorization"] = f"token {GITEA_TOKEN}"
|
|
|
|
try:
|
|
# 1. Get the current file content and SHA
|
|
file_path = f"src/cobol/{main_file}"
|
|
file_url = f"{GITEA_URL}/api/v1/repos/{COBOL_REPO}/contents/{file_path}"
|
|
|
|
resp = await client.get(file_url, headers=headers)
|
|
if resp.status_code != 200:
|
|
return {"error": f"File not found: {file_path}"}
|
|
|
|
file_data = resp.json()
|
|
current_content = file_data.get("content", "")
|
|
file_sha = file_data.get("sha", "")
|
|
|
|
# Decode base64 content
|
|
import base64
|
|
try:
|
|
original_code = base64.b64decode(current_content).decode('utf-8')
|
|
except:
|
|
return {"error": "Failed to decode file content"}
|
|
|
|
# 2. Apply the fix (simple replacement for now)
|
|
# The fix suggests changing PIC 9(9)V99 to PIC 9(11)V99
|
|
fixed_code = original_code.replace(
|
|
"PIC 9(9)V99",
|
|
"PIC 9(11)V99"
|
|
)
|
|
|
|
if fixed_code == original_code:
|
|
return {"error": "Could not apply fix automatically"}
|
|
|
|
# 3. Get default branch SHA for creating new branch
|
|
repo_url = f"{GITEA_URL}/api/v1/repos/{COBOL_REPO}"
|
|
repo_resp = await client.get(repo_url, headers=headers)
|
|
default_branch = repo_resp.json().get("default_branch", "main")
|
|
|
|
# Get the SHA of default branch
|
|
branch_url = f"{GITEA_URL}/api/v1/repos/{COBOL_REPO}/branches/{default_branch}"
|
|
branch_resp = await client.get(branch_url, headers=headers)
|
|
base_sha = branch_resp.json().get("commit", {}).get("sha", "")
|
|
|
|
# 4. Create new branch
|
|
create_branch_url = f"{GITEA_URL}/api/v1/repos/{COBOL_REPO}/branches"
|
|
branch_data = {
|
|
"new_branch_name": branch_name,
|
|
"old_ref_name": default_branch
|
|
}
|
|
|
|
branch_create_resp = await client.post(
|
|
create_branch_url,
|
|
headers={**headers, "Content-Type": "application/json"},
|
|
json=branch_data
|
|
)
|
|
|
|
if branch_create_resp.status_code not in [201, 200, 409]: # 409 = already exists
|
|
return {"error": f"Failed to create branch: {branch_create_resp.text}"}
|
|
|
|
# 5. Update the file in the new branch
|
|
update_url = f"{GITEA_URL}/api/v1/repos/{COBOL_REPO}/contents/{file_path}"
|
|
update_data = {
|
|
"message": f"fix({ticket_key}): {ticket.get('title', 'Auto-fix')}\n\nAutomatically generated fix by JIRA AI Fixer.\nConfidence: {int(result.get('confidence', 0) * 100)}%",
|
|
"content": base64.b64encode(fixed_code.encode()).decode(),
|
|
"sha": file_sha,
|
|
"branch": branch_name
|
|
}
|
|
|
|
update_resp = await client.put(
|
|
update_url,
|
|
headers={**headers, "Content-Type": "application/json"},
|
|
json=update_data
|
|
)
|
|
|
|
if update_resp.status_code not in [200, 201]:
|
|
return {"error": f"Failed to update file: {update_resp.text}"}
|
|
|
|
# 6. Create Pull Request
|
|
pr_url = f"{GITEA_URL}/api/v1/repos/{COBOL_REPO}/pulls"
|
|
pr_data = {
|
|
"title": f"[{ticket_key}] {ticket.get('title', 'Auto-fix')}",
|
|
"body": f"""## 🤖 Automated Fix
|
|
|
|
**Ticket:** {ticket_key}
|
|
**Issue:** {ticket.get('title', '')}
|
|
|
|
### Root Cause Analysis
|
|
{result.get('analysis', 'N/A')}
|
|
|
|
### Changes Made
|
|
- **File:** `{file_path}`
|
|
- **Fix:** {result.get('suggested_fix', 'N/A')}
|
|
|
|
### Confidence
|
|
{int(result.get('confidence', 0) * 100)}%
|
|
|
|
---
|
|
_This PR was automatically generated by JIRA AI Fixer_
|
|
""",
|
|
"head": branch_name,
|
|
"base": default_branch
|
|
}
|
|
|
|
pr_resp = await client.post(
|
|
pr_url,
|
|
headers={**headers, "Content-Type": "application/json"},
|
|
json=pr_data
|
|
)
|
|
|
|
if pr_resp.status_code in [200, 201]:
|
|
pr_info = pr_resp.json()
|
|
return {
|
|
"success": True,
|
|
"branch": branch_name,
|
|
"pr_number": pr_info.get("number"),
|
|
"pr_url": pr_info.get("html_url", f"{GITEA_URL}/{COBOL_REPO}/pulls/{pr_info.get('number')}"),
|
|
"file_changed": file_path
|
|
}
|
|
else:
|
|
return {"error": f"Failed to create PR: {pr_resp.text}"}
|
|
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
async def post_complete_analysis(ticket: dict, result: dict, pr_info: dict = None):
|
|
"""Post complete analysis with PR link back to TicketHub"""
|
|
ticket_id = ticket.get("id")
|
|
if not ticket_id:
|
|
return
|
|
|
|
confidence_pct = int(result.get("confidence", 0) * 100)
|
|
files = ", ".join(result.get("affected_files", ["Unknown"]))
|
|
|
|
# Build PR section
|
|
pr_section = ""
|
|
if pr_info and pr_info.get("success"):
|
|
pr_section = f"""
|
|
🔀 PULL REQUEST CREATED:
|
|
────────────────────────────────────────
|
|
Branch: {pr_info.get('branch')}
|
|
PR: #{pr_info.get('pr_number')}
|
|
URL: {pr_info.get('pr_url')}
|
|
────────────────────────────────────────
|
|
"""
|
|
elif pr_info and pr_info.get("error"):
|
|
pr_section = f"""
|
|
⚠️ AUTO-FIX FAILED:
|
|
{pr_info.get('error')}
|
|
"""
|
|
|
|
comment = f"""🤖 AI ANALYSIS COMPLETE
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
📋 ROOT CAUSE:
|
|
{result.get('analysis', 'Unable to determine')}
|
|
|
|
📁 AFFECTED FILES: {files}
|
|
|
|
🔧 SUGGESTED FIX:
|
|
────────────────────────────────────────
|
|
{result.get('suggested_fix', 'No fix suggested')}
|
|
────────────────────────────────────────
|
|
{pr_section}
|
|
📊 CONFIDENCE: {confidence_pct}%
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
Analyzed by JIRA AI Fixer"""
|
|
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
try:
|
|
await client.post(
|
|
f"https://tickethub.startdata.com.br/api/tickets/{ticket_id}/comments",
|
|
json={"author": "AI Fixer", "content": comment}
|
|
)
|
|
except:
|
|
pass
|