File size: 8,497 Bytes
474cf2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c5d888
474cf2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Hackathon inference loop for the EmailTriage OpenEnv environment.

Runs all 3 tasks (easy, medium, hard) sequentially using the OpenAI client.
Emits structured [START]/[STEP]/[END] logs per the hackathon spec.
"""

import os
import json
from typing import List, Optional

from openai import OpenAI

from EmailTriage import EmailtriageAction, EmailtriageEnv

API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
API_KEY = os.getenv("HF_TOKEN")
LOCAL_IMAGE_NAME = os.getenv("IMAGE_NAME")
BENCHMARK_NAME = "openenv-emailtriage"

TASK_IDS = ["easy", "medium", "hard"]

# Per-task step budgets (must fit within 20min total runtime)
TASK_MAX_STEPS = {
    "easy": 6,
    "medium": 10,
    "hard": 12,
}


# ---------------------------------------------------------------------------
# Structured stdout logging (hackathon spec)
# ---------------------------------------------------------------------------


def log_start(task: str, env: str, model: str) -> None:
    print(f"[START] task={task} env={env} model={model}", flush=True)


def log_step(
    step: int,
    action: str,
    reward: float,
    done: bool,
    error: Optional[str],
) -> None:
    error_value = error if error else "null"
    print(
        f"[STEP] step={step} action={action} reward={reward:.2f} "
        f"done={str(done).lower()} error={error_value}",
        flush=True,
    )


def log_end(success: bool, steps: int, rewards: List[float]) -> None:
    rewards_str = ",".join(f"{value:.2f}" for value in rewards)
    print(
        f"[END] success={str(success).lower()} "
        f"steps={steps} rewards={rewards_str}",
        flush=True,
    )


# ---------------------------------------------------------------------------
# Prompt construction
# ---------------------------------------------------------------------------

SYSTEM_PROMPT = (
    "You are an elite, proactive email triage assistant operating in a strictly structured environment. "
    "Your goal is to process the entire inbox efficiently, maximizing your rewards.\n"
    "CRITICAL RULES FOR STATE ADVANCEMENT:\n"
    "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"
    "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"
    "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"
    "4. RESPONDING: If 'Recently read emails' contains a client email that needs a reply, immediately use action_type='draft_email'. "
    "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"
    "5. SCHEDULING CALENDAR: If a read email asks for a meeting, first use action_type='query_calendar' (target_email_id=-1) to load availability. "
    "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"
    "6. JSON FORMAT: Respond ONLY with valid JSON. Keys required: action_type, target_email_id, draft_content, proposed_slot. No markdown, no conversational text."
)


def build_user_prompt(
    task_id: str,
    inbox_preview: List[dict],
    returned_emails: List[str],
    calendar_slots: List[str],
    last_action_result: str,
) -> str:
    slots = ", ".join(calendar_slots) if calendar_slots else "none"
    inbox_lines = [
        f"id={item.get('id')} sender={item.get('sender')} "
        f"priority={item.get('priority')} subject={item.get('subject')}"
        for item in inbox_preview
    ]
    inbox_block = (
        " | ".join(inbox_lines) if inbox_lines else "no unread emails"
    )
    reads_block = " | ".join(returned_emails) if returned_emails else "none"

    return (
        f"Task difficulty: {task_id}. "
        f"Inbox preview: {inbox_block}. "
        f"Recently read emails: {reads_block}. "
        f"Calendar slots: {slots}. "
        f"Last action result: {last_action_result}."
    )


# ---------------------------------------------------------------------------
# LLM action selection
# ---------------------------------------------------------------------------


def choose_action_with_llm(
    client: OpenAI,
    task_id: str,
    prompt: str,
) -> EmailtriageAction:
    default_action = EmailtriageAction(
        action_type="query_calendar",
        target_email_id=-1,
        draft_content="",
        proposed_slot="",
    )

    try:
        completion = client.chat.completions.create(
            model=MODEL_NAME,
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": prompt},
            ],
            temperature=0.2,
            max_tokens=200,
            stream=False,
        )
        raw_content = (completion.choices[0].message.content or "").strip()
        if not raw_content:
            return default_action

        # Strip markdown fences if the model wraps JSON
        if raw_content.startswith("```"):
            lines = raw_content.split("\n")
            lines = [l for l in lines if not l.strip().startswith("```")]
            raw_content = "\n".join(lines)

        data = json.loads(raw_content)
        return EmailtriageAction(
            action_type=data.get("action_type", "query_calendar"),
            target_email_id=int(data.get("target_email_id", -1)),
            draft_content=data.get("draft_content", ""),
            proposed_slot=data.get("proposed_slot", ""),
        )
    except Exception:
        return default_action


# ---------------------------------------------------------------------------
# Single-task runner
# ---------------------------------------------------------------------------


async def run_task(
    llm_client: OpenAI,
    env: EmailtriageEnv,
    task_id: str,
) -> None:
    """Run a single task (easy/medium/hard) and emit structured logs."""
    max_steps = TASK_MAX_STEPS[task_id]
    task_name = f"email-triage-{task_id}"
    rewards: List[float] = []
    steps_taken = 0
    success = False

    log_start(task=task_name, env=BENCHMARK_NAME, model=MODEL_NAME)

    try:
        result = await env.reset(options={"task_id": task_id})

        for step in range(1, max_steps + 1):
            obs = result.observation
            if result.done or obs.inbox_remaining <= 0:
                break

            prompt = build_user_prompt(
                task_id=task_id,
                inbox_preview=obs.inbox_preview,
                returned_emails=obs.returned_emails,
                calendar_slots=obs.calendar_slots,
                last_action_result=obs.last_action_result,
            )
            action = choose_action_with_llm(llm_client, task_id, prompt)
            result = await env.step(action)

            reward = float(result.reward or 0.0)
            rewards.append(reward)
            steps_taken = step

            action_str = (
                f"{action.action_type}("
                f"target_email_id={action.target_email_id},"
                f"proposed_slot={action.proposed_slot})"
            )
            log_step(
                step=step,
                action=action_str,
                reward=reward,
                done=bool(result.done),
                error=None,
            )

            if result.done:
                break

        if rewards:
            avg = sum(rewards) / len(rewards)
            success = avg >= 0.5

    finally:
        log_end(success=success, steps=steps_taken, rewards=rewards)


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------


async def main() -> None:
    if not API_KEY:
        raise RuntimeError(
            "HF_TOKEN must be set in environment variables."
        )

    llm_client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
    
    env = await EmailtriageEnv.from_docker_image(LOCAL_IMAGE_NAME)



    try:
        for task_id in TASK_IDS:
            await run_task(llm_client, env, task_id)
    finally:
        await env.close()


if __name__ == "__main__":
    import asyncio
    asyncio.run(main())