yash-080706 commited on
Commit
474cf2d
·
verified ·
1 Parent(s): a69c08b

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. inference.py +251 -0
inference.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hackathon inference loop for the EmailTriage OpenEnv environment.
2
+
3
+ Runs all 3 tasks (easy, medium, hard) sequentially using the OpenAI client.
4
+ Emits structured [START]/[STEP]/[END] logs per the hackathon spec.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ from typing import List, Optional
10
+
11
+ from openai import OpenAI
12
+
13
+ from EmailTriage import EmailtriageAction, EmailtriageEnv
14
+
15
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
16
+ MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
17
+ API_KEY = os.getenv("HF_TOKEN")
18
+ LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME", "emailtriage-env:latest")
19
+ BENCHMARK_NAME = "openenv-emailtriage"
20
+
21
+ TASK_IDS = ["easy", "medium", "hard"]
22
+
23
+ # Per-task step budgets (must fit within 20min total runtime)
24
+ TASK_MAX_STEPS = {
25
+ "easy": 6,
26
+ "medium": 10,
27
+ "hard": 12,
28
+ }
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Structured stdout logging (hackathon spec)
33
+ # ---------------------------------------------------------------------------
34
+
35
+
36
+ def log_start(task: str, env: str, model: str) -> None:
37
+ print(f"[START] task={task} env={env} model={model}", flush=True)
38
+
39
+
40
+ def log_step(
41
+ step: int,
42
+ action: str,
43
+ reward: float,
44
+ done: bool,
45
+ error: Optional[str],
46
+ ) -> None:
47
+ error_value = error if error else "null"
48
+ print(
49
+ f"[STEP] step={step} action={action} reward={reward:.2f} "
50
+ f"done={str(done).lower()} error={error_value}",
51
+ flush=True,
52
+ )
53
+
54
+
55
+ def log_end(success: bool, steps: int, rewards: List[float]) -> None:
56
+ rewards_str = ",".join(f"{value:.2f}" for value in rewards)
57
+ print(
58
+ f"[END] success={str(success).lower()} "
59
+ f"steps={steps} rewards={rewards_str}",
60
+ flush=True,
61
+ )
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Prompt construction
66
+ # ---------------------------------------------------------------------------
67
+
68
+ SYSTEM_PROMPT = (
69
+ "You are an elite, proactive email triage assistant operating in a strictly structured environment. "
70
+ "Your goal is to process the entire inbox efficiently, maximizing your rewards.\n"
71
+ "CRITICAL RULES FOR STATE ADVANCEMENT:\n"
72
+ "1. AVOID LOOPS: Check the 'Last action result' and 'Recently read emails'. If you just read an email, DO NOT read it again. You must take the next logical step (archive or draft_email).\n"
73
+ "2. SPAM/NEWSLETTERS: If an unread email subject from the 'Inbox preview' clearly looks like spam, marketing, or a low-priority notification, immediately use action_type='archive'.\n"
74
+ "3. IMPORTANT EMAILS: If an unread email is a client request, meeting, or escalation, use action_type='read' first to get the full text.\n"
75
+ "4. RESPONDING: If 'Recently read emails' contains a client email that needs a reply, immediately use action_type='draft_email'. "
76
+ "Your draft_content MUST be professional, mention 'thank', reference specific details from the subject, end firmly with a period, and be over 40 characters.\n"
77
+ "5. SCHEDULING CALENDAR: If a read email asks for a meeting, first use action_type='query_calendar' (target_email_id=-1) to load availability. "
78
+ "In your VERY NEXT turn, use action_type='draft_email' and provide one of the listed slots exactly as shown in the 'proposed_slot' field.\n"
79
+ "6. JSON FORMAT: Respond ONLY with valid JSON. Keys required: action_type, target_email_id, draft_content, proposed_slot. No markdown, no conversational text."
80
+ )
81
+
82
+
83
+ def build_user_prompt(
84
+ task_id: str,
85
+ inbox_preview: List[dict],
86
+ returned_emails: List[str],
87
+ calendar_slots: List[str],
88
+ last_action_result: str,
89
+ ) -> str:
90
+ slots = ", ".join(calendar_slots) if calendar_slots else "none"
91
+ inbox_lines = [
92
+ f"id={item.get('id')} sender={item.get('sender')} "
93
+ f"priority={item.get('priority')} subject={item.get('subject')}"
94
+ for item in inbox_preview
95
+ ]
96
+ inbox_block = (
97
+ " | ".join(inbox_lines) if inbox_lines else "no unread emails"
98
+ )
99
+ reads_block = " | ".join(returned_emails) if returned_emails else "none"
100
+
101
+ return (
102
+ f"Task difficulty: {task_id}. "
103
+ f"Inbox preview: {inbox_block}. "
104
+ f"Recently read emails: {reads_block}. "
105
+ f"Calendar slots: {slots}. "
106
+ f"Last action result: {last_action_result}."
107
+ )
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # LLM action selection
112
+ # ---------------------------------------------------------------------------
113
+
114
+
115
+ def choose_action_with_llm(
116
+ client: OpenAI,
117
+ task_id: str,
118
+ prompt: str,
119
+ ) -> EmailtriageAction:
120
+ default_action = EmailtriageAction(
121
+ action_type="query_calendar",
122
+ target_email_id=-1,
123
+ draft_content="",
124
+ proposed_slot="",
125
+ )
126
+
127
+ try:
128
+ completion = client.chat.completions.create(
129
+ model=MODEL_NAME,
130
+ messages=[
131
+ {"role": "system", "content": SYSTEM_PROMPT},
132
+ {"role": "user", "content": prompt},
133
+ ],
134
+ temperature=0.2,
135
+ max_tokens=200,
136
+ stream=False,
137
+ )
138
+ raw_content = (completion.choices[0].message.content or "").strip()
139
+ if not raw_content:
140
+ return default_action
141
+
142
+ # Strip markdown fences if the model wraps JSON
143
+ if raw_content.startswith("```"):
144
+ lines = raw_content.split("\n")
145
+ lines = [l for l in lines if not l.strip().startswith("```")]
146
+ raw_content = "\n".join(lines)
147
+
148
+ data = json.loads(raw_content)
149
+ return EmailtriageAction(
150
+ action_type=data.get("action_type", "query_calendar"),
151
+ target_email_id=int(data.get("target_email_id", -1)),
152
+ draft_content=data.get("draft_content", ""),
153
+ proposed_slot=data.get("proposed_slot", ""),
154
+ )
155
+ except Exception:
156
+ return default_action
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Single-task runner
161
+ # ---------------------------------------------------------------------------
162
+
163
+
164
+ async def run_task(
165
+ llm_client: OpenAI,
166
+ env: EmailtriageEnv,
167
+ task_id: str,
168
+ ) -> None:
169
+ """Run a single task (easy/medium/hard) and emit structured logs."""
170
+ max_steps = TASK_MAX_STEPS[task_id]
171
+ task_name = f"email-triage-{task_id}"
172
+ rewards: List[float] = []
173
+ steps_taken = 0
174
+ success = False
175
+
176
+ log_start(task=task_name, env=BENCHMARK_NAME, model=MODEL_NAME)
177
+
178
+ try:
179
+ result = await env.reset(options={"task_id": task_id})
180
+
181
+ for step in range(1, max_steps + 1):
182
+ obs = result.observation
183
+ if result.done or obs.inbox_remaining <= 0:
184
+ break
185
+
186
+ prompt = build_user_prompt(
187
+ task_id=task_id,
188
+ inbox_preview=obs.inbox_preview,
189
+ returned_emails=obs.returned_emails,
190
+ calendar_slots=obs.calendar_slots,
191
+ last_action_result=obs.last_action_result,
192
+ )
193
+ action = choose_action_with_llm(llm_client, task_id, prompt)
194
+ result = await env.step(action)
195
+
196
+ reward = float(result.reward or 0.0)
197
+ rewards.append(reward)
198
+ steps_taken = step
199
+
200
+ action_str = (
201
+ f"{action.action_type}("
202
+ f"target_email_id={action.target_email_id},"
203
+ f"proposed_slot={action.proposed_slot})"
204
+ )
205
+ log_step(
206
+ step=step,
207
+ action=action_str,
208
+ reward=reward,
209
+ done=bool(result.done),
210
+ error=None,
211
+ )
212
+
213
+ if result.done:
214
+ break
215
+
216
+ if rewards:
217
+ avg = sum(rewards) / len(rewards)
218
+ success = avg >= 0.5
219
+
220
+ finally:
221
+ log_end(success=success, steps=steps_taken, rewards=rewards)
222
+
223
+
224
+ # ---------------------------------------------------------------------------
225
+ # Main
226
+ # ---------------------------------------------------------------------------
227
+
228
+
229
+ async def main() -> None:
230
+ if not API_KEY:
231
+ raise RuntimeError(
232
+ "HF_TOKEN must be set in environment variables."
233
+ )
234
+
235
+ llm_client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
236
+
237
+ env = await EmailtriageEnv.from_docker_image(LOCAL_IMAGE_NAME)
238
+
239
+
240
+
241
+ try:
242
+ for task_id in TASK_IDS:
243
+ await run_task(llm_client, env, task_id)
244
+ finally:
245
+ await env.close()
246
+
247
+
248
+ if __name__ == "__main__":
249
+ import asyncio
250
+ asyncio.run(main())
251
+