| | """ |
| | 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() |