Intro to Agentic AI - Homework: Researcher Agent (task + solution). Follows on from https://gist.github.com/MightContainNuts/c0b66966e5e8982683bb226c740a4505 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. | | """ | | | 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 |