File size: 8,354 Bytes
7487b95
 
 
 
 
 
 
 
 
 
 
 
1af65e5
3c1a783
 
7487b95
1af65e5
7487b95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c1a783
7487b95
3c1a783
 
7487b95
 
3c1a783
 
7487b95
 
3c1a783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7487b95
3c1a783
 
 
 
 
 
 
7487b95
3c1a783
7487b95
3c1a783
7487b95
 
3c1a783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7487b95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60489dc
 
 
bad64d6
60489dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
from langchain_core.messages import AnyMessage, HumanMessage, AIMessage, SystemMessage
from langgraph.prebuilt import ToolNode
from langgraph.graph import START, StateGraph, END
from langchain_community.tools import DuckDuckGoSearchRun
from langchain_openai import ChatOpenAI
import requests
import base64
import json
import io
import contextlib
import os
import subprocess
import tempfile

OPENAI = os.getenv("OPENAI")

def replace(_, new_value):
    return new_value

search_tool = DuckDuckGoSearchRun()

tools = [search_tool]


llm = ChatOpenAI(
    model="gpt-4o",
    openai_api_key=OPENAI,
)
llm_with_tools = llm.bind_tools(tools)


# Generate the AgentState and Agent graph
class AgentState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    question: Annotated[str, replace]
    task_id: Annotated[str, replace]
    file_name: Annotated[str, replace]

def get_question(state: AgentState):
    print("get_question")
    question_dict = json.loads(state['messages'][-1].content)
    print(question_dict)
    state['question'] = question_dict["question"]
    state['task_id'] = question_dict["task_id"]
    state['file_name'] = question_dict["file_name"]
    return state


def reformulate_question(state: AgentState):
    print("reformulate_question")
    sys_msg = SystemMessage(content=f"You are a detective and you have to answer a hard question. The first step is to understand the question. Can you reformulate it to make it clear and concise ? The question is : {state['question']}")
    
    state['messages'] = state['messages'] + [llm_with_tools.invoke([sys_msg])]
    return state

def assistant(state: AgentState):
    print("assistant")
    # System message
    sys_msg="""
You are a helpful assistant that can answer questions about the world. You can use the internet through the search tool to find information.
"""
    sys_msg = SystemMessage(content=sys_msg)
    state['messages'] = state['messages'] + [llm_with_tools.invoke([sys_msg] + state["messages"])]
    return state

def get_final_answer(state: AgentState):
    print("get_final_answer")
    sys_msg = SystemMessage(content=f"""
Reply the answer and only the answer of the question. Do not make a sentence, just the answer. If the answer is a number, return it in numeric form.
If the question specifies a format, please return it in the specified format. Do not add unnecessary uppercasing or punctuation. Here is the question again: {state['question']} and here is the answer: {state['messages'][-1].content}
""")
    final_answer = state["messages"][-1].content  # The last assistant message
    print(f"final answer: {final_answer}")
    state['messages'] = state['messages'] + [llm.invoke([sys_msg])]
    return state

    
def python_interpreter(state: AgentState):
    print("python interpreter")

    question_id = state['task_id']
    url = f"https://agents-course-unit4-scoring.hf.space/files/{question_id}"
    
    # Step 1: Download the code
    response = requests.get(url)
    python_code = response.content.decode("utf-8")

    print("Running user code with subprocess...")

    # Step 2: Write code to temp file
    with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as tmp_file:
        tmp_file.write(python_code)
        tmp_file_path = tmp_file.name

    try:
        # Step 3: Run using subprocess (set timeout if needed)
        result = subprocess.run(
            ["python3", tmp_file_path],
            capture_output=True,
            text=True,
            timeout=30  # seconds
        )
        if result.returncode != 0:
            output = f"⚠️ Error:\n{result.stderr.strip()}"
        else:
            output = result.stdout.strip() or "Code ran but produced no output."
    except subprocess.TimeoutExpired:
        output = "Execution timed out."
    except Exception as e:
        output = f"Unexpected error: {e}"

    # Step 4: Update agent state
    state['messages'] = state['messages'] + [
        AIMessage(content=f"The output of the Python code is:\n```\n{output}\n```")
    ]
    
    return state



# def python_interpreter(state: AgentState):
#     question_id = state['task_id']
#     url = f"https://agents-course-unit4-scoring.hf.space/files/{question_id}"

#     # Step 1: Fetch Python code as bytes and decode
#     response = requests.get(url)
#     python_code = response.content.decode("utf-8")
#     # print(python_code)

#     # Step 2: Capture stdout
#     stdout = io.StringIO()
#     with contextlib.redirect_stdout(stdout):
#         try:
#             exec(python_code)  # Use empty dict for isolated global context
#         except Exception as e:
#             output = f"Error during execution: {e}"
#         else:
#             output = stdout.getvalue().strip()
#     print("Code output : ", output)
#     # Step 3: Save output as message
#     state['messages'] = state['messages'] + [
#         AIMessage(content=f"The output of the python code is : {output}")
#     ]
#     return state

def image_interpreter(state: AgentState):
    print("image interpreter")
    """Answers a question about an image. Provide raw image bytes and a question."""
    question_id = state['task_id']
    url = f"https://agents-course-unit4-scoring.hf.space/files/{question_id}"
    image = requests.get(url).content
    image_base64 = base64.b64encode(image).decode("utf-8")

    messages=[
        {"role": "system", "content": "You are a helpful assistant that can answer questions based on images."},
        {
            "role": "human",
            "content": [
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}},
                {"type": "text", "text": state['question']}
            ],
        },
    ]
    resp = llm.invoke(messages)
    print("Image response : ", resp)
    state['messages'] = state['messages'] + [resp]

    return state

def not_handled(state: AgentState):
    print("not_handled")
    state['messages'] = state['messages'] + [AIMessage(content="I cannot handle this question.")]
    return state


def tools_condition(state: AgentState) -> str:
    print("tools_condition")
    last_msg = state["messages"][-1]

    if "tool_calls" in last_msg.additional_kwargs:
        # LLM is requesting a tool
        return "tools"

    # No tool requested → go to final summary
    return "get_final_answer"

def files_condition(state: AgentState) -> str:
    print("files_condition")
    if state["file_name"].endswith(".py"):
        return "python_interpreter"
    elif state["file_name"].endswith(".png"):
        return "image_interpreter"
    elif state["file_name"]=="":
        return "reformulate_question"
    else:
        return "not_handled"


def build_agent():
    ## The graph
    builder = StateGraph(AgentState)
    
    # Define nodes: these do the work
    builder.add_node("get_question", get_question)
    builder.add_node("reformulate_question", reformulate_question)
    builder.add_node("assistant", assistant)
    builder.add_node("tools", ToolNode(tools))
    builder.add_node("get_final_answer", get_final_answer)
    builder.add_node("python_interpreter", python_interpreter)
    builder.add_node("image_interpreter", image_interpreter)
    builder.add_node("not_handled", not_handled)
    
    # Define edges: these determine how the control flow moves
    builder.add_edge(START, "get_question")
    builder.add_conditional_edges(
        "get_question",
        files_condition,
        {
            "python_interpreter": "python_interpreter",
            "image_interpreter": "image_interpreter",
            "reformulate_question": "reformulate_question",
            "not_handled": "not_handled"
        }
    )
    builder.add_edge("python_interpreter", "get_final_answer")
    builder.add_edge("image_interpreter", "get_final_answer")
    builder.add_edge("reformulate_question", "assistant")
    builder.add_conditional_edges(
        "assistant",
        tools_condition,
        {
            "tools": "tools",
            "get_final_answer": "get_final_answer"
        }
    )
    builder.add_edge("tools", "assistant")
    builder.add_edge("get_final_answer", END)
    builder.add_edge("not_handled", END)
    mySuperAgent = builder.compile()
    return mySuperAgent