feat: AI auto-fix + Gemini Flash provider #93

Merged
chihlasm merged 14 commits from feat/ai-autofix-gemini into main 2026-02-27 07:32:24 +00:00
2 changed files with 26 additions and 1 deletions
Showing only changes of commit 6fc76187c0 - Show all commits

View File

@@ -5,10 +5,13 @@ Supports Gemini (google-genai) and Anthropic (anthropic) as interchangeable
backends for JSON generation used by the AI Flow Builder. backends for JSON generation used by the AI Flow Builder.
""" """
import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from app.core.config import settings from app.core.config import settings
logger = logging.getLogger(__name__)
class AIProvider(ABC): class AIProvider(ABC):
"""Abstract base class for AI providers.""" """Abstract base class for AI providers."""
@@ -74,6 +77,16 @@ class GeminiProvider(AIProvider):
config=config, config=config,
) )
# Log finish reason to detect truncation
if response.candidates:
finish_reason = getattr(response.candidates[0], "finish_reason", None)
logger.info("Gemini finish_reason=%s model=%s", finish_reason, self._model)
if str(finish_reason) == "MAX_TOKENS":
logger.warning(
"Gemini output truncated (MAX_TOKENS). max_output_tokens=%d",
max_tokens,
)
text = response.text or "" text = response.text or ""
input_tokens = getattr(response.usage_metadata, "prompt_token_count", 0) or 0 input_tokens = getattr(response.usage_metadata, "prompt_token_count", 0) or 0
output_tokens = ( output_tokens = (

View File

@@ -154,15 +154,23 @@ async def scaffold_branches(
raw_text, input_tokens, output_tokens = await provider.generate_json( raw_text, input_tokens, output_tokens = await provider.generate_json(
system_prompt=SCAFFOLD_SYSTEM_PROMPT, system_prompt=SCAFFOLD_SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_message}], messages=[{"role": "user", "content": user_message}],
max_tokens=1024, max_tokens=2048,
) )
logger.info(
"scaffold raw response (tokens in=%d out=%d, len=%d): %s",
input_tokens,
output_tokens,
len(raw_text),
raw_text[:500],
)
raw_text = _strip_markdown_fences(raw_text) raw_text = _strip_markdown_fences(raw_text)
cost = _estimate_cost(input_tokens, output_tokens) cost = _estimate_cost(input_tokens, output_tokens)
try: try:
data = json.loads(raw_text) data = json.loads(raw_text)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logger.error("scaffold JSON parse failed. Full text (%d chars): %s", len(raw_text), raw_text)
raise ValueError(f"AI returned invalid JSON: {e}") raise ValueError(f"AI returned invalid JSON: {e}")
branches = data.get("branches", []) branches = data.get("branches", [])
@@ -224,6 +232,10 @@ async def generate_branch_detail(
try: try:
branch_tree = json.loads(raw_text) branch_tree = json.loads(raw_text)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logger.error(
"branch_detail attempt=%d JSON parse failed (%d chars): %s",
attempt, len(raw_text), raw_text[:500],
)
if attempt < 2: if attempt < 2:
messages.append({"role": "assistant", "content": raw_text}) messages.append({"role": "assistant", "content": raw_text})
messages.append({ messages.append({