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