File size: 19,864 Bytes
10e9b7d
 
eccf8e4
7d65c66
3c4371f
3a03273
697d647
772d3fb
a066788
772d3fb
1f5cba5
5f605c3
1f5cba5
 
04bd45b
1f5cba5
e2bc6df
9fb6d05
1f5cba5
e80aab9
3db6293
e80aab9
a59a680
a03e926
bf7e7ca
697d647
3a03273
0fed708
 
833b3b5
cd98238
1bc8bac
 
 
 
 
 
b8a605f
0fed708
cd98238
b8a605f
7dbc634
b8a605f
 
 
 
 
 
a59a680
b8a605f
cd98238
 
0fed708
cd98238
b8a605f
5166389
cd98238
 
b8a605f
7dbc634
 
 
b8a605f
 
04bd45b
 
b8a605f
 
0e29657
b8a605f
7dbc634
0e29657
04bd45b
0e29657
 
 
 
 
7fb0070
a59a680
0e29657
 
 
 
1bc8bac
7dbc634
1bc8bac
b8a605f
7dbc634
 
b8a605f
7dbc634
b8a605f
0e29657
 
0fed708
 
0e29657
65abbbc
 
0e29657
51b14d9
8e1dd81
 
 
 
 
0fed708
51b14d9
8e1dd81
 
 
 
 
 
 
51b14d9
 
 
a59a680
 
b8a605f
51b14d9
b8a605f
51b14d9
 
 
 
 
 
 
3563dd6
 
b8a605f
 
51b14d9
958a56b
8e1dd81
e2f7458
51b14d9
958a56b
51b14d9
 
 
 
 
 
958a56b
51b14d9
0e29657
0fed708
eba3a48
ae24cc7
 
 
 
 
 
 
e339dd2
 
 
 
 
ae24cc7
7dbc634
ae24cc7
 
 
 
7dbc634
ae24cc7
 
 
 
 
7dbc634
ae24cc7
 
 
 
7dbc634
ae24cc7
 
 
e339dd2
a59a680
ae24cc7
7dbc634
ae24cc7
 
0e29657
d714e29
 
 
 
 
 
 
 
 
 
 
0fed708
0e29657
 
0fed708
0e29657
d714e29
0e29657
d714e29
0e29657
 
cf84beb
0e29657
 
0fed708
9a37625
b8a605f
7dbc634
b8a605f
66102de
 
 
 
b8a605f
a59a680
66102de
7dbc634
0e29657
7dbc634
0e29657
 
b8a605f
0e29657
 
 
d714e29
0e29657
 
d714e29
 
 
 
 
0e29657
d714e29
 
0e29657
d714e29
0e29657
 
99b84e4
d849921
0fed708
 
5166389
65abbbc
0fed708
 
65abbbc
4288fbd
8e1dd81
1acc5fc
894c1ff
1acc5fc
 
 
 
 
 
 
 
 
4288fbd
1acc5fc
4288fbd
 
5166389
4288fbd
0e29657
 
0fed708
 
 
31243f4
 
 
5166389
c7a6db7
 
 
2f80942
 
 
 
 
 
a89d475
 
5166389
c7a6db7
4021bf3
1f5cba5
 
 
 
 
b90251f
31243f4
 
 
 
7d65c66
b177367
3c4371f
7e4a06b
1ca9f65
3c4371f
7e4a06b
3c4371f
7d65c66
3c4371f
7e4a06b
31243f4
 
e80aab9
b177367
31243f4
 
 
3c4371f
31243f4
b177367
36ed51a
c1fd3d2
3c4371f
7d65c66
31243f4
eccf8e4
31243f4
7d65c66
31243f4
 
3c4371f
 
31243f4
e80aab9
31243f4
 
3c4371f
 
7d65c66
3c4371f
7d65c66
31243f4
 
e80aab9
b177367
0c482eb
7d65c66
 
3c4371f
31243f4
 
 
 
 
 
 
5166389
7d65c66
 
31243f4
 
7d65c66
31243f4
 
3c4371f
31243f4
 
b177367
7d65c66
3c4371f
31243f4
e80aab9
7d65c66
31243f4
e80aab9
7d65c66
e80aab9
 
31243f4
e80aab9
 
3c4371f
 
 
e80aab9
 
31243f4
 
e80aab9
3c4371f
e80aab9
 
3c4371f
e80aab9
7d65c66
3c4371f
31243f4
7d65c66
31243f4
3c4371f
 
 
 
 
e80aab9
31243f4
 
 
 
7d65c66
31243f4
 
 
 
e80aab9
 
 
 
31243f4
0ee0419
e514fd7
 
 
81917a3
e514fd7
 
 
 
 
 
 
 
e80aab9
 
7e4a06b
e80aab9
31243f4
e80aab9
9088b99
7d65c66
 
e80aab9
31243f4
 
 
e80aab9
 
 
77f790c
3c4371f
7d65c66
3c4371f
7d65c66
3a178ff
3f7f23e
3c4371f
 
7d65c66
3c4371f
7d65c66
 
 
 
 
 
 
 
 
3c4371f
 
31243f4
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
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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
import os
import gradio as gr
import requests
import inspect
import pandas as pd
from langgraph.prebuilt import ToolNode


# from typing import Any, Dict
# from typing import TypedDict, Annotated

from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain.schema import HumanMessage, SystemMessage, AIMessage
# Create a ToolNode that knows about your web_search function
import json
from state import AgentState

# --- Constants ---
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"

from tools import ocr_image_tool, parse_excel_tool, web_search_tool, run_tools, audio_transcriber_tool, wikipedia_search_tool

llm = ChatOpenAI(model_name="gpt-4.1-mini", temperature=0.0)

# agent = create_react_agent(model=llm, tools=tool_node)

# ─── 2) Revised plan_node ───
def plan_node(state: AgentState) -> AgentState:
    prior_msgs = state.get("messages", [])
    user_input = ""
    for msg in reversed(prior_msgs):
        if isinstance(msg, HumanMessage):
            user_input = msg.content
            break

    # (1) Build your system/human messages exactly as before
    system_msg = SystemMessage(
        content=(
            "You are an agent that decides whether to call a tool or answer directly.\n"
            "User's question: \"" + user_input + "\"\n\n"
            "• If you can answer directly, return exactly {\"final_answer\":\"<your answer>\"}.\n"
            "• Otherwise, respond with exactly one of:\n"
            "    {\"web_search_query\":\"<search terms>\"}\n"
            "    {\"ocr_path\":\"<path to image>\"}\n"
            "    {\"excel_path\":\"<path to xlsx>\", \"excel_sheet_name\":\"<sheet>\"}\n"
            "    {\"audio_path\":\"<path to audio file>\"}\n"
            "    {\"wiki_query\":\"<wikipedia search terms>\"}\n"
            "Do not include any extra characters or markdown—only the JSON literal."
        )
    )
    human_msg = HumanMessage(content=user_input)

    # (2) Call the LLM
    llm_response = llm.invoke([system_msg, human_msg])
    llm_out = llm_response.content.strip()

    # ── DEBUG: print raw LLM output ──
    # print("\n>>> plan_node got raw LLM output:")
    # print(llm_out)
    # print("<<< end raw output\n")

    # (3) Append the LLM output to the message history
    ai_msg = AIMessage(content=llm_out)
    new_msgs = prior_msgs.copy() + [ai_msg]

    # (4) Try parsing as JSON
    try:
        parsed = json.loads(llm_out)
        # print(">>> plan_node parsed JSON:", parsed)
        if isinstance(parsed, dict):
            partial: AgentState = {"messages": new_msgs}
            allowed = {
                "web_search_query",
                "ocr_path",
                "excel_path",
                "excel_sheet_name",
                "audio_path",
                "wiki_query",
                "final_answer"
            }
            for k, v in parsed.items():
                if k in allowed:
                    partial[k] = v
                    # print(f">>> plan_node is setting {k!r} → {v!r}")
            return partial
    except json.JSONDecodeError as e:
        # print(">>> plan_node JSON parse error:", e)
        pass
    # (5) Fallback
    # print(">>> plan_node falling back to final_answer alone\n")
    return {"messages": new_msgs, "final_answer": "Sorry, I could not parse your intent."}



# ─── 3) Revised finalize_node ───
def finalize_node(state: AgentState) -> AgentState:
    if state.get("final_answer") is not None:
        return {"final_answer": state["final_answer"]}

    # Re‐extract the last user question
    question = ""
    for msg in reversed(state.get("messages", [])):
        if isinstance(msg, HumanMessage):
            question = msg.content
            break

    # Build one monolithic context
    combined = f"USER_QUESTION: {question}\n"
    if sr := state.get("web_search_result"):
        combined += f"WEB_SEARCH_RESULT: {sr}\n"
    if orc := state.get("ocr_result"):
        combined += f"OCR_RESULT: {orc}\n"
    if exr := state.get("excel_result"):
        combined += f"EXCEL_RESULT: {exr}\n"
    # Note: your code already stores the audio transcription under "transcript"
    if tr := state.get("transcript"):
        combined += f"AUDIO_TRANSCRIPT: {tr}\n"
    if wr := state.get("wiki_result"):
        combined += f"WIKIPEDIA_RESULT: {wr}\n"

    # Here we demand a JSON response with a single key "final_answer"
    combined += (
        "Based on the above, respond with exactly one JSON object, and nothing else. "
        "The JSON object must have exactly one key: \"final_answer\". "
        "For example:\n"
        "{\"final_answer\":\"42\"}\n"
        "Do NOT include any explanation, markdown, or any extra whitespace outside the JSON object. "
        "If the answer is multiple words, put them in a comma-separated string, e.g. \"red,green,blue\". "
        "If the answer is a number, it must be digits only—e.g. \"725.00\".\n"
        "If the answer is a list of items, put them in a comma-separated string, e.g. \"item1,item2,item3\". "
        "If the user prompt asks you to do something, then do it "
    )

    # Debug print
    # print("\n>>> finalize_node JSON‐strict prompt:\n" + combined + "\n<<< end prompt >>>\n")

    llm_response = llm.invoke([SystemMessage(content=combined)])
    raw = llm_response.content.strip()
    # print(">>> finalize_node got raw response:", raw)

    try:
        parsed = json.loads(raw)
        return {"final_answer": parsed["final_answer"]}
    except Exception as e:
        # If the LLM did not return valid JSON, store the error so you can see it
        # print(">>> finalize_node JSON parse error:", e, "raw was:", raw)
        return {"final_answer": f"ERROR: invalid JSON from finalize_node: {raw}"}

# ─── 4) Wrap tools in a ToolNode ───
def tool_node(state: AgentState) -> AgentState:
    """
    Inspect exactly which tool‐key was set in `state` and call that function.
    Returns only the partial state (with the tool's outputs) so that merge_tool_output can combine it.
    """
    # We expect exactly one of these keys to be non‐empty:
    #   "web_search_query", "ocr_path", "excel_path"/"excel_sheet_name", "audio_path"
    # Whichever is present, call the corresponding tool and return its result.
    
    if state.get("wiki_query"):
        out = wikipedia_search_tool(state)
        return out
    
    if state.get("web_search_query"):
        # print(f">>> tools_node dispatching web_search_tool with query: {state['web_search_query']!r}")
        out = web_search_tool(state)
        return out

    if state.get("ocr_path"):
        # print(f">>> tools_node dispatching ocr_image_tool with path: {state['ocr_path']!r}")
        out = ocr_image_tool(state)
        return out

    if state.get("excel_path"):
        # We assume plan_node always sets both excel_path and excel_sheet_name together
        # print(f">>> tools_node dispatching parse_excel_tool with path: {state['excel_path']!r}, sheet: {state.get('excel_sheet_name')!r}")
        out = parse_excel_tool(state)
        return out

    if state.get("audio_path"):
        # print(f">>> tools_node dispatching audio_transcriber_tool with path: {state['audio_path']!r}")
        out = audio_transcriber_tool(state)
        return out



    # If we somehow reach here, no recognized tool key was set:
    # print(">>> tools_node: no valid tool key found in state!")
    return {}


# Add a node to store the previous state

def store_prev_state(state: AgentState) -> AgentState:
    return {**state, "prev_state": state.copy()}

def merge_tool_output(state: AgentState) -> AgentState:
    prev_state = state.get("prev_state", {})
    merged = {**prev_state, **state}
    merged.pop("prev_state", None)
    return merged

# ─── 5) Build the graph ───
graph = StateGraph(AgentState)

# 5.a) Register nodes
graph.add_node("plan", plan_node)
graph.add_node("store_prev_state", store_prev_state)
graph.add_node("tools", tool_node)
graph.add_node("merge_tool_output", merge_tool_output)
graph.add_node("finalize", finalize_node)

# 5.b) Wire START → plan
graph.add_edge(START, "plan")

# 5.c) plan → conditional: if any tool key was set, go to "tools"; otherwise "finalize"
def route_plan(plan_out: AgentState) -> str:
    # print what keys are present in plan_out
    # print(f">> route_plan sees plan_out keys: {list(plan_out.keys())}")

    if (
        plan_out.get("web_search_query")
        or plan_out.get("ocr_path")
        or plan_out.get("excel_path")
        or plan_out.get("audio_path")
        or plan_out.get("wiki_query")
    ):
        # print(">> route_plan ➡️ tools")
        return "tools"
    # print(">> route_plan ➡️ finalize")
    return "finalize"


graph.add_conditional_edges(
    "plan",
    route_plan,
    {"tools": "store_prev_state", "finalize": "finalize"}
)

# 5.d) store_prev_state → tools
graph.add_edge("store_prev_state", "tools")

# 5.e) tools → merge_tool_output
graph.add_edge("tools", "merge_tool_output")

# 5.f) merge_tool_output → finalize
graph.add_edge("merge_tool_output", "finalize")

# 5.g) finalize → END
graph.add_edge("finalize", END)

compiled_graph = graph.compile()


# ─── 6) respond_to_input ───
def respond_to_input(user_input: str, task_id) -> str:
    """
    Seed state['messages'] with a SystemMessage (tools description) + HumanMessage(user_input).
    Then invoke the graph; return the final_answer from the resulting state.
    """
    system_msg = SystemMessage(
    content=(
        "You are an agent that picks exactly one course of action:\n"
        "Use Wikipedia first before even using web search Please. "
        "  1) If the user's question is best answered by looking up a Wikipedia summary, return\n"
        "     {\"wiki_query\":\"<query for Wikipedia>\"} and nothing else.\n"
        "  2) Otherwise, if Wikipedia won't directly help, but you still need web results, return\n"
        "     {\"web_search_query\":\"<search terms>\"} and nothing else.\n"
        "  3) Otherwise, if it's an image, use OCR: return {\"ocr_path\":\"<path>\"}.\n"
        "  4) Otherwise, if it's a spreadsheet question, return {\"excel_path\":\"<path>\", \"excel_sheet_name\":\"<sheet>\"}.\n"
        "  5) Otherwise, if it's an audio transcription, return {\"audio_path\":\"<path to audio file>\"}.\n"
        "  6) If you can answer immediately without any tool, return exactly {\"final_answer\":\"<your answer>\"}.\n"
        "Do NOT include any other keys, text, or explanation—just one JSON object with exactly one of those keys."
    )
)
    human_msg = HumanMessage(content=user_input)

    initial_state: AgentState = {"messages": [system_msg, human_msg], "task_id": task_id}
    final_state = compiled_graph.invoke(initial_state)
    return final_state.get("final_answer", "Error: No final answer generated.")




class BasicAgent:
    def __init__(self):
        print("BasicAgent initialized.")
    def __call__(self, question: str, task_id) -> str:
        # print(f"Agent received question (first 50 chars): {question[:50]}...")
        # fixed_answer = "This is a default answer."
        # print(f"Agent returning fixed answer: {fixed_answer}")
        print()
        print()
        print()
        print()
        
        
        print(f"Agent received question: {question}")
        print()
        return respond_to_input(question, task_id)
        # return fixed_answer






def run_and_submit_all( profile: gr.OAuthProfile | None):
    """
    Fetches all questions, runs the BasicAgent on them, submits all answers,
    and displays the results.
    """
    # --- Determine HF Space Runtime URL and Repo URL ---
    space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code

    if profile:
        username= f"{profile.username}"
        print(f"User logged in: {username}")
    else:
        print("User not logged in.")
        return "Please Login to Hugging Face with the button.", None

    api_url = DEFAULT_API_URL
    questions_url = f"{api_url}/questions"
    submit_url = f"{api_url}/submit"

    # 1. Instantiate Agent ( modify this part to create your agent)
    try:
        agent = BasicAgent()
    except Exception as e:
        print(f"Error instantiating agent: {e}")
        return f"Error initializing agent: {e}", None
    # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
    agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
    print(agent_code)

    # 2. Fetch Questions
    print(f"Fetching questions from: {questions_url}")
    try:
        response = requests.get(questions_url, timeout=15)
        response.raise_for_status()
        questions_data = response.json()
        if not questions_data:
             print("Fetched questions list is empty.")
             return "Fetched questions list is empty or invalid format.", None
        print(f"Fetched {len(questions_data)} questions.")
    except requests.exceptions.RequestException as e:
        print(f"Error fetching questions: {e}")
        return f"Error fetching questions: {e}", None
    except requests.exceptions.JSONDecodeError as e:
         print(f"Error decoding JSON response from questions endpoint: {e}")
         print(f"Response text: {response.text[:500]}")
         return f"Error decoding server response for questions: {e}", None
    except Exception as e:
        print(f"An unexpected error occurred fetching questions: {e}")
        return f"An unexpected error occurred fetching questions: {e}", None

    # 3. Run your Agent
    
    results_log = []
    answers_payload = []
    print(f"Running agent on {len(questions_data)} questions...")
    for item in questions_data:
        task_id = item.get("task_id")
        question_text = item.get("question")
        if not task_id or question_text is None:
            print(f"Skipping item with missing task_id or question: {item}")
            continue
        try:
            submitted_answer = agent(question_text, task_id)
            answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
            results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
        except Exception as e:
             print(f"Error running agent on task {task_id}: {e}")
             results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})

    if not answers_payload:
        print("Agent did not produce any answers to submit.")
        return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)

    # 4. Prepare Submission 
    submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
    status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
    print(status_update)

    # 5. Submit
    print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
    try:
        response = requests.post(submit_url, json=submission_data, timeout=60)
        response.raise_for_status()
        result_data = response.json()
        final_status = (
            f"Submission Successful!\n"
            f"User: {result_data.get('username')}\n"
            f"Overall Score: {result_data.get('score', 'N/A')}% "
            f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
            f"Message: {result_data.get('message', 'No message received.')}"
        )
        print("Submission successful.")
        results_df = pd.DataFrame(results_log)
        return final_status, results_df
    except requests.exceptions.HTTPError as e:
        error_detail = f"Server responded with status {e.response.status_code}."
        try:
            error_json = e.response.json()
            error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
        except requests.exceptions.JSONDecodeError:
            error_detail += f" Response: {e.response.text[:500]}"
        status_message = f"Submission Failed: {error_detail}"
        print(status_message)
        results_df = pd.DataFrame(results_log)
        return status_message, results_df
    except requests.exceptions.Timeout:
        status_message = "Submission Failed: The request timed out."
        print(status_message)
        results_df = pd.DataFrame(results_log)
        return status_message, results_df
    except requests.exceptions.RequestException as e:
        status_message = f"Submission Failed: Network error - {e}"
        print(status_message)
        results_df = pd.DataFrame(results_log)
        return status_message, results_df
    except Exception as e:
        status_message = f"An unexpected error occurred during submission: {e}"
        print(status_message)
        results_df = pd.DataFrame(results_log)
        return status_message, results_df


# --- Build Gradio Interface using Blocks ---
with gr.Blocks() as demo:
    gr.Markdown("# Basic Agent Evaluation Runner")
    gr.Markdown(
        """
        **Instructions:**

        1.  Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
        2.  Log in to your Hugging Face account using the button below. This uses your HF username for submission.
        3.  Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.

        ---
        **Disclaimers:**
        Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions).
        This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async.
        """
    )

    gr.LoginButton()

    run_button = gr.Button("Run Evaluation & Submit All Answers")

    status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
    # Removed max_rows=10 from DataFrame constructor
    results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)

    run_button.click(
        fn=run_and_submit_all,
        outputs=[status_output, results_table]
    )

if __name__ == "__main__":
    # print("LangGraph version:", langgraph.__version__) 
    print("\n" + "-"*30 + " App Starting " + "-"*30)
    # Check for SPACE_HOST and SPACE_ID at startup for information
    space_host_startup = os.getenv("SPACE_HOST")
    space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
    # import langgraph
    # print("▶︎ LangGraph version:", langgraph.__version__)
    if space_host_startup:
        print(f"✅ SPACE_HOST found: {space_host_startup}")
        print(f"   Runtime URL should be: https://{space_host_startup}.hf.space")
    else:
        print("ℹ️  SPACE_HOST environment variable not found (running locally?).")

    if space_id_startup: # Print repo URLs if SPACE_ID is found
        print(f"✅ SPACE_ID found: {space_id_startup}")
        print(f"   Repo URL: https://huggingface.co/spaces/{space_id_startup}")
        print(f"   Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
    else:
        print("ℹ️  SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")

    print("-"*(60 + len(" App Starting ")) + "\n")

    print("Launching Gradio Interface for Basic Agent Evaluation...")
    demo.launch(debug=True, share=False)