DEV Community

Franz Wong
Franz Wong

Posted on

Write AI agent from scratch without LangChain and CrewAI

We are going to create an AI agent which can perform timezone conversion with tools / function calling. Only openai library is used.

Full source code will be given at the end.

Set up

We only need to install openai library.

pip install openai
Enter fullscreen mode Exit fullscreen mode

Define tools

We will define 2 tools. One is to get today's date and the other is to convert the timezone.

We also define tool_map which stores the details about the tools. The key of the map is the function name. prompt is the details we will send to LLM and function is the function we will call.

If your tools perform sensitive operations, you should verify or sanitize the parameters first.

def get_today_date(timezone):
    return datetime.datetime.now(ZoneInfo(timezone)).strftime("%Y-%m-%d")


def convert_timezone(source_timezone, target_timezone, year, month, day, hour, minute):
    dt = datetime.datetime(year, month, day, hour, minute, tzinfo=ZoneInfo(source_timezone))
    return dt.astimezone(ZoneInfo(target_timezone))

tool_map = {
    "get_today_date": {
        "prompt": """get_today_date(timezone)
e.g. get_today_date("Asia/Tokyo")
Description: Get the date of today.
""",
        "function": get_today_date,
    },
    "convert_timezone": {
        "prompt": """convert_timezone(source_timezone, target_timezone, year, month, day, hour, minute)
e.g. convert_timezone("Asia/Tokyo", "Europe/London" 2025, 2, 15, 13, 0)
Description: Convert time from source timezone to target timezone
""",
        "function": convert_timezone
    }
}
Enter fullscreen mode Exit fullscreen mode

Define system prompt

We apply "ReAct Prompting" to enable act and reasoning abilities. This also allows us to use tools.

In order to make LLM stop generating after proposing an action, we ask it to output "PAUSE" text after an action.

system_prompt = f"""Answer the following questions as best you can. You have access to the following tools:

{"\n".join([tool["prompt"] for tool in tool_map.values()])}

If you need to use the tool, you MUST return PAUSE!!!! This is very IMPORTANT.

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{", ".join(tool_map.keys())}]. This ONLY includes the name of the tool.
Action Input: the input to the action. This ONLY includes the parameters of the tool. Example: Action Input: a, "b", c
PAUSE
Observation: the result of the action
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!"""
Enter fullscreen mode Exit fullscreen mode

Pseudocode of AI agent

Before we see the actual code of AI agent, we check the pseudocode first.

We have a loop to keep sending messages to LLM and performing actions.

messages = [system_prompt, question_prompt]

completion = chat(llm, messages)
add completion to messages with role 'assistant'

while 'Final Answer' is not in completion and maximum round is not reached:

  If 'Action' is not found in completion:
    continue
  action = parse_action(completion)

  If 'Action Input' is not in completion:
    continue
  action_input = parse_action_input(completion)

  result = action(acion_input)

  add result to messages with role 'assistant'

  completion = chat(llm, messages)
  add completion to messages with role 'assistant'
Enter fullscreen mode Exit fullscreen mode

chat function

chat function is straight forward. It only creates chat completion with the messages we have.

Stop word "PAUSE" is used to make LLM stop generation after proposing an action.

def chat(client, messages) -> str:
    completion = client.chat.completions.create(messages=messages, model="gpt-4o", stop="PAUSE")
    completion_content = completion.choices[0].message.content
    print("Chat completion:")
    print(completion_content)
    return completion_content
Enter fullscreen mode Exit fullscreen mode

parse_action function

We use regular expression to extract tool function name. Tool function will be returned. We also make sure tool function is defined in tool_map for security reason.

You can also change the system prompt to ask LLM returning JSON to ease parsing.

def parse_action(completion_content: str):
    action_pattern = r"Action: ([^\n]*)"
    match = re.search(action_pattern, completion_content)
    if not match:
        return None
    tool_name = match.group(1)
    if tool_name not in tool_map:
        raise ValueError(f"Tool '{tool_name}' does not exist")
    return tool_map[tool_name]
Enter fullscreen mode Exit fullscreen mode

parse_action_input function

As same as parse_action, we use regular expression to extract parameters of tool.

def parse_action_input(completion_content: str):
    action_input_pattern = r"Action Input: ([^\n]*)"
    match = re.search(action_input_pattern, completion_content)
    if not match:
        return None
    args_string = match.group(1)
    return eval(f"({args_string},)")
Enter fullscreen mode Exit fullscreen mode

AI agent main logic

Here is the implementaiton of the above pseudocode.

def main():
    client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

    question = "Convert 13:49 today in London time to Japan time. Please consider daylight saving."
    print(f"Question: {question}")

    messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": f"Question: {question}"}]

    completion_content = chat(client, messages)
    messages.append({"role": "assistant", "content": completion_content})

    current_round = 0
    max_round = 5

    while "Answer" not in completion_content and current_round < max_round:
        current_round += 1

        action = parse_action(completion_content)
        if action is None:
            continue

        action_input = parse_action_input(completion_content)
        if action_input is None:
            continue

        action_result = action["function"](*action_input)
        print(f"Action result: {action_result}")

        messages.append({"role": "assistant", "content": f"Observation: {action_result}"})

        completion_content = chat(client, messages)
        messages.append({"role": "assistant", "content": completion_content})


if __name__ == '__main__':
    main()
Enter fullscreen mode Exit fullscreen mode

Sample result

Question: Convert 13:49 today in London time to Japan time. Please consider daylight saving.
Chat completion:
Thought: To accurately convert 13:49 London time to Japan time, it's important to consider the current date and whether daylight saving time is in effect. I'll first fetch the current date in the "Europe/London" timezone to understand if daylight saving time needs to be taken into account.
Action: get_today_date
Action Input: "Europe/London"

Action result: 2025-02-23
Chat completion:
Thought: Today is February 23, 2025. Daylight saving time in London typically starts on the last Sunday in March and ends on the last Sunday in October. Therefore, currently, London is on Greenwich Mean Time (GMT, UTC+0). Japan does not observe daylight saving time and is always on Japan Standard Time (JST, UTC+9). I will now convert 13:49 London time to Japan time for today.
Action: convert_timezone
Action Input: "Europe/London", "Asia/Tokyo", 2025, 2, 23, 13, 49

Action result: 2025-02-23 22:49:00+09:00
Chat completion:
Thought: I have successfully converted 13:49 London time to Japan time. The converted time is 22:49 on February 23, 2025.
Final Answer: 13:49 in London on February 23, 2025, is 22:49 in Japan time.
Enter fullscreen mode Exit fullscreen mode

Full source code

import datetime
import os
import re
from zoneinfo import ZoneInfo

from openai import OpenAI


def get_today_date(timezone):
    return datetime.datetime.now(ZoneInfo(timezone)).strftime("%Y-%m-%d")


def convert_timezone(source_timezone, target_timezone, year, month, day, hour, minute):
    dt = datetime.datetime(year, month, day, hour, minute, tzinfo=ZoneInfo(source_timezone))
    return dt.astimezone(ZoneInfo(target_timezone))


tool_map = {
    "get_today_date": {
        "prompt": """get_today_date(timezone)
e.g. get_today_date("Asia/Tokyo")
Description: Get the date of today.
""",
        "function": get_today_date,
    },
    "convert_timezone": {
        "prompt": """convert_timezone(source_timezone, target_timezone, year, month, day, hour, minute)
e.g. convert_timezone("Asia/Tokyo", "Europe/London" 2025, 2, 15, 13, 0)
Description: Convert time from source timezone to target timezone
""",
        "function": convert_timezone
    }
}

system_prompt = f"""Answer the following questions as best you can. You have access to the following tools:

{"\n".join([tool["prompt"] for tool in tool_map.values()])}

If you need to use the tool, you MUST return PAUSE!!!! This is very IMPORTANT.

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{", ".join(tool_map.keys())}]. This ONLY includes the name of the tool.
Action Input: the input to the action. This ONLY includes the parameters of the tool. Example: Action Input: a, "b", c
PAUSE
Observation: the result of the action
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!"""


def chat(client, messages) -> str:
    completion = client.chat.completions.create(messages=messages, model="gpt-4o", stop="PAUSE")
    completion_content = completion.choices[0].message.content
    print("Chat completion:")
    print(completion_content)
    return completion_content


def parse_action(completion_content: str):
    action_pattern = r"Action: ([^\n]*)"
    match = re.search(action_pattern, completion_content)
    if not match:
        return None
    tool_name = match.group(1)
    if tool_name not in tool_map:
        raise ValueError(f"Tool '{tool_name}' does not exist")
    return tool_map[tool_name]


def parse_action_input(completion_content: str):
    action_input_pattern = r"Action Input: ([^\n]*)"
    match = re.search(action_input_pattern, completion_content)
    if not match:
        return None
    args_string = match.group(1)
    return eval(f"({args_string},)")


def main():
    client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

    question = "Convert 13:49 today in London time to Japan time. Please consider daylight saving."
    print(f"Question: {question}")

    messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": f"Question: {question}"}]

    completion_content = chat(client, messages)
    messages.append({"role": "assistant", "content": completion_content})

    current_round = 0
    max_round = 5

    while "Final Answer" not in completion_content and current_round < max_round:
        current_round += 1

        action = parse_action(completion_content)
        if action is None:
            continue

        action_input = parse_action_input(completion_content)
        if action_input is None:
            continue

        action_result = action["function"](*action_input)
        print(f"Action result: {action_result}")

        messages.append({"role": "assistant", "content": f"Observation: {action_result}"})

        completion_content = chat(client, messages)
        messages.append({"role": "assistant", "content": completion_content})


if __name__ == '__main__':
    main()

Enter fullscreen mode Exit fullscreen mode

Top comments (0)