> ## Documentation Index
> Fetch the complete documentation index at: https://docs.seekr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Weather agent with a custom Python tool

> Build a SeekrFlow agent that calls your own Python function as a tool, where the function fetches live data from an external API.

export const DownloadNotebook = ({href, filename}) => <div className="not-prose" style={{
  marginTop: "4px",
  marginBottom: "16px"
}}>
    <a href={href} download={filename} className="inline-flex items-center gap-2 border border-[#00dad3] bg-[#00dad3]/10 text-[#007774] dark:text-[#00dad3] no-underline" style={{
  padding: "6px 14px",
  borderRadius: "8px",
  fontSize: "14px",
  fontWeight: "600"
}}>
      <Icon icon="download" size={16} />
      Download Jupyter notebook
    </a>
  </div>;

export const RecipeMeta = ({tags = [], level, time, version}) => {
  const pill = {
    display: "inline-block",
    padding: "2px 10px",
    borderRadius: "6px",
    fontSize: "12px",
    fontWeight: "600",
    marginRight: "6px",
    marginBottom: "6px"
  };
  const hasMeta = level || time || version;
  return <div className="not-prose" style={{
    marginTop: "4px",
    marginBottom: "12px"
  }}>
      {tags.length > 0 && <div style={{
    marginBottom: hasMeta ? "8px" : "0"
  }}>
          {tags.map(t => <span key={t} className="border border-[#00dad3] bg-[#00dad3]/10 text-[#007774] dark:text-[#00dad3]" style={pill}>
              {t}
            </span>)}
        </div>}
      {hasMeta && <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-black/60 dark:text-white/60">
          {level && <span className="inline-flex items-center gap-1.5">
              <Icon icon="gauge" size={14} />
              {level}
            </span>}
          {time && <span className="inline-flex items-center gap-1.5">
              <Icon icon="clock" size={14} />
              {time}
            </span>}
          {version && <span className="inline-flex items-center gap-1.5">
              <Icon icon="cube" size={14} />
              Verified with seekrai {version}
            </span>}
        </div>}
    </div>;
};

<RecipeMeta tags={["Agents", "Function calling", "External API"]} level="Beginner" time="~15 min" version="0.29.0" />

<DownloadNotebook href="/notebooks/weather-agent-custom-python-tool.json" filename="weather-agent-custom-python-tool.ipynb" />

This recipe shows the smallest useful pattern for extending an agent with your
own code: you write a plain Python function, register it as a custom function,
wrap it in a `run_python` tool, and attach that tool to an agent. When the agent
decides it needs live data, SeekrFlow runs your function and feeds the result
back into the conversation.

The example function calls a public weather API ([wttr.in](https://wttr.in)) and
returns the forecast for a single ZIP code, so you can see the full round trip
from agent to your code to a third-party service and back.

## What you'll build

An agent that:

1. Exposes a custom Python function as a `run_python` tool.
2. Calls the tool whenever the user asks about the weather.
3. Runs your function, which requests live data from an external API.
4. Reports back exactly what the API returned.

## Prerequisites

* A SeekrFlow API key, set as the `SEEKR_API_KEY` environment variable
* Python 3.8 or later
* The SeekrFlow SDK: `pip install seekrai`

<Warning>
  This recipe creates billable resources. When you are done, remove them with the
  [cleanup step](#clean-up).
</Warning>

## Build it

<Steps>
  <Step title="Write the weather function">
    Save the following as `weather_fn.py`. This is an ordinary Python function with
    no SeekrFlow dependencies. The docstring matters: SeekrFlow uses it to tell the
    agent what the tool does and when to call it, so write it the way you would
    write a tool description.

    <CodeGroup>
      ```python weather_fn.py theme={null}
      import urllib.request
      import urllib.error


      def get_current_weather() -> str:
          """Get the current weather for ZIP code 22207 (Arlington, VA).

          Fetches live weather data from a public API. Always call this tool
          when the user asks about the weather or wants a weather report.

          Returns:
              str: Raw weather data from the API, or an error message.
          """
          url = "https://wttr.in/22207?0"
          try:
              req = urllib.request.Request(url, headers={"User-Agent": "curl/7.64.1"})
              with urllib.request.urlopen(req, timeout=10) as resp:
                  status = resp.status
                  body = resp.read().decode()
              return f"[HTTP {status}] {body}"
          except urllib.error.HTTPError as e:
              return f"[HTTPError {e.code}] {e.reason}: {e.read().decode()}"
          except urllib.error.URLError as e:
              return f"[URLError] {e.reason}"
          except Exception as e:
              return f"[Error] {type(e).__name__}: {e}"
      ```
    </CodeGroup>

    <Note>
      The function catches its own errors and returns them as a string instead of
      raising. That way a failed API call comes back to the agent as readable text it
      can relay, rather than crashing the run.
    </Note>
  </Step>

  <Step title="Set up the client">
    Create a second file, `weather_agent.py`, for the agent itself. Start with the
    imports, configuration, and client. Keep `weather_fn.py` in the same directory,
    since you will upload it by path in a later step.

    <CodeGroup>
      ```python weather_agent.py theme={null}
      import os
      import time

      from seekrai import SeekrFlow
      from seekrai.types.tools import CreateRunPython, RunPythonConfig
      from seekrai.types.agents.agent import CreateAgentRequest

      API_KEY = os.environ["SEEKR_API_KEY"]
      API_URL = "https://flow.seekr.com/v1/"
      MODEL_ID = "meta-llama/Llama-3.1-70B-Instruct"
      FUNCTION_FILE = "weather_fn.py"
      AGENT_NAME = "Weather Agent"
      TOOL_NAME = "weather_tool"
      FUNCTION_NAME = "get_current_weather"

      client = SeekrFlow(api_key=API_KEY, base_url=API_URL)
      ```
    </CodeGroup>

    <Info>
      This recipe uses `meta-llama/Llama-3.1-70B-Instruct`. A larger instruct model
      follows the "always call the tool" instruction reliably and handles the
      tool-calling handshake well. Any instruct model that supports tool calling
      works here, so you can substitute a smaller or newer model such as
      `meta-llama/Llama-3.3-70B-Instruct` to trade some quality for speed and cost.
    </Info>
  </Step>

  <Step title="Add two helper functions">
    These helpers wait for asynchronous operations to finish: one polls a run until
    it completes, and one polls the agent until it is `Active` after promotion. Add
    them to `weather_agent.py`.

    <CodeGroup>
      ```python weather_agent.py theme={null}
      def run_agent(agent_id: str, prompt: str, timeout: int = 300) -> str:
          thread = client.agents.threads.create()
          client.agents.threads.create_message(thread_id=thread.id, role="user", content=prompt)
          run_response = client.agents.runs.run(agent_id=agent_id, thread_id=thread.id, stream=False)

          deadline = time.time() + timeout
          while time.time() < deadline:
              run = client.agents.runs.retrieve(run_response.run_id, thread.id)
              if run.status.value == "completed":
                  break
              if run.status.value in ("failed", "canceled"):
                  raise RuntimeError(f"Run ended with status: {run.status.value}")
              time.sleep(3)
          else:
              raise TimeoutError(f"Run did not complete within {timeout}s.")

          messages = client.agents.threads.list_messages(thread.id, limit=10, order="desc")
          for msg in messages:
              if msg.role == "assistant":
                  return msg.content if isinstance(msg.content, str) else str(msg.content)
          return "(no assistant message found)"


      def promote_and_wait(agent_id: str, timeout: int = 120) -> None:
          client.agents.promote(agent_id)
          deadline = time.time() + timeout
          while time.time() < deadline:
              agent = client.agents.retrieve(agent_id)
              if agent.status.value == "Active":
                  return
              if agent.status.value == "Failed":
                  raise RuntimeError(f"Agent {agent_id} failed to deploy.")
              time.sleep(3)
          raise TimeoutError(f"Agent did not become Active within {timeout}s.")
      ```
    </CodeGroup>
  </Step>

  <Step title="Register the custom function">
    Upload `weather_fn.py` so SeekrFlow can run it. The returned function ID
    connects your code to the tool in the next step.

    <CodeGroup>
      ```python weather_agent.py theme={null}
      fn = client.agents.custom_functions.create(file_path=FUNCTION_FILE)
      print(f"Function registered: {fn.id!r}")
      ```
    </CodeGroup>
  </Step>

  <Step title="Create the run_python tool">
    Wrap the registered function in a `run_python` tool. The `description` is what
    the agent sees when it decides whether to call the tool, so make it specific.

    <CodeGroup>
      ```python weather_agent.py theme={null}
      tool = client.tools.create(
          CreateRunPython(
              name=TOOL_NAME,
              description="Fetches the current weather for ZIP code 22207 (Arlington, VA) from a live API.",
              config=RunPythonConfig(function_ids=[fn.id]),
          )
      )
      print(f"Tool created: {tool.id!r}")
      ```
    </CodeGroup>
  </Step>

  <Step title="Create the agent">
    Create the agent, attach the tool by ID, and give it instructions that tell it
    to always call the tool for weather questions.

    <CodeGroup>
      ```python weather_agent.py theme={null}
      agent = client.agents.create(
          CreateAgentRequest(
              name=AGENT_NAME,
              instructions=(
                  "You are a weather assistant. Whenever the user asks about the weather, "
                  "you MUST call the 'weather_tool' first and then report exactly what it returned."
              ),
              model_id=MODEL_ID,
              tool_ids=[tool.id],
          )
      )
      print(f"Agent created: {agent.id!r}")
      ```
    </CodeGroup>
  </Step>

  <Step title="Promote the agent">
    Promote the agent to deploy it, then wait for it to become `Active`.

    <CodeGroup>
      ```python weather_agent.py theme={null}
      promote_and_wait(agent.id)
      print(f"'{AGENT_NAME}' is Active.")
      ```
    </CodeGroup>
  </Step>

  <Step title="Send a prompt">
    Send a prompt and print the agent's reply. Because the instructions require it,
    the agent calls your function, which requests live data from wttr.in, and then
    reports the result.

    <CodeGroup>
      ```python weather_agent.py theme={null}
      response = run_agent(agent.id, "What's the weather like right now?")
      print("\nAgent says:\n")
      print(response)
      ```
    </CodeGroup>
  </Step>

  <Step title="Run the script">
    Run the finished script:

    ```bash theme={null}
    python weather_agent.py
    ```

    You should see the agent report the current Arlington, VA forecast, sourced live
    from the weather API through your function.
  </Step>
</Steps>

<h2 id="clean-up">
  Clean up resources (optional)
</h2>

Promoted agents and their tools stay in your account until you remove them. When
you are done experimenting, tear down the resources this recipe created. This
deletes every agent, tool, and function that matches the names set at the top of
`weather_agent.py`, so run it only against a scratch account.

<CodeGroup>
  ```python weather_agent.py theme={null}
  for a in [a for a in client.agents.list_agents() if a.name == AGENT_NAME]:
      try:
          client.agents.demote(a.id)
      except Exception:
          pass
      client.agents.delete(a.id)
      print(f"Deleted agent: {a.id!r}")

  for t in [t for t in client.tools.list().data if t.name == TOOL_NAME]:
      client.tools.delete(t.id)
      print(f"Deleted tool: {t.id!r}")

  for f in [f for f in client.agents.custom_functions.list_functions() if f.name == FUNCTION_NAME]:
      client.agents.custom_functions.delete(f.id)
      print(f"Deleted function: {f.id!r}")
  ```
</CodeGroup>

## Next steps

* **Accept parameters.** The function takes no arguments, so it always reports
  the same ZIP code. Add a `zip_code: str` parameter and describe it in the
  docstring so the agent can supply the location from the user's question.
* **Call your own API.** Swap the wttr.in request for a call to an internal
  service to give the agent access to your own live data.
* **Add more tools.** Attach several functions to one agent and let it choose
  which to call based on the question.
