Spaces:
Sleeping
Sleeping
File size: 7,920 Bytes
75c7554 ebe0bd3 75c7554 a31af37 75c7554 13c01ef 75c7554 ebe0bd3 ee949bc 75c7554 ebe0bd3 81dfbda 5960eed ebe0bd3 75c7554 9ae498c 75c7554 13c01ef a31af37 13c01ef a31af37 13c01ef a31af37 9a61905 a31af37 13c01ef 75c7554 9a61905 a31af37 75c7554 a31af37 75c7554 a31af37 75c7554 a31af37 75c7554 a31af37 75c7554 a31af37 75c7554 a31af37 75c7554 5960eed ee949bc 5960eed ee949bc 5960eed ee949bc ebe0bd3 5960eed ebe0bd3 75c7554 ebe0bd3 75c7554 71a770c 13c01ef 5960eed 13c01ef 5960eed a31af37 5960eed a31af37 75c7554 9f270f4 71a770c 9f270f4 ebe0bd3 81dfbda ebe0bd3 71a770c 6149154 a31af37 6149154 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | """
LLM Evaluator – analyzes BESS-RL evaluation results using the Hugging Face Router.
Includes a heuristic fallback for when API quotas are exceeded.
All models are routed via the Hugging Face OpenAI-compatible API.
"""
import os
import json
import re
import requests
import openai
from dotenv import load_dotenv
# Load environment variables from .env
load_dotenv()
# Default Model IDs for the HF Router
DEFAULT_QWEN_MODEL = os.getenv("HF_QWEN_MODEL", "Qwen/Qwen2.5-72B-Instruct")
DEFAULT_GPT_MODEL = os.getenv("HF_GPT_MODEL", "openai/gpt-oss-20b")
DEFAULT_FALLBACK_MODEL = "meta-llama/Llama-3.3-70B-Instruct"
TASK_DESCRIPTIONS = {
"easy": "Energy Arbitrage only (buy cheap, sell expensive)",
"medium": "Energy Arbitrage + Frequency Regulation",
"hard": "Energy Arbitrage + Frequency Regulation + Peak Shaving",
}
_FALLBACK_BASE = {
"available": False,
"verdict": "N/A",
"score": 0.0,
"reward_score": 0.0,
"summary": "",
"strengths": [],
"weaknesses": [],
"recommendations": [],
"confidence": "N/A",
"detailed_analysis": "",
"error": None,
"is_heuristic": False,
"provider": "UNKNOWN"
}
def _extract_json(text: str) -> dict:
"""Robustly extracts JSON from a string, handling markdown or surrounding text."""
text = text.strip()
parsed = {}
# Try direct parse first
try:
parsed = json.loads(text)
except:
# Try finding JSON block with regex
json_pattern = r'\{.*\}'
matches = re.findall(json_pattern, text, re.DOTALL)
for m in reversed(matches):
try:
parsed = json.loads(m)
break
except:
continue
# If no valid JSON found, use a fallback structure
if not isinstance(parsed, dict) or not parsed:
parsed = {
"verdict": "Unknown",
"score": 0.0,
"reward_score": 0.0,
"summary": "Parsing failed.",
"detailed_analysis": text[:500] if text else "No content"
}
# Manual regex for key floating point fields to be safe
for field in ["score", "reward_score"]:
if field not in parsed or parsed[field] == 0.0:
match = re.search(fr'"{field}"\s*:\s*([\d\.]+)', text)
if match:
try: parsed[field] = float(match.group(1))
except: pass
if field in parsed and isinstance(parsed[field], (int, float)):
parsed[field] = max(0.001, min(0.999, float(parsed[field])))
return parsed
def _get_heuristic_analysis(data: dict) -> dict:
"""Provides a rule-based assessment when LLMs are unavailable."""
scores = data.get("scores", {})
overall = scores.get("overall", 0)
task = data.get("task", "hard")
if overall >= 0.85: verdict = "Excellent"
elif overall >= 0.70: verdict = "Good"
elif overall >= 0.50: verdict = "Needs Improvement"
else: verdict = "Poor"
strengths, weaknesses, recommendations = [], [], []
if scores.get("reward", 0) > 0.8: strengths.append("Strong reward optimization")
else: weaknesses.append("Sub-optimal profit generation")
if scores.get("soc_readiness", 0) > 0.8: strengths.append("Excellent peak-hour SOC management")
else: recommendations.append("Improve SOC buffer management before evening peaks")
if scores.get("ps_adherence", 0) < 0.9 and task == "hard":
weaknesses.append("Frequent peak shaving violations")
recommendations.append("Increase penalty factor for grid load violations in reward function")
if scores.get("cycle_discipline", 0) < 0.6:
weaknesses.append("Aggressive battery cycling")
recommendations.append("Increase degradation cost coefficient to preserve battery health")
h_score = (
scores.get("reward", 0) * 0.4 +
scores.get("soc_readiness", 0) * 0.2 +
scores.get("ps_adherence", 0) * 0.3 +
scores.get("cycle_discipline", 0) * 0.1
)
return {
**_FALLBACK_BASE,
"available": True,
"is_heuristic": True,
"provider": "LOCAL",
"verdict": verdict,
"score": max(0.001, min(0.999, round(h_score, 3))),
"reward_score": max(0.001, min(0.999, round(scores.get("reward", 0), 3))),
"summary": f"Heuristic assessment: The agent shows a {verdict.lower()} grasp of the {task} task.",
"strengths": strengths or ["Stable baseline performance"],
"weaknesses": weaknesses or ["Minor inefficiencies"],
"recommendations": recommendations or ["Continue training"],
"confidence": "Medium (Rule-based)",
"detailed_analysis": "Local rule-based assessment."
}
def _build_prompt(data: dict) -> str:
task = data.get("task", "unknown")
scores = data.get("scores", {})
return f"""Analyze BESS RL Agent Performance:
Task: {task}
Mean Reward: {data.get('reward_mean', 0):.1f}
Simulated Reward Score: {scores.get('reward', 0):.3f}
Simulated Overall Score: {scores.get('overall', 0):.3f}
Return ONLY this JSON format:
{{
"verdict": "Excellent|Good|Needs Improvement|Poor",
"score": <float 0-1 overall assessment>,
"reward_score": <float 0-1 specifically for profit performance>,
"summary": "...",
"strengths": ["..."],
"weaknesses": ["..."],
"recommendations": ["..."],
"confidence": "High|Medium|Low",
"detailed_analysis": "..."
}}
"""
def _call_router(client: openai.OpenAI, model_id: str, prompt: str) -> dict:
"""Internal helper to make the chat completion call."""
response = client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": prompt}],
temperature=0.3
)
content = response.choices[0].message.content
parsed = _extract_json(content)
# Ensure all required fields are present to prevent frontend crashes
for key, val in _FALLBACK_BASE.items():
if key not in parsed and key != "available":
parsed[key] = val
return {**parsed, "available": True, "provider": f"HF:{model_id}"}
def _get_router_analysis(data: dict, model_id: str) -> dict:
"""Calls the Hugging Face Router with automatic fallback support."""
api_token = os.getenv("HF_TOKEN") or os.getenv("PowerGrid") or os.getenv("HUGGING_FACE_HUB_TOKEN")
if not api_token:
raise ValueError("Hugging Face token not found (expected HF_TOKEN).")
client = openai.OpenAI(
base_url="https://router.huggingface.co/v1",
api_key=api_token
)
prompt = _build_prompt(data)
try:
return _call_router(client, model_id, prompt)
except Exception as e:
error_msg = str(e)
if "model_not_found" in error_msg or "does not exist" in error_msg or "404" in error_msg:
try:
result = _call_router(client, DEFAULT_FALLBACK_MODEL, prompt)
result["summary"] = f"NOTE: Primary model {model_id} unavailable. Falling back. " + result.get("summary", "")
return result
except:
pass
raise ValueError(f"Failed to analyze: {error_msg}")
def get_llm_analysis(data: dict, provider: str = "QWEN", model_name: str = None) -> dict:
"""Main entry point for technical LLM analysis via Hugging Face Router."""
try:
if provider == "QWEN":
model_id = model_name or DEFAULT_QWEN_MODEL
else:
model_id = model_name or DEFAULT_GPT_MODEL
return _get_router_analysis(data, model_id)
except Exception as e:
error_msg = str(e)
return {
**_FALLBACK_BASE,
"available": False,
"summary": f"Evaluation Failed: {error_msg}",
"error": error_msg,
"detailed_analysis": f"The requested analysis could not be completed.\n\nReason: {error_msg}"
}
|