File size: 10,918 Bytes
bd468ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3358fd3
bd468ee
 
 
 
 
374834e
bd468ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374834e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3358fd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dfb7db1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3358fd3
 
 
 
 
bd468ee
 
 
3358fd3
 
 
 
 
bd468ee
 
 
3358fd3
 
 
 
 
 
 
 
 
 
374834e
3358fd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dfb7db1
 
 
 
 
 
 
 
 
 
 
 
 
 
3358fd3
dfb7db1
3358fd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd468ee
 
3358fd3
bd468ee
3358fd3
bd468ee
 
3358fd3
 
 
bd468ee
3358fd3
 
 
 
 
 
 
 
 
 
 
 
bd468ee
 
3358fd3
bd468ee
3358fd3
 
 
 
 
bd468ee
 
 
 
 
374834e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3358fd3
 
 
 
 
 
 
 
bd468ee
 
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
import os
import sys
import json
from openai import OpenAI


def load_local_env() -> None:
    """Lightweight .env loader for local runs without requiring python-dotenv."""
    env_path = os.path.join(os.path.dirname(__file__), ".env")
    if not os.path.exists(env_path):
        return

    with open(env_path, "r", encoding="utf-8") as f:
        for raw_line in f:
            line = raw_line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            key, value = line.split("=", 1)
            key = key.strip()
            value = value.strip().strip('"').strip("'")
            if key and key not in os.environ:
                os.environ[key] = value


load_local_env()

# Add the envs module to path so we can import client and models
sys.path.append(os.path.join(os.path.dirname(__file__), "envs", "rag_optimizer_env"))
from client import RagOptimizerEnvClient
from models import RagOptimizerAction

# Load environment variables
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
HF_TOKEN = os.getenv("HF_TOKEN")

MAX_STEPS = 30
TASK_IDS = ["easy", "medium", "hard"]

SYSTEM_PROMPT = """You are an automated Data Engineer managing an AI Knowledge Base.

Your goal is to optimize the messy chunks of text in the database so that a TF-IDF Search Algorithm can find answers easily.

You must resolve contradictions, categorize documents, and delete unnecessary documents.



After each action you will receive a "current_reward" score (0.01 to 0.99) indicating how well the KB currently performs. Use this to guide your strategy.



You have the following actions:

- {"action_type": "read_document", "doc_id": "..."}

- {"action_type": "update_document", "doc_id": "...", "text": "..."}

- {"action_type": "delete_document", "doc_id": "..."}

- {"action_type": "add_metadata", "doc_id": "...", "metadata_key": "...", "metadata_value": "..."}

- {"action_type": "submit"}



You must return ONLY a raw JSON object detailing the action you want to take!"""

def format_action_str(action: RagOptimizerAction) -> str:
    if action.action_type == "read_document":
        return f"read('{action.doc_id}')"
    elif action.action_type == "update_document":
        return f"update('{action.doc_id}')"
    elif action.action_type == "delete_document":
        return f"delete('{action.doc_id}')"
    elif action.action_type == "add_metadata":
        return f"add_metadata('{action.doc_id}','{action.metadata_key}')"
    elif action.action_type == "submit":
        return "submit()"
    return f"{action.action_type}()"

# --- Reflexion (Long Term Memory) ---
LESSONS_FILE = os.path.join(os.path.dirname(__file__), "memory", "lessons_learned.json")

def load_lessons():
    if os.path.exists(LESSONS_FILE):
        try:
            with open(LESSONS_FILE, "r") as f:
                return json.load(f)
        except:
            pass
    return []

def save_lesson(lesson_text, task_id):
    os.makedirs(os.path.dirname(LESSONS_FILE), exist_ok=True)
    lessons = load_lessons()
    lessons.append({"task": task_id, "lesson": lesson_text})
    with open(LESSONS_FILE, "w") as f:
        json.dump(lessons, f, indent=2)

def get_system_prompt():
    prompt = SYSTEM_PROMPT
    lessons = load_lessons()
    if lessons:
        prompt += "\n\nPAST LESSONS LEARNED (DO NOT REPEAT MISTAKES):\n"
        for l in lessons[-5:]:  # Show only top 5 recent
            prompt += f"- {l['lesson']}\n"
    return prompt


def _safe_reset(env: RagOptimizerEnvClient, task_id: str):
    """Reset env for a specific task with compatibility fallbacks."""
    try:
        return env.reset(task_id=task_id)
    except TypeError:
        try:
            return env.reset(task=task_id)
        except TypeError:
            return env.reset()


def _clamp_score(value: float) -> float:
    if value < 0.01:
        return 0.01
    if value > 0.99:
        return 0.99
    return value


def _extract_json_object(text: str) -> dict:
    """Extract a JSON object from model output that may include extra text."""
    if not text:
        raise ValueError("empty model response")

    raw = text.strip()
    try:
        return json.loads(raw)
    except Exception:
        pass

    # Common markdown fence wrapper
    if "```" in raw:
        parts = raw.split("```")
        for part in parts:
            candidate = part.strip()
            if candidate.lower().startswith("json"):
                candidate = candidate[4:].strip()
            if candidate.startswith("{") and candidate.endswith("}"):
                try:
                    return json.loads(candidate)
                except Exception:
                    pass

    # Fallback: take substring between first '{' and last '}'
    start = raw.find("{")
    end = raw.rfind("}")
    if start != -1 and end != -1 and end > start:
        candidate = raw[start:end + 1]
        return json.loads(candidate)

    raise ValueError("no JSON object found in model response")


def run_task_episode(

    env: RagOptimizerEnvClient,

    llm_client: OpenAI,

    task_id: str,

) -> None:
    step_rewards = []
    success = False
    error_msg = "null"
    score = 0.01
    step = 0

    print(f"[START] task={task_id} env=OpenEnv model={MODEL_NAME}")

    # We suppress any other custom prints to respect the STDOUT format strictly
    import contextlib
    import io

    with contextlib.redirect_stdout(io.StringIO()):
        try:
            result = _safe_reset(env, task_id)
            observation = result.observation
        except Exception as e:
            error_msg = str(e).replace('\n', ' ')
            print(f"[END] success=false steps=0 score=0.01 rewards=")
            return

    history = [{"role": "system", "content": get_system_prompt()}]

    init_obs = {
        "server_feedback": observation.message,
        "current_reward": observation.reward,
        "current_knowledge_base": observation.current_docs,
    }
    history.append({"role": "user", "content": json.dumps(init_obs, indent=2)})

    for i in range(1, MAX_STEPS + 1):
        step = i
        messages = list(history)

        action_str = "unknown"
        error_msg = "null"

        try:
            try:
                completion = llm_client.chat.completions.create(
                    model=MODEL_NAME,
                    messages=messages,
                    response_format={"type": "json_object"},
                    max_tokens=1000,
                )
            except Exception:
                # Some OpenAI-compatible providers may not enforce response_format.
                completion = llm_client.chat.completions.create(
                    model=MODEL_NAME,
                    messages=messages,
                    max_tokens=1000,
                )
            response_text = completion.choices[0].message.content or ""
            action_data = _extract_json_object(response_text)

            # Normalize fields if model returns lists instead of strings
            for field in ("doc_id", "text", "metadata_key", "metadata_value"):
                val = action_data.get(field)
                if isinstance(val, list):
                    if val and isinstance(val[0], str):
                        action_data[field] = " ".join(val)
                    elif val and isinstance(val[0], dict):
                        action_data[field] = json.dumps(val[0])
                    else:
                        action_data[field] = str(val[0]) if val else ""

            action = RagOptimizerAction(**action_data)
            action_str = format_action_str(action)

        except Exception as exc:
            error_msg = str(exc).replace('\n', ' ')
            action = RagOptimizerAction(action_type="submit")
            action_str = format_action_str(action)

        # Suppress normal prints during step
        with contextlib.redirect_stdout(io.StringIO()):
            try:
                result = env.step(action)
                observation = result.observation
                reward = _clamp_score(float(result.reward))
            except Exception as e:
                error_msg = str(e).replace('\n', ' ')
                reward = 0.01
                result = type("obj", (object,), {"done": True})()
                observation = type("obj", (object,), {"message": "error", "current_docs": {}})()

        step_rewards.append(reward)
        done = "true" if result.done else "false"

        print(f"[STEP] step={step} action={action_str} reward={reward:.2f} done={done} error={error_msg}")

        if result.done:
            success = True if reward > 0.5 else False
            score = _clamp_score(float(reward))
            break

        history.append({"role": "assistant", "content": json.dumps(action.model_dump(), default=str)})
        next_obs = {
            "server_feedback": observation.message,
            "current_reward": observation.reward,
            "current_knowledge_base": observation.current_docs,
        }
        history.append({"role": "user", "content": json.dumps(next_obs, indent=2)})
    else:
        # Reached max steps
        success = False
        score = _clamp_score(float(result.reward))

    rewards_str = ",".join([f"{r:.2f}" for r in step_rewards])
    done_str = "true" if success else "false"
    print(f"[END] success={done_str} steps={step} score={score:.2f} rewards={rewards_str}")

    # Memory Reflexion Trigger
    if not success and score < 0.6:
        # Agent failed, try to reflect
        hist_str = json.dumps([m["content"] for m in history[-6:]])  # get last few actions/obs
        ref_prompt = f"The agent failed task '{task_id}' with final reward {score}. Last context: {hist_str}. Write a 1-sentence tactical lesson stating explicitly what data engineering action the agent should have done instead."
        try:
            resp = llm_client.chat.completions.create(
                model=MODEL_NAME, 
                messages=[{"role": "user", "content": ref_prompt}], 
                max_tokens=60
            )
            lesson = resp.choices[0].message.content.strip()
            save_lesson(lesson, task_id)
            print(f"[MEMORY] Learned lesson: {lesson}")
        except:
            pass

def main():
    # Setup OpenAI Client
    client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)

    with RagOptimizerEnvClient(base_url="http://localhost:8000").sync() as env:
        for task_id in TASK_IDS:
            run_task_episode(env=env, llm_client=client, task_id=task_id)

if __name__ == "__main__":
    main()