Spaces:
Sleeping
Sleeping
| # Copyright (c) Meta Platforms, Inc. and affiliates. | |
| # All rights reserved. | |
| # | |
| # This source code is licensed under the BSD-style license found in the | |
| # LICENSE file in the root directory of this source tree. | |
| """ | |
| Rag Optimizer Environment Implementation. | |
| The agent acts as a Data Engineer to un-block a broken RAG pipeline. | |
| """ | |
| from copy import deepcopy | |
| from uuid import uuid4 | |
| from typing import Dict, Any, List | |
| from openenv.core.env_server.interfaces import Environment | |
| from openenv.core.env_server.types import State | |
| # Import scikit-learn for our Grader (fallback/legacy) | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| import numpy as np | |
| # Hybrid Search imports | |
| from sentence_transformers import SentenceTransformer | |
| from rank_bm25 import BM25Okapi | |
| try: | |
| from models import RagOptimizerAction, RagOptimizerObservation | |
| except ImportError: | |
| from models import RagOptimizerAction, RagOptimizerObservation | |
| class RagOptimizerEnvironment(Environment): | |
| """ | |
| RAG Optimizer Engine. | |
| Maintains a simulated Knowledge Base and grades it using TF-IDF. | |
| """ | |
| SUPPORTS_CONCURRENT_SESSIONS: bool = True | |
| def __init__(self): | |
| self._state = State(episode_id=str(uuid4()), step_count=0) | |
| self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2') | |
| self.kb = {} | |
| self.test_suite = [] | |
| self.dense_vectors = {} | |
| # Load the Real World Datasets | |
| import json | |
| import os | |
| seed_path = os.path.join(os.path.dirname(__file__), "kb_seed.json") | |
| with open(seed_path, "r") as f: | |
| self.seed_data = json.load(f) | |
| self._setup_task("easy") | |
| def _setup_task(self, task_id: str): | |
| if task_id not in ["easy", "medium", "hard"]: | |
| task_id = "easy" | |
| # Clone the fresh dataset from seed so the agent can destroy it | |
| self.kb = deepcopy(self.seed_data[task_id]) | |
| if task_id == "easy": | |
| self.test_suite = [ | |
| {"query": "How do I run the server in production with concurrency?", "target_concept": "uvicorn main:app --workers 4"}, | |
| {"query": "Which version of Pydantic does FastAPI use by default?", "target_concept": "defaults to Pydantic v2"} | |
| ] | |
| elif task_id == "medium": | |
| self.kb = deepcopy(self.seed_data["hard"]) | |
| self.test_suite = [ | |
| {"query": "Which entity bankruptcy was the climax of the disaster?", "target_concept": "bankruptcy of Lehman Brothers"}, | |
| {"query": "What types of collateralized assets lost their worth?", "target_concept": "Mortgage-backed securities"}, | |
| {"query": "Who did predatory lenders primarily go after?", "target_concept": "low-income homebuyers"} | |
| ] | |
| elif task_id == "hard": | |
| self.kb = { | |
| # v1 — legacy, wrong routing syntax, poisons retrieval rank for v3 | |
| "fastapi_routing_v1": { | |
| "text": "FastAPI routing uses @app.route() decorator similar to Flask. Define routes with methods=['GET','POST']. Pydantic v1 handles all schema validation.", | |
| "metadata": {"version": "legacy"} | |
| }, | |
| # v2 — also legacy, still wrong syntax, further dilutes correct doc | |
| "fastapi_routing_v2": { | |
| "text": "FastAPI routes are defined using @app.route() with type hints added. Validation is done through Pydantic v1 models attached to each endpoint.", | |
| "metadata": {"version": "legacy"} | |
| }, | |
| # v3 — CORRECT current doc, must survive and rank #1 after purge | |
| "fastapi_routing_v3": { | |
| "text": "FastAPI uses @app.get(), @app.post() and other HTTP method decorators for routing. It defaults to Pydantic v2 for request and response validation.", | |
| "metadata": {"version": "current"} | |
| }, | |
| } | |
| # 15 generic distractors — low semantic overlap, provide ranking noise | |
| for i in range(15): | |
| self.kb[f"doc_distractor_{i}"] = { | |
| "text": f"General web framework note {i}: always use async handlers for better throughput in high-traffic services.", | |
| "metadata": {} | |
| } | |
| self.test_suite = [ | |
| # v1 and v2 both contain @app.route() which competes semantically with v3 | |
| # Deleting v1 and v2 pushes v3 to rank 1 → each delete gives a visible reward jump | |
| {"query": "How are routes defined in FastAPI?", | |
| "target_concept": "@app.get(), @app.post() decorators"}, | |
| {"query": "Which Pydantic version does FastAPI v2 use by default?", | |
| "target_concept": "defaults to Pydantic v2"}, | |
| ] | |
| self._rebuild_cache() | |
| def _rebuild_cache(self): | |
| """Called whenever KB documents are added, removed, or updated.""" | |
| if not self.kb: | |
| self.dense_vectors = {} | |
| return | |
| doc_ids = list(self.kb.keys()) | |
| # Append metadata to text for embedding | |
| doc_texts = [(self.kb[d]["text"] + " " + " ".join(self.kb[d]["metadata"].values())).strip() for d in doc_ids] | |
| vectors = self.embedding_model.encode(doc_texts, convert_to_tensor=False) | |
| self.dense_vectors = {doc_id: vectors[i] for i, doc_id in enumerate(doc_ids)} | |
| def _get_kb_summary(self) -> Dict[str, Dict]: | |
| """Returns a summary of the KB for the observation.""" | |
| summary = {} | |
| for k, v in self.kb.items(): | |
| summary[k] = {"metadata": v.get("metadata", {}), "length": len(v.get("text", ""))} | |
| return summary | |
| def reset(self, **kwargs) -> RagOptimizerObservation: | |
| self._state = State(episode_id=str(uuid4()), step_count=0) | |
| task_id = kwargs.get("task_id") or kwargs.get("task") or "easy" | |
| self._setup_task(task_id) | |
| return RagOptimizerObservation( | |
| message=f"RagOptimizerEnv Initialized for task: {task_id}. Resolve conflicts, append metadata, or splinter chunks to win.", | |
| current_docs=self._get_kb_summary(), | |
| done=False, | |
| reward=self._evaluate_kb() | |
| ) | |
| def _evaluate_kb(self) -> float: | |
| """The Grader: Evaluates the current KB using Hybrid RRF (BM25 + Semantic MRR).""" | |
| if not self.kb or not self.test_suite: | |
| return 0.01 | |
| doc_ids = list(self.kb.keys()) | |
| doc_texts = [(self.kb[d]["text"] + " " + " ".join(self.kb[d]["metadata"].values())).strip() for d in doc_ids] | |
| # 1. BM25 Corpus Preparation | |
| tokenized_corpus = [doc.lower().split() for doc in doc_texts] | |
| bm25 = BM25Okapi(tokenized_corpus) | |
| # 2. Dense Matrix | |
| doc_vectors = np.array([self.dense_vectors[d] for d in doc_ids]) | |
| mrr_sum = 0.0 | |
| for case in self.test_suite: | |
| # BM25 Search | |
| tokenized_query = case["query"].lower().split() | |
| bm25_scores = bm25.get_scores(tokenized_query) | |
| bm25_ranks = bm25_scores.argsort()[::-1] | |
| # Dense Search | |
| query_vec = self.embedding_model.encode(case["query"]) | |
| dense_scores = cosine_similarity([query_vec], doc_vectors)[0] | |
| dense_ranks = dense_scores.argsort()[::-1] | |
| # Reciprocal Rank Fusion (RRF) | |
| rrf_scores = {d: 0.0 for d in doc_ids} | |
| k = 60 | |
| for rank, idx in enumerate(bm25_ranks): | |
| rrf_scores[doc_ids[idx]] += 1.0 / (k + rank + 1) | |
| for rank, idx in enumerate(dense_ranks): | |
| rrf_scores[doc_ids[idx]] += 1.0 / (k + rank + 1) | |
| # Grade Top-K fused list using MRR | |
| ranked_doc_ids = sorted(rrf_scores.keys(), key=lambda x: rrf_scores[x], reverse=True) | |
| req_meta = case.get("required_metadata_key") | |
| req_meta_val = case.get("required_metadata_value") | |
| case_mrr = 0.0 | |
| for i, doc_id in enumerate(ranked_doc_ids): | |
| # Is this the true doc? | |
| doc_text = self.kb[doc_id]["text"].lower() | |
| if case["target_concept"].lower() in doc_text: | |
| valid = True | |
| # Medium Task Semantic Check | |
| if req_meta and req_meta_val: | |
| if self.kb[doc_id]["metadata"].get(req_meta) != req_meta_val: | |
| valid = False | |
| if valid: | |
| case_mrr = 1.0 / (i + 1) # MRR formula starts at rank 1 | |
| break | |
| mrr_sum += case_mrr | |
| base_reward = float(mrr_sum / len(self.test_suite)) | |
| # Step Cost Penalty calculation (-0.01 per step) | |
| cost_penalty = self._state.step_count * 0.01 | |
| return max(0.01, min(0.99, base_reward - cost_penalty)) | |
| def step(self, action: RagOptimizerAction) -> RagOptimizerObservation: # type: ignore[override] | |
| self._state.step_count += 1 | |
| msg = "" | |
| done = False | |
| reward = 0.01 | |
| try: | |
| if action.action_type == "read_document": | |
| if action.doc_id in self.kb: | |
| msg = f"Content of {action.doc_id}: {self.kb[action.doc_id]['text']}" | |
| else: | |
| msg = f"Error: doc_id {action.doc_id} not found." | |
| elif action.action_type == "delete_document": | |
| if action.doc_id in self.kb: | |
| del self.kb[action.doc_id] | |
| self._rebuild_cache() | |
| msg = f"Deleted {action.doc_id}." | |
| else: | |
| msg = f"Error: doc_id {action.doc_id} not found." | |
| elif action.action_type == "update_document": | |
| if not action.doc_id or not action.text: | |
| msg = "Error: doc_id and text required for update_document." | |
| else: | |
| if action.doc_id not in self.kb: | |
| self.kb[action.doc_id] = {"text": "", "metadata": {}} | |
| self.kb[action.doc_id]["text"] = action.text | |
| self._rebuild_cache() | |
| msg = f"Updated text for {action.doc_id}." | |
| elif action.action_type == "add_metadata": | |
| if not action.doc_id or not action.metadata_key or not action.metadata_value: | |
| msg = "Error: doc_id, metadata_key, and metadata_value required." | |
| else: | |
| if action.doc_id not in self.kb: | |
| msg = f"Error: doc_id {action.doc_id} not found." | |
| else: | |
| self.kb[action.doc_id]["metadata"][action.metadata_key] = action.metadata_value | |
| self._rebuild_cache() | |
| msg = f"Added metadata to {action.doc_id}." | |
| elif action.action_type == "submit": | |
| done = True | |
| reward = self._evaluate_kb() | |
| msg = f"Evaluation complete. Final reward: {reward:.2f}" | |
| except Exception as e: | |
| msg = f"Action failed: {str(e)}" | |
| if not done: | |
| reward = self._evaluate_kb() | |
| return RagOptimizerObservation( | |
| message=msg, | |
| current_docs=self._get_kb_summary(), | |
| done=done, | |
| reward=reward, | |
| ) | |
| def state(self) -> State: | |
| return self._state |