Spaces:
Running
Running
| # HARvestGym Judge Architecture | |
| This document specifies the full judge architecture — how task completion is verified and how rewards are computed after each episode ends. | |
| The judge is a deterministic, programmatic component. It does **not** use an LLM to score episodes. Every grader produces a score in `[0.0, 1.0]` that is then scaled to the reward range defined in `README.md`. | |
| --- | |
| ## Overview | |
| ``` | |
| Episode ends (model calls done() or max_steps=20 reached) | |
| │ | |
| ▼ | |
| Judge.evaluate(episode: Episode, task: Task) → EpisodeResult | |
| │ | |
| ├─► 1. Identify task template from task.template_id | |
| │ | |
| ├─► 2. Run programmatic grader for this template | |
| │ │ | |
| │ ├─► Probe live application state (HTTP calls from judge, not model) | |
| │ ├─► Inspect episode trajectory (call sequence, parameter sources) | |
| │ └─► Compute score in [0.0, 1.0] | |
| │ | |
| ├─► 3. Verify parameter sourcing (for partial credit) | |
| │ │ | |
| │ └─► Cross-reference each curl call against ground truth catalog | |
| │ | |
| └─► 4. Compute final reward | |
| │ | |
| └─► Combine task score + parameter sourcing + step-level signals | |
| ``` | |
| --- | |
| ## Data Structures | |
| ```python | |
| @dataclass | |
| class Episode: | |
| task: Task | |
| steps: list[Step] # all tool calls and results | |
| session_state: dict # final session state | |
| total_steps: int | |
| terminated_by: str # "done_call" | "max_steps" | |
| @dataclass | |
| class Step: | |
| step_num: int | |
| tool: str # browser_agent | search_endpoints | curl_exec | search_episode_data | done | |
| action: str # raw tool call string | |
| result: Any # tool return value | |
| curl_parsed: CurlCall | None # None for non-curl steps | |
| @dataclass | |
| class CurlCall: | |
| method: str | |
| url: str | |
| path: str # normalized (IDs replaced with {id}) | |
| headers: dict | |
| body: dict | str | None | |
| status_code: int | |
| response_body: Any | |
| @dataclass | |
| class Task: | |
| template_id: int # 1–7 | |
| description: str # instantiated task string (with actual values) | |
| params: dict # e.g. {"product_name": "Radiant Tee", "sku": "MH01"} | |
| app: str # shopping | forum | wikipedia | shopping_admin | |
| base_url: str | |
| difficulty: str # easy | medium | hard | |
| @dataclass | |
| class EpisodeResult: | |
| task_score: float # 0.0–1.0 from grader | |
| parameter_sourcing_score: float # 0.0–1.0 from trajectory analysis | |
| auth_obtained: bool # did the model successfully authenticate? | |
| reward: float # final composite reward | |
| details: dict # per-grader diagnostic info for logging | |
| ``` | |
| --- | |
| ## Graders: Per-Template Verification | |
| Each template has its own grader. All graders make real HTTP calls to the live EC2 application to verify state — they do not rely solely on the episode trajectory. | |
| ### Template 1 — Easy | Shopping: List products in category `{category_name}` | |
| **Success condition:** The model's curl call returned a 200 response containing at least one product in the correct category. | |
| ```python | |
| def grade_template_1(episode: Episode, task: Task) -> float: | |
| category_name = task.params["category_name"] | |
| # Find the curl call that returned products | |
| for step in episode.steps: | |
| if step.curl_parsed and step.curl_parsed.status_code == 200: | |
| body = step.curl_parsed.response_body | |
| if isinstance(body, dict) and "items" in body: | |
| items = body["items"] | |
| # Verify at least one item belongs to the target category | |
| for item in items: | |
| # Check category_links or category name in item | |
| if _item_matches_category(item, category_name): | |
| return 1.0 | |
| # Items returned but wrong category — partial credit | |
| if len(items) > 0: | |
| return 0.3 | |
| return 0.0 | |
| def _item_matches_category(item: dict, category_name: str) -> bool: | |
| """Check category_links or custom_attributes for category match.""" | |
| # Magento items carry category_links: [{"category_id": N}] | |
| # Judge verifies by calling GET /rest/V1/categories?searchCriteria[filter...]=name | |
| # and comparing category IDs. This is a judge-side probe, not relying on model output. | |
| ... | |
| ``` | |
| **Reward mapping:** | |
| | Score | Meaning | Reward | | |
| |-------|---------|--------| | |
| | 1.0 | Products listed from correct category | +2.0 | | |
| | 0.3 | Products returned but wrong/unknown category | +0.5 | | |
| | 0.0 | No valid product list response | −1.5 | | |
| --- | |
| ### Template 2 — Easy | Wikipedia: Retrieve article for `{title}` | |
| **Success condition:** The model made a successful HTTP GET that returned a 200 response for a URL containing the article title (or a redirect to it). Content parsing is explicitly not required. | |
| ```python | |
| def grade_template_2(episode: Episode, task: Task) -> float: | |
| title = task.params["title"] | |
| title_slug = title.lower().replace(" ", "_") | |
| for step in episode.steps: | |
| if step.curl_parsed and step.curl_parsed.status_code == 200: | |
| url = step.curl_parsed.url.lower() | |
| if title_slug in url or title.lower() in url: | |
| return 1.0 | |
| # Check for search result that found the article (indirect) | |
| for step in episode.steps: | |
| if step.curl_parsed and step.curl_parsed.status_code == 200: | |
| body_str = str(step.curl_parsed.response_body).lower() | |
| if title.lower() in body_str and "wiki" in step.curl_parsed.url.lower(): | |
| return 0.5 # found reference but didn't fetch the article directly | |
| return 0.0 | |
| ``` | |
| **Reward mapping:** | |
| | Score | Reward | | |
| |-------|--------| | |
| | 1.0 | Correct article URL fetched with 200 | +2.0 | | |
| | 0.5 | Article title found in search results but not fetched | +0.5 | | |
| | 0.0 | No Wikipedia response | −1.5 | | |
| --- | |
| ### Template 3 — Medium | Shopping: Add `{product_name}` to a guest cart | |
| **Success condition:** Judge probes the cart after the episode to verify the item is present. | |
| ```python | |
| def grade_template_3(episode: Episode, task: Task) -> float: | |
| product_name = task.params["product_name"] | |
| sku = task.params.get("sku") # known from parameter pool | |
| # Extract cart_id from episode trajectory | |
| cart_id = _extract_cart_id(episode) | |
| if not cart_id: | |
| return _partial_score_no_cart(episode) | |
| # Judge probes the live application | |
| cart_response = _judge_probe( | |
| f"GET /rest/V1/guest-carts/{cart_id}", | |
| task.base_url | |
| ) | |
| if not cart_response or cart_response.status_code != 200: | |
| return 0.1 # cart was created but can't be verified | |
| items = cart_response.body.get("items", []) | |
| for item in items: | |
| if item.get("sku") == sku or _fuzzy_match(item.get("name", ""), product_name): | |
| return 1.0 | |
| # Cart exists but item not in it | |
| if len(items) == 0 and cart_id: | |
| return 0.2 # cart created, item not added | |
| return 0.0 | |
| def _partial_score_no_cart(episode: Episode) -> float: | |
| """Partial credit: did the model attempt the right sequence?""" | |
| attempted_cart_create = any( | |
| s.curl_parsed and "guest-carts" in s.curl_parsed.path | |
| and s.curl_parsed.method == "POST" | |
| for s in episode.steps if s.curl_parsed | |
| ) | |
| return 0.15 if attempted_cart_create else 0.0 | |
| ``` | |
| **Reward mapping:** | |
| | Score | Reward | | |
| |-------|--------| | |
| | 1.0 | Item confirmed in cart via judge probe | +3.5 | | |
| | 0.2 | Cart created, item not added | +0.5 | | |
| | 0.15 | Correct call attempted, cart not created | +0.3 | | |
| | 0.0 | No valid attempt | −1.5 | | |
| --- | |
| ### Template 4 — Medium | Forum: Retrieve all posts in `{forum_category}` (authed) | |
| **Success condition:** The model authenticated and fetched a post listing that includes posts from the target category. | |
| ```python | |
| def grade_template_4(episode: Episode, task: Task) -> float: | |
| forum_category = task.params["forum_category"] | |
| score = 0.0 | |
| # Check authentication was obtained | |
| auth_obtained = _check_forum_auth(episode) | |
| if auth_obtained: | |
| score += 0.3 # auth is partial credit on its own (see reward table) | |
| # Find a curl call that returned a post listing for the correct category | |
| for step in episode.steps: | |
| if step.curl_parsed and step.curl_parsed.status_code == 200: | |
| url = step.curl_parsed.url | |
| body = step.curl_parsed.response_body | |
| # Postmill returns post listings at /f/{category} | |
| if f"/f/{forum_category.lower()}" in url.lower(): | |
| if _response_contains_posts(body): | |
| return 1.0 | |
| return score # 0.3 if only auth, 0.0 if nothing | |
| def _check_forum_auth(episode: Episode) -> bool: | |
| """Authentication: a POST to /login returned a redirect (302) or 200 with session cookie.""" | |
| for step in episode.steps: | |
| if step.curl_parsed: | |
| if step.curl_parsed.method == "POST" and "/login" in step.curl_parsed.path: | |
| if step.curl_parsed.status_code in (200, 302): | |
| return True | |
| return False | |
| ``` | |
| **Reward mapping:** | |
| | Score | Reward | | |
| |-------|--------| | |
| | 1.0 | Authenticated + posts fetched from correct category | +3.5 | | |
| | 0.3 | Authentication only, no post fetch | +0.8 | | |
| | 0.0 | No valid attempt | −1.5 | | |
| --- | |
| ### Template 5 — Hard | Forum: Create a post titled `{title}` in `{category}` | |
| **Success condition:** Judge probes the forum category page after the episode to verify the post exists. | |
| ```python | |
| def grade_template_5(episode: Episode, task: Task) -> float: | |
| title = task.params["title"] | |
| category = task.params["category"] | |
| # Phase 1: check authentication | |
| auth_ok = _check_forum_auth(episode) | |
| # Phase 2: check CSRF token was extracted and used | |
| csrf_used = _check_csrf_in_trajectory(episode) | |
| # Phase 3: judge probes the forum to verify post exists | |
| posts = _judge_probe_forum_category(category, task.base_url) | |
| for post in posts: | |
| if _fuzzy_match(post.get("title", ""), title): | |
| return 1.0 | |
| # Partial credit breakdown | |
| if auth_ok and csrf_used: | |
| return 0.5 # got auth and CSRF right, but post didn't land | |
| if auth_ok: | |
| return 0.3 | |
| return 0.0 | |
| def _check_csrf_in_trajectory(episode: Episode) -> bool: | |
| """Check that a POST body contained a _csrf_token field.""" | |
| for step in episode.steps: | |
| if step.curl_parsed and step.curl_parsed.method == "POST": | |
| body_str = str(step.curl_parsed.body or "") | |
| if "_csrf_token" in body_str and len(body_str) > 20: | |
| return True | |
| return False | |
| ``` | |
| **Reward mapping:** | |
| | Score | Reward | | |
| |-------|--------| | |
| | 1.0 | Post confirmed in forum via judge probe | +5.0 | | |
| | 0.5 | Auth + CSRF correct, post not created | +1.5 | | |
| | 0.3 | Auth only | +0.8 | | |
| | 0.0 | No valid attempt | −1.5 | | |
| --- | |
| ### Template 6 — Hard | Shopping: Guest checkout for `{product_name}` | |
| **Success condition:** A complete order was created. Judge checks for an order ID in the trajectory and optionally probes the admin API. | |
| ```python | |
| def grade_template_6(episode: Episode, task: Task) -> float: | |
| sku = task.params.get("sku") | |
| # Check for order ID in trajectory (checkout success returns an integer order ID) | |
| for step in episode.steps: | |
| if step.curl_parsed and step.curl_parsed.status_code == 200: | |
| body = step.curl_parsed.response_body | |
| # Magento checkout success: POST /rest/V1/guest-carts/{id}/order returns integer | |
| if isinstance(body, int) and body > 0: | |
| return 1.0 | |
| # Magento checkout success: body could also be JSON with "order_id" | |
| if isinstance(body, dict) and body.get("order_id"): | |
| return 1.0 | |
| # Partial credit: did the model get through cart + item + shipping? | |
| stages = _checkout_stages_completed(episode, sku) | |
| if stages >= 4: # cart + item + email + shipping estimate | |
| return 0.6 | |
| if stages >= 2: # cart + item | |
| return 0.3 | |
| if stages >= 1: # cart only | |
| return 0.1 | |
| return 0.0 | |
| def _checkout_stages_completed(episode: Episode, sku: str) -> int: | |
| """Count how many checkout stages the model completed successfully.""" | |
| stages = 0 | |
| paths_hit = {s.curl_parsed.path for s in episode.steps if s.curl_parsed and s.curl_parsed.status_code == 200} | |
| if any("guest-carts" in p and "{" not in p for p in paths_hit): stages += 1 # cart created | |
| if any("guest-carts" in p and "items" in p for p in paths_hit): stages += 1 # item added | |
| if any("guest-carts" in p and "shipping" in p for p in paths_hit): stages += 1 # shipping | |
| if any("guest-carts" in p and "payment" in p for p in paths_hit): stages += 1 # payment/order | |
| return stages | |
| ``` | |
| **Reward mapping:** | |
| | Score | Reward | | |
| |-------|--------| | |
| | 1.0 | Order created (order_id in response) | +5.0 | | |
| | 0.6 | 4+ stages completed | +2.5 | | |
| | 0.3 | Cart + item only | +0.8 | | |
| | 0.1 | Cart only | +0.3 | | |
| | 0.0 | No valid attempt | −1.5 | | |
| --- | |
| ### Template 7 — Hard | Shopping Admin: Create product with SKU `{sku}`, price `{price}` | |
| **Success condition:** Judge probes the admin API to confirm the product exists with the correct SKU and price. | |
| ```python | |
| def grade_template_7(episode: Episode, task: Task) -> float: | |
| sku = task.params["sku"] | |
| price = float(task.params["price"]) | |
| # Phase 1: check admin authentication | |
| admin_token = _extract_admin_token(episode) | |
| if not admin_token: | |
| return 0.0 | |
| # Phase 2: judge probes the Magento REST API to confirm product exists | |
| product = _judge_probe( | |
| f"GET /rest/V1/products/{sku}", | |
| task.base_url, | |
| headers={"Authorization": f"Bearer {admin_token}"} | |
| ) | |
| if not product or product.status_code != 200: | |
| # Product might exist under a different auth context — try admin token from env | |
| product = _judge_probe_with_env_admin_token(f"GET /rest/V1/products/{sku}", task.base_url) | |
| if product and product.status_code == 200: | |
| actual_price = float(product.body.get("price", -1)) | |
| price_ok = abs(actual_price - price) < 0.01 | |
| return 1.0 if price_ok else 0.7 # product exists but wrong price | |
| # Partial credit: correct API called with correct schema | |
| if _attempted_product_creation(episode, sku): | |
| return 0.2 | |
| return 0.0 | |
| def _extract_admin_token(episode: Episode) -> str | None: | |
| """Find admin bearer token from a POST /rest/V1/integration/admin/token response.""" | |
| for step in episode.steps: | |
| if step.curl_parsed and step.curl_parsed.status_code == 200: | |
| if "integration/admin/token" in step.curl_parsed.path: | |
| body = step.curl_parsed.response_body | |
| if isinstance(body, str) and len(body) > 10: | |
| return body.strip('"') | |
| return None | |
| ``` | |
| **Reward mapping:** | |
| | Score | Reward | | |
| |-------|--------| | |
| | 1.0 | Product confirmed in Magento with correct price | +5.0 | | |
| | 0.7 | Product exists but wrong price | +2.0 | | |
| | 0.2 | Admin auth + correct endpoint called | +0.5 | | |
| | 0.0 | No admin auth | −1.5 | | |
| --- | |
| ## Parameter Sourcing Verification | |
| In addition to the task-specific grader, the judge runs a parameter sourcing analysis over the full episode trajectory. This cross-references each curl call against the ground truth catalog to verify that parameter values were obtained from the correct sources. | |
| ```python | |
| def verify_parameter_sourcing(episode: Episode, task: Task, catalog: list[dict]) -> float: | |
| """ | |
| Returns a score in [0.0, 1.0] representing how correctly the model | |
| sourced parameter values across all curl calls in the episode. | |
| Checks each curl call against the ground truth catalog entry for that endpoint. | |
| """ | |
| correct = 0 | |
| total = 0 | |
| for step in episode.steps: | |
| if not step.curl_parsed: | |
| continue | |
| catalog_entry = _find_catalog_entry(step.curl_parsed.path, step.curl_parsed.method, catalog) | |
| if not catalog_entry: | |
| continue | |
| # Check each parameter in the curl call | |
| for param_name, param_meta in catalog_entry.get("path_params", {}).items(): | |
| total += 1 | |
| value_used = _extract_path_param(step.curl_parsed.url, param_name, catalog_entry) | |
| if value_used and _param_sourced_correctly(value_used, param_meta, episode, step): | |
| correct += 1 | |
| for param_name, param_meta in catalog_entry.get("body_params", {}).items(): | |
| total += 1 | |
| value_used = _extract_body_param(step.curl_parsed.body, param_name) | |
| if value_used and _param_sourced_correctly(value_used, param_meta, episode, step): | |
| correct += 1 | |
| if total == 0: | |
| return 0.0 | |
| return correct / total | |
| def _param_sourced_correctly(value: Any, param_meta: dict, episode: Episode, step: Step) -> bool: | |
| """ | |
| Verify that a parameter value came from the expected source. | |
| Source types: | |
| TASK_SPEC — value must appear in the task description string | |
| PREV_CALL — value must appear in a prior step's response body | |
| AUTH_FLOW — value must come from an auth response (token, session) | |
| STATIC — value must match a known constant (e.g., store_id = 1) | |
| DERIVED — value must be derivable from another parameter in this call | |
| """ | |
| source = param_meta.get("source") | |
| if source == "TASK_SPEC": | |
| return str(value) in episode.task.description | |
| elif source == "PREV_CALL": | |
| from_endpoint = param_meta.get("from_endpoint") | |
| from_field = param_meta.get("from_field") | |
| # Check prior steps for a response from from_endpoint with value at from_field | |
| for prior_step in episode.steps: | |
| if prior_step.step_num >= step.step_num: | |
| break | |
| if prior_step.curl_parsed: | |
| if _path_matches(prior_step.curl_parsed.path, from_endpoint): | |
| extracted = _extract_field(prior_step.curl_parsed.response_body, from_field) | |
| if str(extracted) == str(value): | |
| return True | |
| return False | |
| elif source == "AUTH_FLOW": | |
| # Value must appear in a session_state field or auth response | |
| return str(value) in str(episode.session_state.values()) | |
| elif source == "STATIC": | |
| expected = param_meta.get("value") | |
| return str(value) == str(expected) | |
| elif source == "DERIVED": | |
| same_as = param_meta.get("same_as") | |
| # Value must equal another param in the same call | |
| # (e.g., quote_id must equal cart_id which is in the path) | |
| if same_as and step.curl_parsed: | |
| other_value = _extract_param_from_call(step.curl_parsed, same_as) | |
| return str(value) == str(other_value) | |
| return False | |
| return False | |
| ``` | |
| --- | |
| ## Final Reward Computation | |
| ```python | |
| def compute_reward( | |
| task_score: float, | |
| parameter_sourcing_score: float, | |
| step_rewards: float, # accumulated per-step rewards from README reward table | |
| auth_obtained: bool, | |
| task: Task, | |
| terminated_by: str | |
| ) -> float: | |
| """ | |
| Combines task grader score, parameter sourcing, and step-level signals | |
| into the final episode reward. | |
| """ | |
| # Map task score to outcome reward (scales with difficulty tier) | |
| tier_multipliers = {"easy": 1.0, "medium": 1.75, "hard": 2.5} | |
| tier = task.difficulty | |
| m = tier_multipliers.get(tier, 1.0) | |
| if task_score == 1.0: | |
| outcome_reward = 2.0 * m # +2.0 (easy), +3.5 (medium), +5.0 (hard) | |
| elif task_score >= 0.5: | |
| outcome_reward = 0.5 * m # partial | |
| elif task_score > 0.0: | |
| outcome_reward = 0.15 * m # minimal attempt credit | |
| else: | |
| outcome_reward = -1.5 # complete failure (same across tiers) | |
| # Auth bonus: applies even on task failure — model learned authentication | |
| auth_bonus = 0.3 if auth_obtained and task_score < 1.0 else 0.0 | |
| # Parameter sourcing bonus (weighted into outcome, not additive) | |
| # Only applied when task succeeds partially — avoids rewarding "busy" episodes | |
| param_bonus = 0.0 | |
| if 0.0 < task_score < 1.0: | |
| param_bonus = parameter_sourcing_score * 0.5 * m | |
| total = outcome_reward + auth_bonus + param_bonus + step_rewards | |
| return round(total, 4) | |
| ``` | |
| **Reward separation guarantee:** | |
| | Episode type | Approximate total reward | | |
| |---|---| | |
| | Easy task success (perfect param sourcing) | +2.0 to +3.2 | | |
| | Easy task failure (busy with steps) | −1.5 + max_step_rewards ≈ −0.2 | | |
| | Hard task success | +5.0 to +7.5 | | |
| | Hard task failure (some progress) | −1.5 + partial ≈ −0.5 to +1.5 | | |
| The terminal outcome reward dominates for complete successes and complete failures. Partial episodes sit in the middle — GRPO can distinguish all three signal zones. | |
| --- | |
| ## Judge Utilities | |
| ```python | |
| def _judge_probe(path: str, base_url: str, headers: dict = None) -> ProbeResult | None: | |
| """ | |
| The judge makes its own HTTP calls to verify application state. | |
| These calls are NOT part of the episode trajectory and do NOT affect rewards. | |
| Judge probes use a dedicated admin token from environment variables. | |
| """ | |
| url = base_url.rstrip("/") + path | |
| admin_headers = {"Authorization": f"Bearer {os.environ['JUDGE_ADMIN_TOKEN']}"} | |
| if headers: | |
| admin_headers.update(headers) | |
| try: | |
| resp = requests.get(url, headers=admin_headers, timeout=10) | |
| return ProbeResult(status_code=resp.status_code, body=resp.json() if resp.text else None) | |
| except Exception: | |
| return None | |
| def _fuzzy_match(s1: str, s2: str, threshold: float = 0.85) -> bool: | |
| """Case-insensitive substring or similarity match.""" | |
| s1, s2 = s1.lower().strip(), s2.lower().strip() | |
| if s1 == s2 or s1 in s2 or s2 in s1: | |
| return True | |
| # Jaccard similarity as fallback | |
| tokens1, tokens2 = set(s1.split()), set(s2.split()) | |
| if not tokens1 or not tokens2: | |
| return False | |
| return len(tokens1 & tokens2) / len(tokens1 | tokens2) >= threshold | |
| ``` | |
| --- | |
| ## Parameter Pool Alignment | |
| The judge is aware that parameter pools are pre-built snapshots of the live application state. For graders that verify values (e.g., SKU, price), the comparison is: | |
| - **SKU matching:** exact string match (SKUs are immutable in Magento) | |
| - **Price matching:** float comparison with ±0.01 tolerance | |
| - **Product name matching:** fuzzy match with 85% threshold (handles whitespace/casing) | |
| - **Category name matching:** fuzzy match, verified against live category tree | |
| The judge does **not** penalize the model if the live application has drifted from the parameter pool (e.g., a product was deleted). In this case, the episode is flagged as `invalid_episode` in the logs and excluded from the training batch. The `build_parameter_pools.py` script should be re-run to refresh the pool if too many episodes are flagged. | |
| --- | |
| ## Concurrent Episode Isolation | |
| All judge probes use read-only endpoints (GETs, admin token reads) to avoid interfering with other concurrent training episodes. The judge never issues write calls to the live application — it only reads state to verify what the model did. | |
| Write isolation (preventing two concurrent episodes from interfering with each other) is handled at the training harness level, not the judge level: | |
| - For **Easy** tasks (read-only): no isolation needed | |
| - For **Medium** tasks (cart operations): each episode uses a fresh guest cart; carts are session-scoped and do not conflict | |
| - For **Hard** tasks (post creation, product creation): episode IDs are embedded in the task params (e.g., SKU is prefixed with episode ID: `{sku}_{episode_id}`) to prevent naming collisions | |
| --- | |
| ## Logging and Diagnostics | |
| Every episode produces a structured log entry: | |
| ```json | |
| { | |
| "episode_id": "ep_1234", | |
| "template_id": 3, | |
| "task_description": "Add 'Radiant Tee' to a guest cart", | |
| "task_score": 1.0, | |
| "parameter_sourcing_score": 0.8, | |
| "auth_obtained": false, | |
| "reward": 3.9, | |
| "step_rewards": 0.85, | |
| "terminated_by": "done_call", | |
| "total_steps": 7, | |
| "grader_details": { | |
| "cart_id_found": "cart-abc123", | |
| "item_confirmed_in_cart": true, | |
| "item_sku": "MH01" | |
| }, | |
| "parameter_sourcing_details": [ | |
| {"step": 5, "param": "cartId", "source": "PREV_CALL", "correct": true}, | |
| {"step": 7, "param": "cartItem.sku", "source": "PREV_CALL", "correct": true}, | |
| {"step": 7, "param": "cartItem.quote_id", "source": "DERIVED", "correct": true} | |
| ] | |
| } | |
| ``` | |
| These logs drive the training analytics and help identify which parameter sourcing patterns the model is still learning. | |