Spaces:
Sleeping
Sleeping
fix: rewrite inference.py to use LiteLLM proxy via API_BASE_URL/API_KEY env vars
Browse files- inference.py +91 -129
- pyproject.toml +1 -0
- requirements.txt +2 -2
- uv.lock +2 -0
inference.py
CHANGED
|
@@ -1,133 +1,91 @@
|
|
| 1 |
"""
|
| 2 |
-
inference.py β
|
| 3 |
Usage:
|
| 4 |
-
python inference.py --task easy
|
| 5 |
-
python inference.py --task
|
| 6 |
-
python inference.py --task hard --agent rule
|
| 7 |
-
python inference.py --task all --agent rule
|
| 8 |
"""
|
| 9 |
|
|
|
|
| 10 |
import argparse
|
| 11 |
import json
|
| 12 |
-
from
|
| 13 |
|
| 14 |
-
from models import StepName,
|
| 15 |
-
from environment import CustomerSupportEnv
|
| 16 |
from graders.base_grader import BaseGrader, HardTaskGrader
|
| 17 |
from tasks import TASK_REGISTRY
|
| 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 |
-
# Step 2: Collect Info
|
| 61 |
-
(
|
| 62 |
-
"To look into this immediately, may I please have your account number "
|
| 63 |
-
"or the email address linked to your subscription? "
|
| 64 |
-
"Could you also confirm the transaction dates and the card used for billing?"
|
| 65 |
-
),
|
| 66 |
-
# Step 3: Investigate
|
| 67 |
-
(
|
| 68 |
-
"Thank you. I am reviewing your billing history right now. "
|
| 69 |
-
"I can see in our billing system that there was indeed a duplicate charge "
|
| 70 |
-
"on both the 1st and the 15th. It appears this was caused by a payment "
|
| 71 |
-
"gateway retry issue on our end. I found the duplicate transaction clearly."
|
| 72 |
-
),
|
| 73 |
-
# Step 4: Resolution
|
| 74 |
-
(
|
| 75 |
-
"I sincerely apologize for this error. I will process a full refund of "
|
| 76 |
-
"$49.99 immediately. You should see this credited back to your card "
|
| 77 |
-
"within 3 to 5 business days. I am also escalating this to our billing "
|
| 78 |
-
"team to ensure this does not happen again. Thank you for your patience."
|
| 79 |
-
),
|
| 80 |
-
],
|
| 81 |
-
|
| 82 |
-
"hard": [
|
| 83 |
-
# Step 1: Empathy (de-escalation required)
|
| 84 |
-
(
|
| 85 |
-
"Mr. Mehta, I am personally and deeply sorry for what you have experienced. "
|
| 86 |
-
"This is completely unacceptable and you have every right to be angry. "
|
| 87 |
-
"I take full responsibility for the failure of our team to respond properly "
|
| 88 |
-
"over the past three weeks. This should not have happened, especially for a "
|
| 89 |
-
"valued VIP customer like yourself. I hear your frustration and I assure you "
|
| 90 |
-
"that I will personally ensure this is resolved today."
|
| 91 |
-
),
|
| 92 |
-
# Step 2: Collect Info
|
| 93 |
-
(
|
| 94 |
-
"To immediately escalate this as our top priority, could you please confirm "
|
| 95 |
-
"your order number and VIP account details? I also need your email and "
|
| 96 |
-
"contact number so I can personally follow up with you today."
|
| 97 |
-
),
|
| 98 |
-
# Step 3: Investigate
|
| 99 |
-
(
|
| 100 |
-
"Thank you, Mr. Mehta. I am reviewing your tickets right now. "
|
| 101 |
-
"I can see five tickets β all marked pending β with no technician dispatched. "
|
| 102 |
-
"I found that this is unacceptable internally. I have already escalated "
|
| 103 |
-
"this to our senior operations manager and our VIP support head. "
|
| 104 |
-
"Our records show the replacement unit is available and ready to ship."
|
| 105 |
-
),
|
| 106 |
-
# Step 4: Resolution
|
| 107 |
-
(
|
| 108 |
-
"Mr. Mehta, here is what I am doing right now: "
|
| 109 |
-
"1) A replacement Premium Standing Desk Pro will be dispatched today with "
|
| 110 |
-
"priority delivery β you will receive it within 24 hours. "
|
| 111 |
-
"2) As per our VIP policy, I am authorizing a compensation of βΉ9,000 "
|
| 112 |
-
"(20% of your order value) credited to your account immediately. "
|
| 113 |
-
"3) I will personally ensure a senior technician contacts you within 2 hours. "
|
| 114 |
-
"4) I am waiving your next month's subscription fee as an additional apology. "
|
| 115 |
-
"I personally guarantee this will be resolved to your satisfaction today."
|
| 116 |
-
),
|
| 117 |
-
],
|
| 118 |
}
|
| 119 |
|
| 120 |
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
-
def run_task(task_name: str) -> dict:
|
| 124 |
-
task = TASK_REGISTRY[task_name]
|
| 125 |
-
grader = HardTaskGrader() if task_name == "hard" else BaseGrader()
|
| 126 |
-
env = CustomerSupportEnv(task=task, grader=grader)
|
| 127 |
-
responses = RULE_BASED_RESPONSES[task_name]
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
print(f"\n{'='*60}")
|
| 133 |
print(f" TASK: {task_name.upper()} | {task.task_id}")
|
|
@@ -136,14 +94,18 @@ def run_task(task_name: str) -> dict:
|
|
| 136 |
print(f" Customer: {task.customer_message[:120]}...")
|
| 137 |
print(f"{'='*60}\n")
|
| 138 |
|
|
|
|
|
|
|
|
|
|
| 139 |
steps_taken = 0
|
| 140 |
-
for i,
|
| 141 |
-
|
| 142 |
-
result, done
|
| 143 |
-
steps_taken
|
| 144 |
|
| 145 |
status = "β
CORRECT" if result.correct else "β WRONG"
|
| 146 |
-
print(f"[Step {i+1}/4] {
|
|
|
|
| 147 |
print(f" Detected : {result.detected_action}")
|
| 148 |
print(f" Reward : {result.reward:.3f} "
|
| 149 |
f"(base={result.base_score:.2f}, bonus={result.step_bonus:.2f}, "
|
|
@@ -153,7 +115,7 @@ def run_task(task_name: str) -> dict:
|
|
| 153 |
print(f" β {pr}")
|
| 154 |
print()
|
| 155 |
|
| 156 |
-
#
|
| 157 |
print(f"[STEP] step={i+1} reward={result.reward:.3f}", flush=True)
|
| 158 |
|
| 159 |
if done:
|
|
@@ -168,7 +130,7 @@ def run_task(task_name: str) -> dict:
|
|
| 168 |
print(f" FAIL REASON : {summary['fail_reason']}")
|
| 169 |
print(f"{'='*60}\n")
|
| 170 |
|
| 171 |
-
#
|
| 172 |
print(
|
| 173 |
f"[END] task={task_name} score={summary['total_reward']:.3f} steps={steps_taken}",
|
| 174 |
flush=True,
|
|
@@ -179,19 +141,19 @@ def run_task(task_name: str) -> dict:
|
|
| 179 |
|
| 180 |
def main():
|
| 181 |
parser = argparse.ArgumentParser()
|
| 182 |
-
parser.add_argument(
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
args
|
|
|
|
| 186 |
|
| 187 |
-
tasks = ["easy", "medium", "hard"] if args.task == "all" else [args.task]
|
| 188 |
results = {}
|
| 189 |
for t in tasks:
|
| 190 |
results[t] = run_task(t)
|
| 191 |
|
| 192 |
print("\nπ FINAL SUMMARY")
|
| 193 |
-
print(json.dumps(results, indent=2))
|
| 194 |
|
| 195 |
|
| 196 |
if __name__ == "__main__":
|
| 197 |
-
main()
|
|
|
|
| 1 |
"""
|
| 2 |
+
inference.py β LLM-based agent using Scaler-injected LiteLLM proxy.
|
| 3 |
Usage:
|
| 4 |
+
python inference.py --task easy
|
| 5 |
+
python inference.py --task all
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
+
import os
|
| 9 |
import argparse
|
| 10 |
import json
|
| 11 |
+
from openai import OpenAI
|
| 12 |
|
| 13 |
+
from models import StepName, EpisodeStatus
|
| 14 |
+
from environment import CustomerSupportEnv, STEP_ORDER
|
| 15 |
from graders.base_grader import BaseGrader, HardTaskGrader
|
| 16 |
from tasks import TASK_REGISTRY
|
| 17 |
|
| 18 |
+
# ββ LLM Client (uses Scaler-injected env vars) ββββββββββββββββββββββββββββββββ
|
| 19 |
+
client = OpenAI(
|
| 20 |
+
base_url=os.environ.get("API_BASE_URL", "https://api.openai.com/v1"),
|
| 21 |
+
api_key=os.environ.get("API_KEY", "no-key"),
|
| 22 |
+
)
|
| 23 |
+
MODEL = os.environ.get("MODEL_NAME", "gpt-4o-mini")
|
| 24 |
+
|
| 25 |
+
# ββ Step-specific system prompts ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
+
STEP_PROMPTS = {
|
| 27 |
+
StepName.EMPATHY: (
|
| 28 |
+
"You are a professional AI customer support agent. "
|
| 29 |
+
"Your ONLY task: Show genuine empathy. Apologize sincerely, validate the "
|
| 30 |
+
"customer's frustration, take responsibility, assure them you will help. "
|
| 31 |
+
"Do NOT ask for information. Do NOT give solutions yet. "
|
| 32 |
+
"Use: 'I am deeply sorry', 'I completely understand', "
|
| 33 |
+
"'This should not have happened', 'I take full responsibility'. "
|
| 34 |
+
"Tone: Warm, sincere. Max 3-4 sentences."
|
| 35 |
+
),
|
| 36 |
+
StepName.COLLECT_INFO: (
|
| 37 |
+
"You are a professional AI customer support agent. "
|
| 38 |
+
"Your ONLY task: Collect the customer's details to investigate their case. "
|
| 39 |
+
"Ask for their order number or account email. "
|
| 40 |
+
"Use phrases like: 'please provide', 'your order number', "
|
| 41 |
+
"'so I can look into this', 'I will need'. "
|
| 42 |
+
"Tone: Professional, direct. Max 2 sentences."
|
| 43 |
+
),
|
| 44 |
+
StepName.INVESTIGATE: (
|
| 45 |
+
"You are a professional AI customer support agent. "
|
| 46 |
+
"Your ONLY task: Investigate and share findings. Say you are reviewing the "
|
| 47 |
+
"case and describe what you found. "
|
| 48 |
+
"Use: 'I am checking', 'I can see in our records', 'I found that', "
|
| 49 |
+
"'Our system shows'. "
|
| 50 |
+
"Do NOT give the final resolution yet. Max 3-4 sentences."
|
| 51 |
+
),
|
| 52 |
+
StepName.RESOLUTION: (
|
| 53 |
+
"You are a professional AI customer support agent. "
|
| 54 |
+
"Your ONLY task: Provide a clear, concrete resolution with a specific action "
|
| 55 |
+
"(refund/replacement/credit/expedite) and timeline. "
|
| 56 |
+
"For VIP/hard tasks, mention 20%% compensation. "
|
| 57 |
+
"Personally guarantee resolution. Max 4-5 sentences."
|
| 58 |
+
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
|
| 62 |
+
def call_llm(task, current_step: StepName) -> str:
|
| 63 |
+
"""Call LLM through the Scaler-injected LiteLLM proxy."""
|
| 64 |
+
system_prompt = STEP_PROMPTS[current_step]
|
| 65 |
+
user_msg = (
|
| 66 |
+
f"Customer message: {task.customer_message}\n"
|
| 67 |
+
f"Context: {task.scenario_context}\n"
|
| 68 |
+
f"Customer emotion: {task.customer_emotion}\n"
|
| 69 |
+
f"Your current task: {current_step.value.upper()}"
|
| 70 |
+
)
|
| 71 |
+
response = client.chat.completions.create(
|
| 72 |
+
model=MODEL,
|
| 73 |
+
messages=[
|
| 74 |
+
{"role": "system", "content": system_prompt},
|
| 75 |
+
{"role": "user", "content": user_msg},
|
| 76 |
+
],
|
| 77 |
+
temperature=0.3,
|
| 78 |
+
max_tokens=250,
|
| 79 |
+
)
|
| 80 |
+
return response.choices[0].message.content.strip()
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
# ββ Runner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 84 |
+
|
| 85 |
+
def run_task(task_name: str) -> dict:
|
| 86 |
+
task = TASK_REGISTRY[task_name]
|
| 87 |
+
grader = HardTaskGrader() if task_name == "hard" else BaseGrader()
|
| 88 |
+
env = CustomerSupportEnv(task=task, grader=grader)
|
| 89 |
|
| 90 |
print(f"\n{'='*60}")
|
| 91 |
print(f" TASK: {task_name.upper()} | {task.task_id}")
|
|
|
|
| 94 |
print(f" Customer: {task.customer_message[:120]}...")
|
| 95 |
print(f"{'='*60}\n")
|
| 96 |
|
| 97 |
+
# Required structured output: START block
|
| 98 |
+
print(f"[START] task={task_name}", flush=True)
|
| 99 |
+
|
| 100 |
steps_taken = 0
|
| 101 |
+
for i, step in enumerate(STEP_ORDER):
|
| 102 |
+
agent_response = call_llm(task, step)
|
| 103 |
+
result, done = env.step(agent_response)
|
| 104 |
+
steps_taken = i + 1
|
| 105 |
|
| 106 |
status = "β
CORRECT" if result.correct else "β WRONG"
|
| 107 |
+
print(f"[Step {i+1}/4] {step.value.upper()} β {status}")
|
| 108 |
+
print(f" Agent : {agent_response[:100]}...")
|
| 109 |
print(f" Detected : {result.detected_action}")
|
| 110 |
print(f" Reward : {result.reward:.3f} "
|
| 111 |
f"(base={result.base_score:.2f}, bonus={result.step_bonus:.2f}, "
|
|
|
|
| 115 |
print(f" β {pr}")
|
| 116 |
print()
|
| 117 |
|
| 118 |
+
# Required structured output: STEP block
|
| 119 |
print(f"[STEP] step={i+1} reward={result.reward:.3f}", flush=True)
|
| 120 |
|
| 121 |
if done:
|
|
|
|
| 130 |
print(f" FAIL REASON : {summary['fail_reason']}")
|
| 131 |
print(f"{'='*60}\n")
|
| 132 |
|
| 133 |
+
# Required structured output: END block
|
| 134 |
print(
|
| 135 |
f"[END] task={task_name} score={summary['total_reward']:.3f} steps={steps_taken}",
|
| 136 |
flush=True,
|
|
|
|
| 141 |
|
| 142 |
def main():
|
| 143 |
parser = argparse.ArgumentParser()
|
| 144 |
+
parser.add_argument(
|
| 145 |
+
"--task", choices=["easy", "medium", "hard", "all"], default="all"
|
| 146 |
+
)
|
| 147 |
+
args = parser.parse_args()
|
| 148 |
+
tasks = ["easy", "medium", "hard"] if args.task == "all" else [args.task]
|
| 149 |
|
|
|
|
| 150 |
results = {}
|
| 151 |
for t in tasks:
|
| 152 |
results[t] = run_task(t)
|
| 153 |
|
| 154 |
print("\nπ FINAL SUMMARY")
|
| 155 |
+
print(json.dumps(results, indent=2), flush=True)
|
| 156 |
|
| 157 |
|
| 158 |
if __name__ == "__main__":
|
| 159 |
+
main()
|
pyproject.toml
CHANGED
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
| 12 |
"uvicorn>=0.23.0",
|
| 13 |
"pydantic>=2.0.0",
|
| 14 |
"pyyaml>=6.0",
|
|
|
|
| 15 |
"openenv-core>=0.2.0",
|
| 16 |
]
|
| 17 |
|
|
|
|
| 12 |
"uvicorn>=0.23.0",
|
| 13 |
"pydantic>=2.0.0",
|
| 14 |
"pyyaml>=6.0",
|
| 15 |
+
"openai>=1.0.0",
|
| 16 |
"openenv-core>=0.2.0",
|
| 17 |
]
|
| 18 |
|
requirements.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
fastapi>=0.100.0
|
| 2 |
uvicorn>=0.23.0
|
| 3 |
-
groq>=0.4.0
|
| 4 |
-
pyyaml>=6.0
|
| 5 |
pydantic>=2.0.0
|
|
|
|
|
|
|
|
|
| 1 |
fastapi>=0.100.0
|
| 2 |
uvicorn>=0.23.0
|
|
|
|
|
|
|
| 3 |
pydantic>=2.0.0
|
| 4 |
+
pyyaml>=6.0
|
| 5 |
+
openai>=1.0.0
|
uv.lock
CHANGED
|
@@ -541,6 +541,7 @@ version = "1.0.0"
|
|
| 541 |
source = { editable = "." }
|
| 542 |
dependencies = [
|
| 543 |
{ name = "fastapi" },
|
|
|
|
| 544 |
{ name = "openenv-core" },
|
| 545 |
{ name = "pydantic" },
|
| 546 |
{ name = "pyyaml" },
|
|
@@ -557,6 +558,7 @@ dev = [
|
|
| 557 |
requires-dist = [
|
| 558 |
{ name = "fastapi", specifier = ">=0.100.0" },
|
| 559 |
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.24.0" },
|
|
|
|
| 560 |
{ name = "openenv-core", specifier = ">=0.2.0" },
|
| 561 |
{ name = "pydantic", specifier = ">=2.0.0" },
|
| 562 |
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" },
|
|
|
|
| 541 |
source = { editable = "." }
|
| 542 |
dependencies = [
|
| 543 |
{ name = "fastapi" },
|
| 544 |
+
{ name = "openai" },
|
| 545 |
{ name = "openenv-core" },
|
| 546 |
{ name = "pydantic" },
|
| 547 |
{ name = "pyyaml" },
|
|
|
|
| 558 |
requires-dist = [
|
| 559 |
{ name = "fastapi", specifier = ">=0.100.0" },
|
| 560 |
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.24.0" },
|
| 561 |
+
{ name = "openai", specifier = ">=1.0.0" },
|
| 562 |
{ name = "openenv-core", specifier = ">=0.2.0" },
|
| 563 |
{ name = "pydantic", specifier = ">=2.0.0" },
|
| 564 |
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" },
|