Zeta / src /core /llm_engine.py
Owadokun Tosin Tobi
Update llm_engine.py
2a07ca7 unverified
import os
import json
import yaml
from pathlib import Path
from typing import List, Dict, Any
import google.generativeai as genai
from loguru import logger
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from dotenv import load_dotenv
# Load Env
load_dotenv()
API_KEY = os.getenv("GEMINI_API_KEY")
if not API_KEY:
logger.critical("GEMINI_API_KEY is missing")
raise ValueError("GEMINI_API_KEY not found in environment")
genai.configure(api_key=API_KEY)
class LLMEngine:
"""
LLM Engine with Externalized Configuration.
Loads prompts from config/prompts.yaml to ensure code/data separation.
"""
def __init__(self, model_name: str = "gemini-2.5-flash"):
self.model_name = model_name
self.model = genai.GenerativeModel(model_name)
self.prompts = self._load_prompts()
logger.info(f"LLM Engine initialized with {model_name}")
def _load_prompts(self) -> Dict[str, Any]:
"""Loads prompt templates from the config directory."""
# Resolves path relative to this file: src/core/../../config/prompts.yaml
base_path = Path(__file__).resolve().parent.parent.parent
config_path = base_path / "config" / "prompts.yaml"
if not config_path.exists():
raise FileNotFoundError(f"Configuration file not found at: {config_path}")
with open(config_path, "r") as f:
return yaml.safe_load(f)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10),
retry=retry_if_exception_type(Exception),
reraise=True
)
async def generate_test_cases(self, requirements_text: str) -> List[Dict[str, Any]]:
logger.debug("Constructing prompt from configuration...")
try:
# Load config sections
config = self.prompts["test_generation"]
# 1. System Role & Context
system_role = config.get("system_role", "")
# 2. Main Instruction (Injecting the dynamic text)
instruction = config.get("instruction", "").format(
requirements_text=requirements_text[:30000]
)
# 3. Few-Shot Examples (NEW: Capturing your expanded section)
examples = config.get("few_shot_examples", "")
# 4. Output Schema
fmt = config.get("output_format", "")
# Construct the Full Context Window
# Order matters: Role -> Task -> Examples -> Strict JSON Format
full_prompt = (
f"{system_role}\n\n"
f"{instruction}\n\n"
f"### REFERENCE EXAMPLES:\n{examples}\n\n"
f"### REQUIRED OUTPUT FORMAT:\n{fmt}"
)
# Async call
response = await self.model.generate_content_async(full_prompt)
return self._parse_json_response(response.text)
except KeyError as e:
logger.error(f"Missing configuration key in prompts.yaml: {e}")
raise e
except Exception as e:
logger.error(f"LLM Generation Failed: {e}")
raise e
def _parse_json_response(self, text: str) -> List[Dict[str, Any]]:
clean_text = text.strip()
if clean_text.startswith("```json"):
clean_text = clean_text[7:-3]
elif clean_text.startswith("```"):
clean_text = clean_text[3:-3]
try:
data = json.loads(clean_text)
if not isinstance(data, list):
raise ValueError("Output is not a list of test cases")
return data
except json.JSONDecodeError as e:
logger.error("Failed to decode JSON response")
raise ValueError(f"Malformed JSON from LLM: {e}")