cd /news/ai-agents/intro-to-agentic-ai-homework-researc… · home › topics › ai-agents › article
[ARTICLE · art-145347] src=gist.github.com ↗ pub= topic=ai-agents verified=true sentiment=· neutral

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.

by read7 min views1 publishedSep 17, 2026

| | """ | | | 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() |
── more in #ai-agents 4 stories · sorted by recency
── more on @openai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/intro-to-agentic-ai-…] indexed:0 read:7min 2026-09-17 · —