File size: 21,421 Bytes
0983a18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
"""
validate.py β€” AdaptiveWorld Submission Validator
=================================================
Runs a comprehensive offline validation of the adaptive-world-env submission.

Checks:
  1. Scenario registry  β€” all 12 scenarios, all difficulty levels
  2. Drift configs      β€” every scenario has a matching DriftInjector config
  3. DriftInjector      β€” inject() mutates world correctly
  4. AdaptiveGrader     β€” grade_task / grade_belief / infer_belief_from_actions
  5. DriftDifficultyController β€” escalation logic
  6. Episode lifecycle  β€” simulated reset β†’ step β†’ done for each difficulty
  7. inference.py       β€” import check (no LLM call needed)

Run from the adaptive-world-env directory:
    python validate.py
"""

import sys
import os
import copy
import traceback

# ── Bootstrap path ─────────────────────────────────────────────────────────────
ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT)

# ── Terminal colours (Windows-safe via ANSI or plain) ──────────────────────────
try:
    import ctypes
    ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 7)
    GREEN  = "\033[92m"
    RED    = "\033[91m"
    YELLOW = "\033[93m"
    BOLD   = "\033[1m"
    RESET  = "\033[0m"
except Exception:
    GREEN = RED = YELLOW = BOLD = RESET = ""

PASS = 0
FAIL = 0
ERRORS = []


def ok(msg):
    global PASS
    PASS += 1
    print(f"  {GREEN}βœ“{RESET} {msg}")


def fail(msg, exc=None):
    global FAIL
    FAIL += 1
    ERRORS.append(msg)
    print(f"  {RED}βœ—{RESET} {msg}")
    if exc:
        print(f"    {YELLOW}β†’ {exc}{RESET}")


def section(title):
    print(f"\n{BOLD}{'─'*60}{RESET}")
    print(f"{BOLD}  {title}{RESET}")
    print(f"{BOLD}{'─'*60}{RESET}")


# ══════════════════════════════════════════════════════════════════════════════
# 1. Scenario Registry
# ══════════════════════════════════════════════════════════════════════════════
section("1 / 7 Β· Scenario Registry")

try:
    from scenarios.registry import SCENARIO_REGISTRY, ALL_SCENARIOS

    DIFFICULTIES = ["easy", "medium", "hard", "expert"]
    REQUIRED_FIELDS = ["id", "description", "domain", "drift_trigger_step",
                       "drift_type", "task_goal", "max_steps"]

    for level in DIFFICULTIES:
        if level in SCENARIO_REGISTRY:
            ok(f"Difficulty '{level}' present")
        else:
            fail(f"Difficulty '{level}' MISSING from registry")

    if len(ALL_SCENARIOS) == 12:
        ok(f"Total scenario count: {len(ALL_SCENARIOS)} (expected 12)")
    else:
        fail(f"Expected 12 scenarios, found {len(ALL_SCENARIOS)}")

    for level, scenarios in SCENARIO_REGISTRY.items():
        if len(scenarios) == 3:
            ok(f"  '{level}' has 3 scenarios")
        else:
            fail(f"  '{level}' has {len(scenarios)} scenarios (expected 3)")

    bad = []
    for s in ALL_SCENARIOS:
        missing = [f for f in REQUIRED_FIELDS if f not in s]
        if missing:
            bad.append(f"{s.get('id', '?')} missing: {missing}")
        if "drift_occurred" in s:
            bad.append(f"{s['id']} has forbidden key 'drift_occurred'")

    if bad:
        for b in bad:
            fail(b)
    else:
        ok(f"All {len(ALL_SCENARIOS)} scenarios have required fields (no forbidden keys)")

except Exception as e:
    fail("Scenario registry import/validation failed", e)
    traceback.print_exc()


# ══════════════════════════════════════════════════════════════════════════════
# 2. Drift Configs
# ══════════════════════════════════════════════════════════════════════════════
section("2 / 7 Β· Drift Injector Configs")

try:
    from server.drift_injector import DriftInjector, DRIFT_CONFIGS

    missing_configs = [s["id"] for s in ALL_SCENARIOS if s["id"] not in DRIFT_CONFIGS]
    if missing_configs:
        for sid in missing_configs:
            fail(f"No DRIFT_CONFIG entry for scenario '{sid}'")
    else:
        ok(f"All {len(ALL_SCENARIOS)} scenarios have DRIFT_CONFIG entries")

    # Quick smoke-test inject on a few known scenarios
    for sid in ["easy_field_rename", "easy_endpoint_version", "hard_status_meaning"]:
        try:
            inj = DriftInjector(sid)
            before = copy.deepcopy(inj.get_world())
            inj.inject()
            after = inj.get_world()
            if before != after:
                ok(f"  DriftInjector({sid!r}).inject() mutates world βœ“")
            else:
                fail(f"  DriftInjector({sid!r}).inject() did NOT change world")
        except Exception as ex:
            fail(f"  DriftInjector({sid!r}) error", ex)

    # Expert-specific helpers
    try:
        inj = DriftInjector("expert_transient_vs_real")
        step = inj.get_transient_error_step()
        if step == 2:
            ok(f"  expert_transient_vs_real: transient_error_step == 2")
        else:
            fail(f"  expected transient_error_step 2, got {step}")
    except Exception as ex:
        fail("  expert_transient_vs_real transient error step check", ex)

    try:
        inj = DriftInjector("expert_cross_service")
        step = inj.get_secondary_drift_step()
        if step == 6:
            ok(f"  expert_cross_service: secondary_drift_step == 6")
        else:
            fail(f"  expected secondary_drift_step 6, got {step}")
    except Exception as ex:
        fail("  expert_cross_service secondary drift step check", ex)

    try:
        DriftInjector("this_scenario_does_not_exist")
        fail("  Unknown scenario should raise ValueError but did not")
    except ValueError:
        ok("  Unknown scenario raises ValueError βœ“")
    except Exception as ex:
        fail("  Unknown scenario check raised unexpected exception", ex)

except Exception as e:
    fail("Drift injector section failed", e)
    traceback.print_exc()


# ══════════════════════════════════════════════════════════════════════════════
# 3. AdaptiveGrader
# ══════════════════════════════════════════════════════════════════════════════
section("3 / 7 Β· AdaptiveGrader")

try:
    from graders.grader import AdaptiveGrader
    g = AdaptiveGrader()

    # grade_task
    r = g.grade_task(task_completed=False, steps_taken=5, max_steps=8, drift_detected=False)
    if r == 0.001:
        ok("grade_task(failed, no detection) == 0.001")
    else:
        fail(f"grade_task(failed, no detection) expected 0.001 got {r}")

    r = g.grade_task(task_completed=False, steps_taken=5, max_steps=8, drift_detected=True)
    if r == 0.150:
        ok("grade_task(failed, detected) == 0.150")
    else:
        fail(f"grade_task(failed, detected) expected 0.150 got {r}")

    r_no = g.grade_task(True, 4, 8, False)
    r_yes = g.grade_task(True, 4, 8, True)
    if r_yes > r_no:
        ok("Proactive drift detection gives bonus reward βœ“")
    else:
        fail(f"Bonus expected: r_with_detect={r_yes:.4f} vs r_without={r_no:.4f}")

    r = g.grade_task(True, 1, 8, True)
    if r <= 0.999:
        ok(f"Task reward capped at 0.999 (got {r})")
    else:
        fail(f"Task reward not capped: {r}")

    # grade_belief β€” field_rename
    score = g.grade_belief(
        {"order_field": "quantity", "required_extra": "customer_id"},
        {"order_field": "quantity", "required_extra": "customer_id"},
        "field_rename"
    )
    if score >= 0.9:
        ok(f"grade_belief: correct field_rename β†’ {score:.3f} (β‰₯0.9) βœ“")
    else:
        fail(f"grade_belief: correct field_rename expected β‰₯0.9, got {score:.3f}")

    score = g.grade_belief({"order_field": "qty"}, {"order_field": "quantity", "required_extra": "customer_id"}, "field_rename")
    if score < 0.5:
        ok(f"grade_belief: stale field_rename β†’ {score:.3f} (<0.5) βœ“")
    else:
        fail(f"grade_belief: stale belief expected <0.5, got {score:.3f}")

    # grade_belief β€” endpoint_version
    score = g.grade_belief({"endpoint": "/mock_api/v2/rooms/book"}, {"rooms_endpoint": "/mock_api/v2/rooms/book"}, "endpoint_version")
    if score == 1.0:
        ok(f"grade_belief: correct endpoint_version β†’ 1.0 βœ“")
    else:
        fail(f"grade_belief: correct endpoint expected 1.0, got {score}")

    # grade_belief β€” None
    score = g.grade_belief(None, {"x": 1}, "field_rename")
    if score == 0.0:
        ok("grade_belief(None, ...) == 0.0 βœ“")
    else:
        fail(f"grade_belief(None) expected 0.0, got {score}")

    # infer_belief_from_actions
    log = [
        {"step": 1, "url": "/mock_api/orders", "status": 200, "response": "{}"},
        {"step": 2, "url": "/mock_api/orders", "status": 422, "response": '{"detail": "..."}'},
        {"step": 3, "url": "/openapi.json",    "status": 200, "response": "{}"},
        {"step": 4, "url": "/mock_api/orders", "status": 200, "response": '{"order_id": "x"}'},
    ]
    score = g.infer_belief_from_actions(log, "field_rename")
    if score == 0.6:
        ok(f"infer_belief_from_actions: probed+recovered β†’ 0.6 βœ“")
    else:
        fail(f"infer_belief_from_actions expected 0.6, got {score}")

    score = g.infer_belief_from_actions([], "field_rename")
    if score == 0.0:
        ok("infer_belief_from_actions([]) == 0.0 βœ“")
    else:
        fail(f"infer_belief_from_actions([]) expected 0.0, got {score}")

except Exception as e:
    fail("AdaptiveGrader section failed", e)
    traceback.print_exc()


# ══════════════════════════════════════════════════════════════════════════════
# 4. DriftDifficultyController
# ══════════════════════════════════════════════════════════════════════════════
section("4 / 7 Β· DriftDifficultyController")

try:
    from server.difficulty_controller import DriftDifficultyController

    ctrl = DriftDifficultyController()
    if ctrl.level == 0:
        ok("Initial level == 0 βœ“")
    else:
        fail(f"Expected initial level 0, got {ctrl.level}")

    # not enough data
    ctrl2 = DriftDifficultyController()
    for _ in range(4):
        ctrl2.record("field_rename", 0.95)
        ctrl2.record("endpoint_version", 0.95)
        ctrl2.record("policy_change", 0.95)
    if ctrl2.level == 0:
        ok("No escalation with < 5 window data βœ“")
    else:
        fail(f"Expected no escalation, level={ctrl2.level}")

    # escalation happens at window=5
    ctrl3 = DriftDifficultyController()
    for _ in range(5):
        ctrl3.record("field_rename", 0.90)
        ctrl3.record("endpoint_version", 0.90)
        ctrl3.record("policy_change", 0.90)
    if ctrl3.level == 1:
        ok("Escalates to level 1 after 5 high-accuracy episodes βœ“")
    else:
        fail(f"Expected level 1 after escalation, got {ctrl3.level}")

    # no escalation if one type below threshold
    ctrl4 = DriftDifficultyController()
    for _ in range(5):
        ctrl4.record("field_rename", 0.90)
        ctrl4.record("endpoint_version", 0.50)  # below threshold
        ctrl4.record("policy_change", 0.90)
    if ctrl4.level == 0:
        ok("No escalation when one drift type below threshold βœ“")
    else:
        fail(f"Expected no escalation, got level {ctrl4.level}")

    # reset
    ctrl5 = DriftDifficultyController()
    ctrl5._escalation_level = 2
    ctrl5.reset()
    if ctrl5.level == 0 and len(ctrl5._history) == 0:
        ok("reset() clears level and history βœ“")
    else:
        fail(f"reset() failed: level={ctrl5.level}, history_len={len(ctrl5._history)}")

    # get_scenario_params at level 0 is identity
    sc = {"id": "easy_field_rename", "drift_trigger_step": 3, "drift_type": "field_rename"}
    result = ctrl.get_scenario_params(sc)
    if result == sc:
        ok("get_scenario_params at level 0 returns unchanged scenario βœ“")
    else:
        fail(f"get_scenario_params unexpected change: {result}")

except Exception as e:
    fail("DriftDifficultyController section failed", e)
    traceback.print_exc()


# ══════════════════════════════════════════════════════════════════════════════
# 5. Models
# ══════════════════════════════════════════════════════════════════════════════
section("5 / 7 Β· Models (AdaptiveAction / AdaptiveObservation / AdaptiveState)")

try:
    from models import AdaptiveAction, AdaptiveObservation, AdaptiveState

    a = AdaptiveAction()
    if a.action_type == "call_api" and a.method == "GET":
        ok("AdaptiveAction defaults: action_type='call_api', method='GET' βœ“")
    else:
        fail(f"AdaptiveAction defaults wrong: {a.action_type}, {a.method}")

    obs = AdaptiveObservation()
    if not hasattr(obs, "drift_occurred") and not hasattr(obs, "drift_type"):
        ok("AdaptiveObservation has NO drift_occurred / drift_type (v2 compliant) βœ“")
    else:
        fail("AdaptiveObservation still has forbidden field drift_occurred / drift_type")

    for field in ["prior_world_model", "episode_history", "belief_accuracy", "difficulty_level"]:
        if hasattr(obs, field):
            ok(f"  AdaptiveObservation has v2 field '{field}' βœ“")
        else:
            fail(f"  AdaptiveObservation missing v2 field '{field}'")

    state = AdaptiveState()
    if not state.drift_injected and state.agent_belief == {} and state.world_truth == {}:
        ok("AdaptiveState defaults correct βœ“")
    else:
        fail(f"AdaptiveState defaults wrong: {state}")

except Exception as e:
    fail("Models section failed", e)
    traceback.print_exc()


# ══════════════════════════════════════════════════════════════════════════════
# 6. Simulated Episode Lifecycle (no live server)
# ══════════════════════════════════════════════════════════════════════════════
section("6 / 7 Β· Simulated Episode Lifecycle (offline, all difficulties)")

try:
    from unittest.mock import patch, MagicMock
    from models import AdaptiveAction, AdaptiveObservation

    def make_mock_http(status=200, body="{}"):
        mock_resp = MagicMock()
        mock_resp.status_code = status
        mock_resp.text = body
        mock_resp.headers = {"content-type": "application/json"}
        return mock_resp

    for difficulty in ["easy", "medium", "hard", "expert"]:
        try:
            with patch("httpx.Client") as MockClient:
                cm = MockClient.return_value.__enter__.return_value
                # Mock the admin mutate call
                cm.post.return_value = make_mock_http(200, "{}")
                # Mock API calls β€” return a successful order
                cm.request.return_value = make_mock_http(
                    200, '{"order_id": "abc123", "status": "confirmed"}'
                )
                cm.get.return_value = make_mock_http(200, '{"endpoints": []}')

                import importlib
                import server.adaptive_world_environment as awe_module
                importlib.reload(awe_module)
                env = awe_module.AdaptiveWorldEnvironment()

                # reset
                obs = env.reset(scenario_id="auto", difficulty=difficulty)
                assert isinstance(obs, AdaptiveObservation), "reset() must return AdaptiveObservation"
                assert not obs.done, "done should be False after reset"
                assert obs.task_description, "task_description should not be empty"

                # step β€” probe schema
                action = AdaptiveAction(action_type="probe_schema")
                obs2 = env.step(action)
                assert isinstance(obs2, AdaptiveObservation)

                # step β€” call_api
                action2 = AdaptiveAction(
                    action_type="call_api",
                    method="POST",
                    url="/mock_api/orders",
                    body={"qty": 1, "product_id": 5},
                )
                obs3 = env.step(action2)
                assert isinstance(obs3, AdaptiveObservation)
                assert env.state.step_count >= 1

                # submit_result
                submit = AdaptiveAction(
                    action_type="submit_result",
                    belief_state={"order_field": "qty", "drift_detected": False},
                )
                obs_final = env.step(submit)
                assert obs_final.done, "done must be True after submit_result"
                assert 0.0 <= obs_final.reward <= 1.0, f"reward out of range: {obs_final.reward}"

                ok(f"  Difficulty '{difficulty}': reset→step→submit OK "
                   f"(reward={obs_final.reward:.4f})")

        except Exception as ex:
            fail(f"  Difficulty '{difficulty}' lifecycle failed", ex)
            traceback.print_exc()

except Exception as e:
    fail("Episode lifecycle section failed", e)
    traceback.print_exc()


# ══════════════════════════════════════════════════════════════════════════════
# 7. Inference.py Import Check
# ══════════════════════════════════════════════════════════════════════════════
section("7 / 7 Β· inference.py Import / Parse Check")

try:
    import ast
    inf_path = os.path.join(ROOT, "inference.py")
    if os.path.exists(inf_path):
        with open(inf_path, "r", encoding="utf-8") as f:
            src = f.read()
        try:
            tree = ast.parse(src)
            ok("inference.py parses without syntax errors βœ“")

            # Check key functions exist
            funcs = {n.name for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)}
            for fn in ["run_episode", "run_evaluation", "build_user_message", "parse_action"]:
                if fn in funcs:
                    ok(f"  Function '{fn}' found in inference.py βœ“")
                else:
                    fail(f"  Function '{fn}' MISSING from inference.py")

            # Check all difficulty choices are listed
            src_lower = src.lower()
            for d in ["easy", "medium", "hard", "expert"]:
                if d in src_lower:
                    ok(f"  Difficulty '{d}' referenced in inference.py βœ“")
                else:
                    fail(f"  Difficulty '{d}' NOT referenced in inference.py")

        except SyntaxError as se:
            fail(f"inference.py has syntax error: {se}")
    else:
        fail(f"inference.py not found at {inf_path}")

except Exception as e:
    fail("inference.py check failed", e)
    traceback.print_exc()


# ══════════════════════════════════════════════════════════════════════════════
# Summary
# ══════════════════════════════════════════════════════════════════════════════
total = PASS + FAIL
print(f"\n{BOLD}{'═'*60}{RESET}")
if FAIL == 0:
    print(f"{GREEN}{BOLD}  ALL {PASS}/{total} CHECKS PASSED βœ“{RESET}")
    print(f"{GREEN}{BOLD}  Submission looks valid!{RESET}")
else:
    print(f"{RED}{BOLD}  {FAIL} / {total} CHECKS FAILED βœ—{RESET}")
    print(f"\n{YELLOW}Failed checks:{RESET}")
    for err in ERRORS:
        print(f"  β€’ {err}")
print(f"{BOLD}{'═'*60}{RESET}\n")

sys.exit(0 if FAIL == 0 else 1)