File size: 10,809 Bytes
7043cc6 0983a18 7043cc6 d78447a 0983a18 d78447a 7043cc6 d78447a 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 d78447a 7043cc6 0983a18 7043cc6 0983a18 7043cc6 d78447a 7043cc6 d78447a aaf289c 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 aaf289c 7043cc6 | 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 | """
inference.py β LLM inference loop for AdaptiveWorld environment (v2).
Updated from Round 1 api-debug-env inference.py to:
- Use AdaptiveAction / AdaptiveObservation v2 types
- Support all 5 action types: call_api, probe_schema, query_history,
declare_belief, submit_result
- System prompt guides the agent to detect drift from evidence
(NOT from an explicit drift_occurred field β that's removed in v2)
- Extract belief_state from LLM output for belief scoring
- Track task_reward and belief_accuracy separately across episodes
"""
import os
import json
import re
from typing import Optional
from dotenv import load_dotenv
from openai import OpenAI
from client import AdaptiveWorldEnv
from models import AdaptiveAction, AdaptiveObservation
load_dotenv()
# ββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ENV_URL = os.getenv("ENV_URL", "http://localhost:7860")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "meta-llama/Llama-3.3-70B-Instruct")
MAX_STEPS = int(os.getenv("MAX_STEPS", "12"))
DIFFICULTY = os.getenv("DIFFICULTY", "easy")
# Use Hugging Face serverless router using local HF token
import huggingface_hub
hf_token = os.getenv("HF_TOKEN") or huggingface_hub.get_token() or "dummy"
client = OpenAI(
base_url="https://api.groq.com/openai/v1",
api_key=hf_token
)
env = AdaptiveWorldEnv(base_url=ENV_URL).sync()
# ββ System prompt ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SYSTEM_PROMPT = """You are an agent completing professional tasks via HTTP APIs.
IMPORTANT: The API world can mutate mid-episode without warning.
- Field names may change (e.g. "qty" β "quantity")
- Endpoints may move (e.g. /book β /v2/book)
- Policies may change (new required fields, new auth schemes)
- Status values may change silently (200 OK but different meaning)
You will NOT be told when a drift occurs. You must detect it from evidence.
AVAILABLE ACTION TYPES:
1. call_api β Make an HTTP request to the mock API
2. probe_schema β GET /openapi.json to inspect current API contract
3. query_history β Review your last N API responses, compare for changes
4. declare_belief β State your current understanding of the world (saves for later scoring)
5. submit_result β End the episode. Include your final belief_state about what changed.
STRATEGY:
- Start with probe_schema to understand the initial API contract
- After any unexpected error (422, 404, 401), call query_history first to compare responses
- If responses look the same but downstream fails silently, the MEANING may have changed
- Use declare_belief to record discoveries as you go
- Always submit_result with your final belief_state β this is scored separately from task completion*
*Belief accuracy is tracked independently. You can score high on belief even if the task fails.
RESPONSE FORMAT (JSON, always):
{
"action_type": "call_api", // or probe_schema, query_history, declare_belief, submit_result
"method": "POST", // for call_api
"url": "/mock_api/orders", // for call_api
"headers": {"Content-Type": "application/json"}, // for call_api
"body": {"quantity": 2, "product_id": 5}, // for call_api
"query_params": {}, // for call_api GET requests
"belief_state": { // for declare_belief or submit_result
"order_field": "quantity",
"required_extra": "customer_id"
},
"history_steps": 3 // for query_history
}
"""
def build_user_message(obs: AdaptiveObservation, step: int) -> str:
"""Build the per-step user message from the current observation."""
parts = [
f"=== Step {step} ===",
f"Task: {obs.task_description}",
f"Domain: {obs.domain}",
f"Progress: {obs.current_step}/{obs.max_steps} steps",
f"Difficulty level: {obs.difficulty_level}/3",
]
if obs.prior_world_model:
parts.append(
f"\\nYour prior world model (from previous episodes in this domain):\\n"
f"{json.dumps(obs.prior_world_model, indent=2)}\\n"
"NOTE: This may be stale if the world has changed since last episode."
)
if obs.last_status_code > 0:
parts.append(f"\\nLast API call:")
parts.append(f" Status: {obs.last_status_code}")
parts.append(f" Response: {obs.last_response_body[:800]}")
if obs.episode_history:
parts.append(f"\\nEpisode history (last {len(obs.episode_history)} steps):")
parts.append(json.dumps(obs.episode_history, indent=2)[:1500])
parts.append(f"\\nFeedback: {obs.step_feedback}")
parts.append(
"\\nRespond with a JSON action object. "
"If you detect drift, use declare_belief to record it before continuing. "
"When ready to finish, use submit_result with your final belief_state."
)
return "\\n".join(parts)
def parse_action(llm_output: str) -> AdaptiveAction:
"""Parse LLM output to AdaptiveAction. Falls back to safe default on parse error."""
try:
# Extract JSON from markdown code blocks or raw output
match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', llm_output, re.DOTALL)
if match:
raw = match.group(1)
else:
raw_match = re.search(r'\{.*\}', llm_output, re.DOTALL)
raw = raw_match.group() if raw_match else "{}"
data = json.loads(raw)
return AdaptiveAction(
action_type=data.get("action_type", "call_api"),
method=data.get("method", "GET"),
url=data.get("url", "/mock_api/orders"),
headers=data.get("headers", {}),
body=data.get("body", {}),
query_params=data.get("query_params", {}),
belief_state=data.get("belief_state"),
history_steps=int(data.get("history_steps", 3)),
)
except Exception as e:
print(f"[parse_action] Parse error: {e}. Defaulting to probe_schema.")
return AdaptiveAction(action_type="probe_schema")
def run_episode(
scenario_id: str = "auto",
difficulty: str = "easy",
verbose: bool = True,
) -> dict:
"""
Run one complete episode.
Returns:
dict with task_reward, belief_accuracy, combined_reward,
steps_taken, drift_detected
"""
step_result = env.reset(scenario_id=scenario_id, difficulty=difficulty)
if verbose:
print(f"\\n{'='*60}")
print(f"EPISODE START")
print(f"Task: {step_result.observation.task_description}")
print(f"Domain: {step_result.observation.domain}")
print(f"Difficulty: {step_result.observation.difficulty_level}/3")
print(f"Prior beliefs: {step_result.observation.prior_world_model}")
print(f"{'='*60}")
conversation: list = [{"role": "system", "content": SYSTEM_PROMPT}]
step = 0
final_reward = 0.0
task_reward = 0.0
belief_accuracy = 0.0
while not step_result.done and step < MAX_STEPS:
step += 1
user_msg = build_user_message(step_result.observation, step)
conversation.append({"role": "user", "content": user_msg})
# LLM call
response = client.chat.completions.create(
model=OPENAI_MODEL,
messages=conversation,
temperature=0.2,
max_tokens=512,
)
llm_output = response.choices[0].message.content
conversation.append({"role": "assistant", "content": llm_output})
if verbose:
print(f"\n--- Step {step} ---")
print(f"LLM: {llm_output[:400]}")
action = parse_action(llm_output)
if verbose:
print(f"Action: {action.action_type} | {action.method} {action.url}")
step_result = env.step(action)
final_reward = step_result.reward
if verbose:
print(f"Status: {step_result.observation.last_status_code} | Reward: {step_result.reward:.4f}")
if step_result.observation.step_feedback:
print(f"Feedback: {step_result.observation.step_feedback}")
if step_result.done:
task_reward = step_result.observation.task_reward
belief_accuracy = step_result.observation.belief_accuracy
break
if verbose:
print(f"\n{'='*60}")
print(f"EPISODE COMPLETE")
print(f"Steps taken: {step}")
print(f"Task reward: {task_reward:.4f}")
print(f"Belief accuracy: {belief_accuracy:.4f}")
print(f"Combined reward: {final_reward:.4f}")
print(f"{'='*60}\n")
return {
"task_reward": task_reward,
"belief_accuracy": belief_accuracy,
"combined_reward": final_reward,
"steps_taken": step,
}
def run_evaluation(
n_episodes: int = 10,
scenario_id: str = "auto",
difficulty: str = "easy",
) -> dict:
"""
Run N episodes and return aggregate statistics.
Used for Day 3 reward signal verification.
"""
results = []
for i in range(n_episodes):
print(f"\n=== Episode {i+1}/{n_episodes} ===")
r = run_episode(scenario_id=scenario_id, difficulty=difficulty, verbose=True)
results.append(r)
task_rewards = [r["task_reward"] for r in results]
belief_accs = [r["belief_accuracy"] for r in results]
combined = [r["combined_reward"] for r in results]
summary = {
"n_episodes": n_episodes,
"avg_task_reward": sum(task_rewards) / len(task_rewards),
"avg_belief_accuracy": sum(belief_accs) / len(belief_accs),
"avg_combined_reward": sum(combined) / len(combined),
"max_task_reward": max(task_rewards),
"max_belief_accuracy": max(belief_accs),
}
print("\n=== EVALUATION SUMMARY ===")
for k, v in summary.items():
print(f" {k}: {v:.4f}" if isinstance(v, float) else f" {k}: {v}")
return summary
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="AdaptiveWorld Inference")
parser.add_argument("--scenario", default="auto", help="Scenario ID or 'auto'")
parser.add_argument("--difficulty", default="easy",
choices=["easy", "medium", "hard", "expert"])
parser.add_argument("--episodes", type=int, default=1)
args = parser.parse_args()
if args.episodes == 1:
run_episode(scenario_id=args.scenario, difficulty=args.difficulty)
else:
run_evaluation(
n_episodes=args.episodes,
scenario_id=args.scenario,
difficulty=args.difficulty,
)
|