refactor: uv-based deps, no nexus auth, LLM retries, lock cleanup, health checks, e2e tests
This commit is contained in:
@@ -36,15 +36,8 @@ def _build_user_message(finding: dict) -> str:
|
||||
return prompt
|
||||
|
||||
|
||||
async def analyze_finding(finding_data: dict) -> dict | None:
|
||||
"""Send a finding to the LLM for security analysis.
|
||||
|
||||
Returns parsed JSON dict on success, or None on failure.
|
||||
"""
|
||||
if not config.llm_api_key:
|
||||
log.warning("LLM_API_KEY not set — skipping LLM analysis")
|
||||
return None
|
||||
|
||||
async def _attempt_llm_call(finding_data: dict) -> dict | None:
|
||||
"""Single attempt to call LLM and parse response."""
|
||||
url = f"{config.llm_api_base.rstrip('/')}/chat/completions"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {config.llm_api_key}",
|
||||
@@ -78,12 +71,21 @@ async def analyze_finding(finding_data: dict) -> dict | None:
|
||||
return None
|
||||
|
||||
try:
|
||||
content = body["choices"][0]["message"]["content"]
|
||||
choices = body.get("choices", [])
|
||||
if not choices:
|
||||
raise ValueError("Empty choices list")
|
||||
message = choices[0].get("message", {})
|
||||
content = message.get("content", "")
|
||||
if not content:
|
||||
raise ValueError("Empty message content")
|
||||
return json.loads(content)
|
||||
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
||||
except (ValueError, json.JSONDecodeError) as e:
|
||||
raw = ""
|
||||
try:
|
||||
raw = body["choices"][0]["message"]["content"]
|
||||
choices = body.get("choices", [])
|
||||
if choices:
|
||||
message = choices[0].get("message", {})
|
||||
raw = message.get("content", "")
|
||||
except (KeyError, IndexError):
|
||||
raw = str(body)[:300]
|
||||
# Some models wrap JSON in markdown code blocks
|
||||
@@ -102,3 +104,32 @@ async def analyze_finding(finding_data: dict) -> dict | None:
|
||||
raw[:200] if isinstance(raw, str) else str(raw)[:200],
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def analyze_finding(finding_data: dict, max_retries: int = 3) -> dict | None:
|
||||
"""Send a finding to the LLM for security analysis with retry logic.
|
||||
|
||||
Returns parsed JSON dict on success, or None on failure.
|
||||
"""
|
||||
if not config.llm_api_key:
|
||||
log.warning("LLM_API_KEY not set — skipping LLM analysis")
|
||||
return None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
result = await _attempt_llm_call(finding_data)
|
||||
if result is not None:
|
||||
return result
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(2**attempt * 2) # 2s, 4s, 8s
|
||||
log.info(
|
||||
"Retrying LLM analysis for rule=%s (attempt %d)",
|
||||
finding_data.get("rule"),
|
||||
attempt + 2,
|
||||
)
|
||||
|
||||
log.error(
|
||||
"LLM analysis failed after %d attempts for rule=%s",
|
||||
max_retries,
|
||||
finding_data.get("rule"),
|
||||
)
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user