Spaces:
Running
Running
File size: 8,507 Bytes
b9d3df6 2b6608e b9d3df6 2b6608e 8597d7f 2b6608e 8597d7f 2b6608e 8597d7f b9d3df6 8597d7f b9d3df6 8597d7f b9d3df6 8597d7f b9d3df6 8597d7f 2b6608e 8597d7f 2b6608e 8597d7f 2b6608e 8597d7f 2b6608e 8597d7f b9d3df6 2b6608e b9d3df6 8597d7f 2b6608e b9d3df6 8597d7f b9d3df6 2b6608e b9d3df6 2b6608e b9d3df6 8597d7f b9d3df6 |
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 |
import os
import json
import datetime
from pathlib import Path
from openai import OpenAI
from dotenv import load_dotenv
# --- Configuration & Initialization ---
load_dotenv()
# The script runs from the 'scripts/' directory, so its root is one level up.
PROJECT_ROOT = Path(__file__).resolve().parent.parent
# File Paths
MASTER_PROMPT_PATH = PROJECT_ROOT / "data" / "master_prompt.json"
FEEDBACK_LOG_PATH = PROJECT_ROOT / "data" / "feedback_log.json"
STATUS_LOG_PATH = PROJECT_ROOT / "data" / "status_log.txt"
# LLM Model ID for Rewriting (Meta-LLM)
META_LLM_MODEL = "x-ai/grok-4-fast"
# The minimum number of negative feedback entries required to trigger an update
MIN_NEGATIVE_FEEDBACK = 3
def load_data(path: Path):
"""Safely loads JSON data, handling empty file content."""
try:
if path.exists():
content = path.read_text().strip()
# Handle empty content
if not content:
return []
data = json.loads(content)
# Ensure correct type based on file (MASTER_PROMPT is dict, FEEDBACK_LOG is list)
if path == MASTER_PROMPT_PATH and not isinstance(data, dict):
return {}
if path == FEEDBACK_LOG_PATH and not isinstance(data, list):
return []
return data
# Create initial state if file doesn't exist
if path == MASTER_PROMPT_PATH:
return {"system_message": "A critical error occurred.", "version": "1.0.0", "last_updated": datetime.datetime.now().isoformat()}
return []
except Exception as e:
print(f"Error loading {path}: {e}")
return []
def aggregate_negative_feedback(feedback_data: list) -> str:
"""
Analyzes the feedback log to summarize only the negative (rating=0) feedback.
"""
negative_feedback = [entry for entry in feedback_data if entry.get("rating") == 0]
if len(negative_feedback) < MIN_NEGATIVE_FEEDBACK:
print(f"INFO: Only {len(negative_feedback)} negative entries found. Skipping optimization.")
return None
# Summarize the negative prompts that led to user dislike
summary = []
for entry in negative_feedback:
summary.append(
f"User disliked the response (Rating 0) after input: '{entry['original_prompt']}' "
f"The resulting OPTIMIZED PROMPT was: '{entry['optimized_prompt']}'"
)
print(f"INFO: Aggregated {len(negative_feedback)} negative feedback entries.")
return "\n---\n".join(summary)
def optimize_system_prompt(current_system_message: str, feedback_summary: str) -> str:
"""
Calls the Meta-LLM to rewrite the system message based on negative feedback.
"""
# We define a strict Meta-Prompt for the Grok model to follow
meta_prompt = (
"You are the **System Prompt Optimizing Agent**. Your goal is to analyze the 'FAILED FEEDBACK' and rewrite the 'CURRENT SYSTEM MESSAGE' "
"to address the problems identified. The new system message must aim to improve the quality of future responses, making them more accurate, "
"detailed, or strictly adherent to formatting rules, based on the failure patterns. "
"You must output **ONLY** the new system message text, nothing else. Do not use markdown quotes."
)
# The user message feeds the prompt and the negative data to the agent
user_message = f"""
CURRENT SYSTEM MESSAGE:
---
{current_system_message}
---
FAILED FEEDBACK (You must incorporate lessons from this data):
---
{feedback_summary}
---
Based ONLY on the above, rewrite the CURRENT SYSTEM MESSAGE to improve it.
New System Message:
"""
try:
# Call OpenRouter API with the Meta-LLM
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENROUTER_API_KEY"),
)
response = client.chat.completions.create(
model=META_LLM_MODEL,
messages=[
{"role": "system", "content": meta_prompt},
{"role": "user", "content": user_message}
],
temperature=0.1, # Low temperature for reliable instruction following
max_tokens=512,
)
new_prompt = response.choices[0].message.content.strip()
print("SUCCESS: New System Prompt generated by Meta-LLM.")
return new_prompt
except Exception as e:
print(f"CRITICAL ERROR: Meta-LLM API call failed: {e}")
return current_system_message # Return original prompt on failure
def increment_version(version_str: str) -> str:
"""Safely increments the minor version (Y in X.Y.Z) of a version string."""
try:
# Assumes format X.Y.Z
parts = version_str.split('.')
if len(parts) < 2:
return "1.0.0"
# Increment the second part (the minor version)
new_minor_version = int(parts[1]) + 1
# Rebuild the version string
# Uses parts[2:] for safety, handles missing Z value if needed
new_parts = [parts[0], str(new_minor_version)] + parts[2:]
return ".".join(new_parts)
except Exception:
# If any part fails (non-integer, etc.), reset to a safe, known state.
return "1.0.0"
def run_optimization():
"""Main function for the MLOps pipeline script."""
print(f"--- Running Prompt Optimization Pipeline at {datetime.datetime.now()} ---")
# 1. Load Data
current_config = load_data(MASTER_PROMPT_PATH)
feedback_data = load_data(FEEDBACK_LOG_PATH)
current_system_message = current_config.get("system_message", "")
if not feedback_data:
print("INFO: Feedback log is empty. Exiting optimization.")
# Update the status log to reflect that the scheduled job ran but was skipped.
update_status_log("MLOps Optimization skipped (No new feedback).")
return
# 2. Aggregate Feedback
feedback_summary = aggregate_negative_feedback(feedback_data)
if feedback_summary is None:
# Optimization was skipped because not enough negative feedback was found
update_status_log("MLOps Optimization skipped (Insufficient negative feedback).")
return
# 3. Optimize Prompt
new_system_message = optimize_system_prompt(current_system_message, feedback_summary)
# 4. Check if prompt actually changed before committing
if new_system_message != current_system_message:
print("\n*** PROMPT UPDATED ***")
# 5. Update Master Prompt File
current_config["system_message"] = new_system_message
current_config["version"] = increment_version(current_config.get("version", "1.0.0"))
current_config["last_updated"] = datetime.datetime.now().isoformat()
with open(MASTER_PROMPT_PATH, 'w') as f:
json.dump(current_config, f, indent=4)
print(f"Successfully wrote new prompt version {current_config['version']} to master_prompt.json")
# 6. Clear Feedback Log (Ready for next cycle)
with open(FEEDBACK_LOG_PATH, 'w') as f:
json.dump([], f)
print("Feedback log cleared.")
# 7. Update the Status Log (Rewrites the file with new timestamp)
update_status_log(f"MLOps Prompt Optimization deployed successfully. New version: {current_config['version']}")
else:
print("\nINFO: No significant change or API error. Master prompt remains the same.")
update_status_log("MLOps Optimization failed to deploy (API error or no meaningful change detected).")
def update_status_log(status_message: str):
"""Writes the current status to the status log file, overwriting the previous entry."""
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S IST")
log_content = f"[{current_time}] {status_message}"
try:
with open(STATUS_LOG_PATH, 'w') as f:
f.write(log_content)
print(f"Status log updated: {log_content}")
except Exception as e:
print(f"CRITICAL ERROR: Failed to write to status log: {e}")
if __name__ == '__main__':
run_optimization() |