# Intro to Agentic AI - Homework: Researcher Agent (task + solution). Follows on from https://gist.github.com/MightContainNuts/c0b66966e5e8982683bb226c740a4505

> Source: <https://gist.github.com/MightContainNuts/21ec3f1701a01d0cf153444eb07132c3>
> Published: 2026-09-17 14:23:01+00:00

|  | """ | 
|  | 02 - Homework SOLUTION: A simple Researcher Agent. | 
|  |  | 
|  | Instructor reference solution for "Praxis: Wir bauen einen einfachen | 
|  | Researcher Agent" - do not share until students have attempted the | 
|  | exercise themselves. | 
|  |  | 
|  | GOAL | 
|  | ----- | 
|  | Build an agent that can answer a question by searching the web: | 
|  |  | 
|  | 1. The user asks a question. | 
|  | 2. The agent (LLM) decides whether a web search is needed. | 
|  | 3. If so, it calls a search tool. | 
|  | 4. It receives the search results and reasons over them. | 
|  | 5. It summarizes the information into a final answer. | 
|  |  | 
|  | This follows the exact same five-stage pattern as 01_intro_to_agents.py. | 
|  | The one new idea here is that the tool now takes an ARGUMENT (the search | 
|  | query), instead of the zero-argument tool used in lesson 1. That means | 
|  | the model's tool request carries arguments as a JSON string, which we | 
|  | must parse before we can use them. | 
|  |  | 
|  | Requirements: | 
|  | pip install openai requests python-dotenv | 
|  |  | 
|  | Put your OpenAI API key in a .env file next to this script: | 
|  | OPENAI_API_KEY=sk-... | 
|  | """ | 
|  |  | 
|  | import json | 
|  |  | 
|  | import requests | 
|  | from dotenv import load_dotenv | 
|  | from openai import OpenAI | 
|  |  | 
|  | load_dotenv() | 
|  |  | 
|  |  | 
|  | # --------------------------------------------------------------------------- | 
|  | # Step 1: The tool - an ordinary Python function | 
|  | # --------------------------------------------------------------------------- | 
|  | # Unlike get_github_status() in lesson 1, this tool takes an argument: | 
|  | # the search query. It uses DuckDuckGo's Instant Answer API, which is | 
|  | # free and requires no API key - the same "no extra setup" spirit as the | 
|  | # GitHub status API. | 
|  | def search_web(query: str) -> str: | 
|  | """Search the web and return a short text answer for `query`.""" | 
|  |  | 
|  | url = "https://api.duckduckgo.com/" | 
|  | params = {"q": query, "format": "json", "no_redirect": 1, "no_html": 1} | 
|  |  | 
|  | try: | 
|  | response = requests.get(url, params=params, timeout=10) | 
|  | response.raise_for_status() | 
|  | data = response.json() | 
|  |  | 
|  | # DuckDuckGo puts its best answer in different fields depending | 
|  | # on the query. We check them in order of usefulness. | 
|  | if data.get("AbstractText"): | 
|  | return data["AbstractText"] | 
|  |  | 
|  | if data.get("Answer"): | 
|  | return data["Answer"] | 
|  |  | 
|  | related_topics = data.get("RelatedTopics") or [] | 
|  | if related_topics and isinstance(related_topics[0], dict): | 
|  | text = related_topics[0].get("Text") | 
|  | if text: | 
|  | return text | 
|  |  | 
|  | return "No search result was found for this query." | 
|  |  | 
|  | except (requests.RequestException, ValueError): | 
|  | return "The web search could not be completed." | 
|  |  | 
|  |  | 
|  | # --------------------------------------------------------------------------- | 
|  | # Step 2: The tool schema - describing the tool to the model | 
|  | # --------------------------------------------------------------------------- | 
|  | # This time the schema declares a required "query" parameter. "strict": | 
|  | # True plus "required": ["query"] tells the model it must always supply | 
|  | # a query string when it calls this tool. | 
|  | TOOLS = [ | 
|  | { | 
|  | "type": "function", | 
|  | "name": "search_web", | 
|  | "description": ( | 
|  | "Search the web for current or factual information. " | 
|  | "Use this when answering the question requires information " | 
|  | "the model may not already know or that could have changed." | 
|  | ), | 
|  | "parameters": { | 
|  | "type": "object", | 
|  | "properties": { | 
|  | "query": { | 
|  | "type": "string", | 
|  | "description": "The search query to look up on the web.", | 
|  | } | 
|  | }, | 
|  | "required": ["query"], | 
|  | "additionalProperties": False, | 
|  | }, | 
|  | "strict": True, | 
|  | } | 
|  | ] | 
|  |  | 
|  |  | 
|  | def main() -> None: | 
|  | client = OpenAI() | 
|  |  | 
|  | question = "Wer hat die Fußball-Weltmeisterschaft 2022 gewonnen?" | 
|  |  | 
|  | # ----------------------------------------------------------------- | 
|  | # Step 3: Ask the model to choose an action | 
|  | # ----------------------------------------------------------------- | 
|  | response = client.responses.create( | 
|  | model="gpt-5-mini", | 
|  | instructions=( | 
|  | "Answer briefly and accurately. " | 
|  | "Use the available tool when the question needs information " | 
|  | "you might not know or that could be outdated." | 
|  | ), | 
|  | input=question, | 
|  | tools=TOOLS, | 
|  | ) | 
|  |  | 
|  | # ----------------------------------------------------------------- | 
|  | # Step 4: Find the model's tool request (if any) | 
|  | # ----------------------------------------------------------------- | 
|  | tool_call = None | 
|  | for item in response.output: | 
|  | if item.type == "function_call": | 
|  | tool_call = item | 
|  | break | 
|  |  | 
|  | if tool_call is None: | 
|  | # The model judged that no search was needed - e.g. for a | 
|  | # question it can already answer reliably on its own. | 
|  | print("The model did not select a tool.") | 
|  | print(f"Final answer: {response.output_text}") | 
|  | return | 
|  |  | 
|  | # ----------------------------------------------------------------- | 
|  | # Step 5: Validate the tool and parse its arguments | 
|  | # ----------------------------------------------------------------- | 
|  | # Never execute a tool name just because the model returned it - | 
|  | # only run tools we explicitly recognize and approve. | 
|  | if tool_call.name != "search_web": | 
|  | print(f"Tool not permitted: {tool_call.name}") | 
|  | return | 
|  |  | 
|  | # The model sends arguments as a JSON string, not a dict - it must | 
|  | # be parsed before we can read individual fields like "query". | 
|  | arguments = json.loads(tool_call.arguments) | 
|  | query = arguments["query"] | 
|  |  | 
|  | print(f"Selected tool: {tool_call.name}") | 
|  | print(f"Search query: {query}") | 
|  |  | 
|  | # ----------------------------------------------------------------- | 
|  | # Step 6: Execute the tool | 
|  | # ----------------------------------------------------------------- | 
|  | tool_result = search_web(query) | 
|  | print(f"Tool result: {tool_result}") | 
|  |  | 
|  | # ----------------------------------------------------------------- | 
|  | # Step 7: Return the result to the model for a final answer | 
|  | # ----------------------------------------------------------------- | 
|  | final_response = client.responses.create( | 
|  | model="gpt-5-mini", | 
|  | previous_response_id=response.id, | 
|  | instructions=( | 
|  | "Answer the original question briefly. " | 
|  | "Use only the result returned by the tool." | 
|  | ), | 
|  | tools=TOOLS, | 
|  | input=[ | 
|  | { | 
|  | "type": "function_call_output", | 
|  | "call_id": tool_call.call_id, | 
|  | "output": tool_result, | 
|  | } | 
|  | ], | 
|  | ) | 
|  |  | 
|  | print(f"Final answer: {final_response.output_text}") | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main() |
