File size: 6,150 Bytes
c48778f
 
 
 
 
 
 
 
 
 
 
 
10e9b7d
c48778f
 
 
 
10e9b7d
3c4371f
c48778f
 
527d5e0
712c104
17dcb7d
c48778f
 
 
 
 
 
 
 
10e9b7d
3db6293
d1a4341
 
c48778f
e80aab9
3c4371f
c48778f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e4a06b
e80aab9
31243f4
 
 
c48778f
3c4371f
527d5e0
712c104
 
527d5e0
712c104
527d5e0
 
712c104
c48778f
eccf8e4
c48778f
 
 
7d65c66
c48778f
 
 
 
 
 
 
 
712c104
c48778f
 
 
 
 
d1a4341
c48778f
 
d1a4341
c48778f
 
 
712c104
 
 
 
 
c48778f
 
 
 
 
 
 
 
 
712c104
 
c48778f
 
 
 
e80aab9
c48778f
 
 
 
527d5e0
c48778f
 
 
 
 
e80aab9
 
c48778f
7d65c66
c48778f
 
e80aab9
 
 
c48778f
0ee0419
c48778f
 
 
e80aab9
7e4a06b
c48778f
31243f4
c48778f
 
 
e80aab9
 
3c4371f
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
"""
app.py: Hugging Face Agents Course, Unit 4 final assignment (faster version).

Changes from the template:
- questions run in parallel (WORKERS at a time); the rate limiter in agent.py keeps
  the total number of model calls inside the free tier
- each question has a time cap, so one stuck question can't hold up the whole run
- answers are cached in answers_cache.json: if a run stops halfway, the next run
  only answers what's missing
- a placeholder @spaces.GPU function, needed only when the Space runs on ZeroGPU
"""

import os
import json
import time
import concurrent.futures as cf

import gradio as gr
import pandas as pd
import requests

import agent as agent_module
from agent import BasicAgent, check_model

try:                       # ZeroGPU checks for one @spaces.GPU function at startup
    import spaces

    @spaces.GPU
    def gpu_placeholder():
        return None
except ImportError:        # CPU Spaces don't have (or need) the spaces package
    pass

DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
WORKERS = 2                  # questions answered at the same time (the agent's rate limiter paces Groq anyway)
RUN_TIME_CAP = 3600          # seconds for the whole run; unfinished questions are submitted as "unknown"
CACHE_FILE = "answers_cache.json"


def load_cache():
    try:
        with open(CACHE_FILE) as f:
            return json.load(f)
    except (OSError, ValueError):
        return {}


def save_cache(cache):
    with open(CACHE_FILE, "w") as f:
        json.dump(cache, f, indent=2)


def run_and_submit_all(profile: gr.OAuthProfile | None, use_cache: bool):
    if not profile:
        return "Please log in to Hugging Face with the button above.", None
    username = profile.username
    space_id = os.getenv("SPACE_ID")
    agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
    api_url = DEFAULT_API_URL

    try:
        agent = BasicAgent()
    except Exception as e:
        return f"Error creating the agent: {e}", None

    # 0. check the API key and quota with one tiny call before spending time on 20 questions
    problem = check_model()
    if problem:
        return ("Test call to the model failed, so nothing was run or submitted.\n"
                f"{problem[:800]}\n\n"
                "Usual causes: Groq's daily free limit used up (wait until tomorrow), a wrong or missing "
                "GROQ_API_KEY secret, or a model name that changed (edit MODEL_ID in agent.py)."), None

    # 1. fetch questions
    try:
        r = requests.get(f"{api_url}/questions", timeout=20)
        r.raise_for_status()
        questions = r.json()
    except Exception as e:
        return f"Error fetching questions: {e}", None
    if not questions:
        return "The question list is empty.", None

    # 2. answer them in parallel, reusing cached answers
    cache = load_cache() if use_cache else {}
    todo = [q for q in questions if q.get("task_id") and q["task_id"] not in cache]
    print(f"{len(questions)} questions, {len(questions) - len(todo)} from cache, {len(todo)} to answer")
    done = 0

    start = time.time()
    pool = cf.ThreadPoolExecutor(max_workers=WORKERS)
    futures = {pool.submit(agent, q["question"], q["task_id"]): q for q in todo}
    try:
        for fut in cf.as_completed(futures, timeout=RUN_TIME_CAP):
            q = futures[fut]
            try:
                answer = fut.result()
            except Exception as e:
                print(f"[{q['task_id']}] error: {e}")
                answer = "unknown"
            if answer and answer != "unknown":      # only real answers are cached
                cache[q["task_id"]] = answer
                save_cache(cache)
            done += 1
            print(f"[{done}/{len(todo)}] {time.time() - start:.0f}s  {answer!r}")
    except cf.TimeoutError:
        print("Time cap reached; unfinished questions are submitted as 'unknown'.")
    pool.shutdown(wait=False, cancel_futures=True)

    # 3. build the payload and the results table
    answers_payload, log = [], []
    for q in questions:
        answer = cache.get(q["task_id"], "unknown")
        answers_payload.append({"task_id": q["task_id"], "submitted_answer": answer})
        log.append({"Task ID": q["task_id"], "Question": q["question"], "Submitted Answer": answer,
                    "Error": agent.last_error.get(q["task_id"], "")})
    results_df = pd.DataFrame(log)

    # 4. submit
    submission = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
    try:
        r = requests.post(f"{api_url}/submit", json=submission, timeout=60)
        r.raise_for_status()
        d = r.json()
        status = (
            f"Model: {agent_module.MODEL_ID}\n"
            f"Submission successful in {time.time() - start:.0f}s\n"
            f"User: {d.get('username')}\n"
            f"Score: {d.get('score', 'N/A')}% "
            f"({d.get('correct_count', '?')}/{d.get('total_attempted', '?')} correct)\n"
            f"Message: {d.get('message', '')}"
        )
    except requests.exceptions.HTTPError as e:
        status = f"Submission failed: {e.response.status_code} {e.response.text[:500]}"
    except Exception as e:
        status = f"Submission failed: {e}"
    return status, results_df


with gr.Blocks() as demo:
    gr.Markdown("# Agents Course: Unit 4 final assignment")
    gr.Markdown(
        "1. Log in with your Hugging Face account.\n"
        "2. Click **Run Evaluation & Submit All Answers**. Questions run in parallel; "
        "with the cache on, a second run only answers the questions that are still missing."
    )
    gr.LoginButton()
    use_cache = gr.Checkbox(value=True, label="Reuse answers from the previous run")
    run_button = gr.Button("Run Evaluation & Submit All Answers")
    status_output = gr.Textbox(label="Run status / submission result", lines=6, interactive=False)
    results_table = gr.DataFrame(label="Questions and agent answers", wrap=True)
    run_button.click(fn=run_and_submit_all, inputs=[use_cache], outputs=[status_output, results_table])

if __name__ == "__main__":
    demo.launch(debug=True, share=False)