Introduction

AI agents are a specific kind of AI application. They are mainly defined by two things: an LLM running in a loop and access to the external world via tools. An AI agent is in effect an LLM that is repeatedly asked to make a decision: given a user input and progress so far, what should be done next? Giving LLMs reasoning capabilities, the architecture has proven powerful and flexible, and is used for a variety of applications.

A common example is a researcher AI agent. Given tools for searching the web, browsing websites, and perhaps storing outputs in a scratch file, the agent assembles a report on a particular topic. After every loop the LLM decides the next action (use a tool or any other custom action defined by the dev), given the progress it’s had so far.

Image by Alex Honchar

In this tutorial, our goal will be to create a simple agent that can create images based on live information, e.g. the current weather in the user’s chosen location.

Try out and see the agent in action by running the agent.ts script that calls already deployed contract.

Deployed contract address:

Prerequisites

You can read this tutorial as-is to understand the basics of building an agent. However, to deploy the contract and interact with it, you will need:

  • A Galadriel devnet account. For more information on setting up a wallet, visit Setting Up A Wallet.
  • Some devnet tokens. Get your free devnet tokens from the Faucet.

Before agents: single LLM call

As the first step, let’s create a contract that makes a single LLM call without any loop or external tools. Once we have that contract in place, we’ll build on it until we have a complete agent with loops.

First, set up the basic smart contract with an oracle integration point.

pragma solidity ^0.8.9;

import "./interfaces/IOracle.sol";

contract Agent {
    address private owner;
    address public oracleAddress;
    string public prompt;

    event OracleAddressUpdated(address indexed newOracleAddress);

    constructor(address initialOracleAddress, string memory systemPrompt) {
        owner = msg.sender;
        oracleAddress = initialOracleAddress;
        prompt = systemPrompt;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Caller is not owner");
        _;
    }

    function setOracleAddress(address newOracleAddress) public onlyOwner {
        oracleAddress = newOracleAddress;
        emit OracleAddressUpdated(newOracleAddress);
    }
}

The constructor includes a systemPrompt parameter, which sets the foundational behavior of your agent. Specifying this prompt at the time of deploying the smart contract enhances flexibility, allowing the agent’s behavior to be tailored from the outset. A typical example of a system prompt might be You are a helpful assistant. This prompt guides the agent’s interactions, ensuring it consistently behaves in a helpful and assistant-like manner.

A simple LLM call

Our first task is to establish a foundation by creating a smart contract that can make a single LLM call. This initial step helps us understand the basic interaction with an LLM without looping or using external tools.

Configuration

Since we are going to use OpenAI LLM, first step is to define configuration parameters using OpenAiRequest object. We will do that in smart contract constructor.

IOracle.OpenAiRequest private config;

constructor(address initialOracleAddress, string memory systemPrompt) {
    owner = msg.sender;
    oracleAddress = initialOracleAddress;
    prompt = systemPrompt;

    config = IOracle.OpenAiRequest({
        model : "gpt-4-turbo-preview",
        frequencyPenalty : 21, // > 20 for null
        logitBias : "", // empty str for null
        maxTokens : 1000, // 0 for null
        presencePenalty : 21, // > 20 for null
        responseFormat : "{\"type\":\"text\"}",
        seed : 0, // null
        stop : "", // null
        temperature : 10, // Example temperature (scaled up, 10 means 1.0), > 20 means null
        topP : 101, // Percentage 0-100, > 100 means null
        tools : "",
        toolChoice : "",
        user : "" // null
    });
}

At the moment we don’t need any tools defined.

Making an LLM call

We need to store the agent steps within a run. For this reason we define two structs: a Message struct to store the message content and the role of the sender, and an AgentRun struct to store the agent history.

struct Message {
    string role;
    string content;
}
struct AgentRun {
    address owner;
    Message[] messages;
    uint responsesCount;
    uint8 max_iterations;
    bool is_finished;
}

Given the above definitions, the runAgent function initializes a new agent run (AgentRun struct) and adds the system prompt and first message, from the message argument which will be the end-user’s first message. The AgentRun struct is then stored in a mapping, with the run ID as the key — we need a unique ID for every Agent so we can retrieve it again when the oracle makes a callback (which we will implement later). max_iterations parameter is used to define how many loops the agent can do before it stops.

Finally, we create an LLM call by calling createOpenAiLlmCall on the oracle, passing the OpenAiRequest configuration and run ID as an argument. This will trigger the oracle to make a request to the LLM. We also emit an event notifying that a new agent run has been created.

event AgentRunCreated(address indexed owner, uint indexed runId);
mapping(uint => AgentRun) public agentRuns;
uint private agentRunCount;

function runAgent(string memory query, uint8 max_iterations) public returns (uint i) {
    AgentRun storage run = agentRuns[agentRunCount];

    run.owner = msg.sender;
    run.is_finished = false;
    run.responsesCount = 0;
    run.max_iterations = max_iterations;

    Message memory systemMessage;
    systemMessage.content = prompt;
    systemMessage.role = "system";
    run.messages.push(systemMessage);

    Message memory newMessage;
    newMessage.content = query;
    newMessage.role = "user";
    run.messages.push(newMessage);

    uint currentId = agentRunCount;
    agentRunCount = agentRunCount + 1;
    IOracle(oracleAddress).createOpenAiLlmCall(currentId, config);

    emit AgentRunCreated(run.owner, currentId);

    return currentId;
}

The oracle needs to fetch the agent history from the contract. It does so by calling two functions: getMessageHistoryContents and getMessageHistoryRoles, on your contract, after your contract invokes createOpenAiLlmCall.

The methods should each return a list, one with message contents and the other listing the roles of the message authors. The list lengths should be equal for a given callbackId.

function getMessageHistoryContents(uint agentId) public view returns (string[] memory) {
    string[] memory messages = new string[](agentRuns[agentId].messages.length);
    for (uint i = 0; i < agentRuns[agentId].messages.length; i++) {
        messages[i] = agentRuns[agentId].messages[i].content;
    }
    return messages;
}
function getMessageHistoryRoles(uint agentId) public view returns (string[] memory) {
    string[] memory roles = new string[](agentRuns[agentId].messages.length);
    for (uint i = 0; i < agentRuns[agentId].messages.length; i++) {
        roles[i] = agentRuns[agentId].messages[i].role;
    }
    return roles;
}

We need a way for the oracle to post a response back to our contract. For this, we implement a callback function onOracleLlmResponse that the oracle can call once it has processed the response. The function should take three arguments: the runId (the run ID we passed in createLlmCall), the response (the response from the LLM), and an errorMessage (non-empty if there was an error). The function should only be callable by the oracle, so we add a onlyOracle modifier to ensure this.

The function first checks if the errorMessage is non-empty. If it wasn’t, the function adds the error to the agent history and marks the run as finished. Otherwise it creates a new message with the response from the LLM and adds it to the agent history. The function increments the message count. Since our goal is to make just one LLM call, the function is marking the run as finished.

function onOracleOpenAiLlmResponse(
    uint runId,
    IOracle.OpenAiResponse memory response,
    string memory errorMessage
) public onlyOracle {
    AgentRun storage run = agentRuns[runId];
    if (!compareStrings(errorMessage, "")) {
        Message memory newMessage;
        newMessage.role = "assistant";
        newMessage.content = errorMessage;
        run.messages.push(newMessage);
        run.responsesCount++;
        run.is_finished = true;
        return;
    }
    if (!compareStrings(response.content, "")) {
        Message memory assistantMessage;
        assistantMessage.content = response.content;
        assistantMessage.role = "assistant";
        run.messages.push(assistantMessage);
        run.responsesCount++;
    }
    run.is_finished = true;
}

Making a loop

So far we’ve made an agent that makes one LLM query and finishes the run. We will now try to modify our agent to make one extra LLM call after getting results from a previous one, thus creating an agent loop.

Let’s modify our onOracleOpenAiLlmResponse to do so. Upon successful response from the Oracle, the agent will ask for more details. We can track agent steps by checking the responsesCount variable and make a decision about whether the agent should make another loop (as the result is incomplete) or end the run.

function onOracleOpenAiLlmResponse(
    uint runId,
    IOracle.OpenAiResponse memory response,
    string memory errorMessage
) public onlyOracle {
    AgentRun storage run = agentRuns[runId];
    if (!compareStrings(errorMessage, "")) {
        Message memory assistantMessage;
        assistantMessage.role = "assistant";
        assistantMessage.content = errorMessage;
        run.messages.push(assistantMessage);
        run.responsesCount++;
        run.is_finished = true;
        return;
    }
    if (!compareStrings(response.content, "")) {
        Message memory assistantMessage;
        assistantMessage.content = response.content;
        assistantMessage.role = "assistant";
        run.messages.push(assistantMessage);
        run.responsesCount++;

        // if this is the first response, ask for more details
        if (run.responsesCount == 1) {
            Message memory newMessage;
            newMessage.content = "Please elaborate!";
            newMessage.role = "user";
            run.messages.push(newMessage);
            IOracle(oracleAddress).createOpenAiLlmCall(runId, config);
            return;
        }
        // we already asked for clarification, mark the agent run as finished
        run.is_finished = true;
    }
}

Example run with an LLM loop

1

Execute the following command to run a script that calls the deployed contract:

npm run agent

The script will interactively ask for the input and then call the contract with the input. The output, when ready, will be printed to the console.

If this step fails, make sure you have set the AGENT_ADDRESS environment variable to the address of your deployed contract.

Agent's task: Who is the president of United States?
Max iterations: 5
Task sent, tx hash: 0x20418bcd17d5c8714c59f4ccab02c8d45eb9c368523f53f45e1eb58af3296c03
Agent started with task: "Who is the president of United States?"
Created agent run ID: 0
STEP: You are a helpful assistant
STEP: Who is the president of United States?
THOUGHT: As of my last update in 2023, Joe Biden is the President of the United States. He was inaugurated as the 46th president on January 20, 2021.
STEP: Please elaborate!
THOUGHT: Joe Biden, whose full name is Joseph Robinette Biden Jr., is an American politician from the Democratic Party who assumed office as the 46th President of the United States on January 20, 2021. Before becoming president, Joe Biden had a long and distinguished career in public service. He was born on November 20, 1942, in Scranton, Pennsylvania, and later moved to Delaware. 

Biden's early entry into politics saw him elected to the New Castle County Council in 1970. Just two years later, at the age of 29, he made a significant leap into national politics by winning a seat in the U.S. Senate, representing Delaware. He would go on to serve in the Senate for 36 years, making him one of the longest-serving senators in American history. During his tenure in the Senate, Biden was known for his work on foreign relations, criminal justice, and drug policy. He served as the Chairman of the Senate Judiciary Committee and was also the Chairman of the Senate Foreign Relations Committee for several years.

In 2008, Joe Biden was elected Vice President of the United States as the running mate of Barack Obama. He served two terms as Vice President from 2009 to 2017. Their administration was notable for its efforts in healthcare reform, economic recovery following the Great Recession, and foreign policy achievements, including the operation that led to the death of Osama bin Laden.

After briefly retiring from public office following his vice presidency, Biden announced his candidacy for the 2020 presidential election. He won the Democratic nomination and went on to defeat the incumbent, Donald Trump, in the November 2020 election. The Biden-Harris ticket’s victory was historic for several reasons, including Kamala Harris becoming the first female Vice President, as well as the highest-ranking female official in U.S. history, and the first African American and Asian American Vice President.

President Biden's administration has focused on addressing the COVID-19 pandemic, economic recovery, climate change, and civil rights. Biden has emphasized unity and healing for the nation, aiming to bridge the political divide and address the challenges facing America in the 21st century.
agent run ID 0 finished!
Done

Let’s add a web search tool to the agent’s capabilities, allowing it to retrieve real-time data such as the current weather.

We first modify our OpenAiRequest configuration by defining the web_search tool and setting toolChoice to auto.

    config = IOracle.OpenAiRequest({
        //...
        tools : "[{\"type\":\"function\",\"function\":{\"name\":\"web_search\",\"description\":\"Search the internet\",\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Search query\"}},\"required\":[\"query\"]}}}]",
        toolChoice : "auto", // "none" or "auto"
        //...
    });

This setup configures the OpenAI LLM to utilize the web_search tool as needed. However, the responsibility to initiate the tool usage lies with us. To facilitate this, we will adjust the onOracleOpenAiLlmResponse function. This function will not only check when a tool is required and activate it accordingly, but it will also monitor whether the agent run has reached its predefined limit for loops or has concluded with a complete satisfying result.

    function onOracleOpenAiLlmResponse(
        uint runId,
        IOracle.OpenAiResponse memory response,
        string memory errorMessage
    ) public onlyOracle {
        AgentRun storage run = agentRuns[runId];
        require(
            !run.is_finished, "Run is finished"
        );

        if (!compareStrings(errorMessage, "")) {
            Message memory newMessage;
            newMessage.role = "assistant";
            newMessage.content = errorMessage;
            run.messages.push(newMessage);
            run.responsesCount++;
            run.is_finished = true;
            return;
        }
        if (run.responsesCount >= run.max_iterations) {
            run.is_finished = true;
            return;
        }
        if (!compareStrings(response.content, "")) {
            Message memory assistantMessage;
            assistantMessage.content = response.content;
            assistantMessage.role = "assistant";
            run.messages.push(assistantMessage);
            run.responsesCount++;
        }
        if (!compareStrings(response.functionName, "")) {
            IOracle(oracleAddress).createFunctionCall(runId, response.functionName, response.functionArguments);
            return;
        }
        // the LLM has given the answer, nothing else to do
        run.is_finished = true;
    }

To manage the output from a tool call effectively, we need to ensure its reception from the oracle is handled appropriately. We will implement the onOracleFunctionResponse function to receive either the output from the tool or an error message. After capturing and storing this information in the agent’s history, we will initiate another LLM call to process the results, continuing the agent’s operations seamlessly.

function onOracleFunctionResponse(
        uint runId,
        string memory response,
        string memory errorMessage
    ) public onlyOracle {
        AgentRun storage run = agentRuns[runId];
        require(
            !run.is_finished, "Run is finished"
        );
        string memory result = response;
        if (!compareStrings(errorMessage, "")) {
            result = errorMessage;
        }
        Message memory newMessage;
        newMessage.role = "user";
        newMessage.content = result;
        run.messages.push(newMessage);
        run.responsesCount++;
        IOracle(oracleAddress).createOpenAiLlmCall(runId, config);
    }

Example run with web_search tool

Agent started with task: "What is the weather like in New York?"
Created agent run ID: 0
STEP: You are a helpful assistant
STEP: What is the weather like in New York?
STEP: [{"title": "Weather Forecast and Conditions for New York City, NY", "link": "https://weather.com/weather/today/l/96f2f84af9a5f5d452eb0574d4e4d8a840c71b05e22264ebdc0056433a642c84", "snippet": "New York City, NY. As of 7:54 am EDT. 46\u00b0. Drizzle. Day 49\u00b0 \u2022 Night 45\u00b0. Small Craft Advisory. Latest News. Why 2024 Will Be One Of The Most Active Seasons ...", "position": 1}, {"title": "Hourly Weather Forecast for New York City, NY", "link": "https://weather.com/weather/hourbyhour/l/f892433d7660da170347398eb8e3d722d8d362fe7dd15af16ce88324e1b96e70", "snippet": "Wednesday, April 17 \u00b7 1 am. 57\u00b0. 1%. Partly Cloudy. Feels Like57\u00b0. WindNNE 3 mph. Humidity46%. UV Index0 of 11 \u00b7 2 am. 56\u00b0. 2%. Mostly Clear. Feels Like56\u00b0.", "position": 2}, {"title": "Current Weather - New York, NY - AccuWeather", "link": "https://www.accuweather.com/en/us/new-york/10021/current-weather/349727", "snippet": "New York, NY \u00b7 Current Weather. 7:15 AM. 47\u00b0F.", "position": 3}, {"title": "Hourly Weather Forecast for Manhattan, NY", "link": "https://weather.com/weather/hourbyhour/l/Manhattan+NY?canonicalCityId=fc47c333c5d13e34e34c9fdb6e047ceb70f7891e01bc9e1d574b5f93f58aa76d", "snippet": "Friday, April 19 \u00b7 1 am. 47\u00b0. 4%. Mostly Cloudy. Feels Like45\u00b0. WindNE 5 mph. Humidity65% \u00b7 2 am. 47\u00b0. 4%. Mostly Cloudy. Feels Like45\u00b0. WindENE 5 mph. Humidity ...", "position": 4}, {"title": "New York, NY Weather Forecast - AccuWeather", "link": "https://www.accuweather.com/en/us/new-york/10021/weather-forecast/349727", "snippet": "10-Day Weather Forecast ; Today. 4/18. 50\u00b0 44\u00b0 ; Fri. 4/19. 56\u00b0 48\u00b0 ; Sat. 4/20. 67\u00b0 45\u00b0 ; Sun. 4/21. 60\u00b0 46\u00b0 ; Mon. 4/22. 65\u00b0 49\u00b0.", "sitelinks": [{"title": "Current Weather", "link": "https://www.accuweather.com/en/us/new-york/10021/current-weather/349727"}, {"title": "Daily", "link": "https://www.accuweather.com/en/us/new-york/10021/daily-weather-forecast/349727"}, {"title": "Hourly", "link": "https://www.accuweather.com/en/us/new-york/10021/hourly-weather-forecast/349727"}, {"title": "MinuteCast", "link": "https://www.accuweather.com/en/us/new-york/10021/minute-weather-forecast/349727"}], "position": 5}, {"title": "Current WeatherNew York, NY - The Weather Network", "link": "https://www.theweathernetwork.com/en/city/us/new-york/new-york/current", "snippet": "Current WeatherNew York, NY. New York, NY. Updated 7 minutes ago. 8. \u00b0C. Light rain. Feels 4. H: 10\u00b0 L: 7\u00b0. Hourly. Full 72 hours \u00b7 12pm. Cloudy. 8\u00b0.", "sitelinks": [{"title": "Hourly", "link": "https://www.theweathernetwork.com/us/hourly-weather-forecast/new-york/new-york"}, {"title": "14 Day", "link": "https://www.theweathernetwork.com/us/14-day-weather-trend/new-york/new-york"}, {"title": "7 Day", "link": "https://www.theweathernetwork.com/us/36-hour-weather-forecast/new-york/new-york"}], "position": 6}, {"title": "Weather Forecast and Conditions for Manhattan, NY", "link": "https://weather.com/weather/today/l/New+York+NY+USNY0996", "snippet": "Manhattan, NY. As of 11:01 am EDT. 61\u00b0. Partly Cloudy. Day 61\u00b0 \u2022 Night 48\u00b0. Small Craft Advisory. Latest News. 'Everything's Gone'- Tornado Survivor ...", "position": 7}, {"title": "Accutrack Radar | New York Weather News - abc7NY", "link": "https://abc7ny.com/weather/", "snippet": "Today's Weather. New York City, NY. Current; Tonight; Tomorrow. CLOUDY. 50\u00b0. Feels Like. 45\u00b0. Sunrise. 6:13 AM. Humidity. 81%. Sunset. 7:38 PM. Windspeed. ENE ...", "sitelinks": [{"title": "AccuWeather: Raw and damp", "link": "https://abc7ny.com/nyc-weather-accuweather-forecast-new-york-city-tri-state/27410/"}, {"title": "AccuTrack Radar", "link": "https://abc7ny.com/weather/doppler/accutrack-radar/"}, {"title": "Weather Alerts", "link": "https://abc7ny.com/weather/alerts/"}, {"title": "New Jersey", "link": "https://abc7ny.com/weather/doppler/new-jersey/"}], "position": 8}, {"title": "Weather - NBC New York", "link": "https://www.nbcnewyork.com/weather/", "snippet": "Click to search by city or zip... New York City, NY. 47\u00b0. Light Drizzle. Feels like40\u00b0. Humidity90%. Precip0.09%. Wind16MPH. Temperature Precipitation. 47\u00ba.", "sitelinks": [{"title": "Maps and Radar", "link": "https://www.nbcnewyork.com/weather/maps/"}, {"title": "Weather", "link": "https://www.nbcnewyork.com/tag/weather/"}, {"title": "Weather Alerts", "link": "https://www.nbcnewyork.com/weather/severe-weather-alerts/"}, {"title": "School Closings", "link": "https://www.nbcnewyork.com/weather/school-closings/"}], "position": 9}, {"title": "New York City, NY Hourly Weather Forecast", "link": "https://www.wunderground.com/hourly/us/ny/new-york-city", "snippet": "New York City, NY Hourly Weather Forecaststar_ratehome ; 1:00 pm, Cloudy, 46 \u00b0F ; 2:00 pm, Cloudy, 47 \u00b0F ; 3:00 pm, Cloudy, 48 \u00b0F ; 4:00 pm, Cloudy, 48 \u00b0F ...", "position": 10}]
THOUGHT: The current weather in New York City, NY is as follows:

- **Temperature**: 46°F (7.8°C) with drizzle.
- **Day Temperature**: Up to 49°F.
- **Night Temperature**: Around 45°F.
- **Wind**: Northeast with speeds around 3 to 5 mph.
- **Humidity**: Ranges from 46% to around 65%.
- **Conditions**: Mainly drizzle and cloudy conditions across different parts of the city.

For more detailed conditions, including forecasts and advisories, you may visit the provided links.
agent run ID 0 finished!
Done

It’s evident that the agent utilized the web_search tool, incorporated the results into its history, and then proceeded to invoke the LLM, which produced the final response.

Adding a tool: image generation

To integrate an additional tool, simply include it in the OpenAiRequest configuration. No further modifications to the agent’s logic are required, as the process is designed to be agnostic of the specific tool being used.

config = IOracle.OpenAiRequest({
    //...
    tools : "[{\"type\":\"function\",\"function\":{\"name\":\"web_search\",\"description\":\"Search the internet\",\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Search query\"}},\"required\":[\"query\"]}}},{\"type\":\"function\",\"function\":{\"name\":\"image_generation\",\"description\":\"Generates an image using Dalle-2\",\"parameters\":{\"type\":\"object\",\"properties\":{\"prompt\":{\"type\":\"string\",\"description\":\"Dalle-2 prompt to generate an image\"}},\"required\":[\"prompt\"]}}}]",
    //...
});

Example run with image_generation tool

Agent's task: Generate an image of a beaver.
Max iterations: 5
Task sent, tx hash: 0xd3e27b24cac0417243642599a917f075d89ceaf6e6aea81ccaab1ba9dcd1347d
Agent started with task: "Generate an image of a beaver."
Created agent run ID: 0
STEP: You are a helpful assistant
STEP: Generate an image of a beaver.
STEP: https://oaidalleapiprodscus.blob.core.windows.net/private/org-OIsLdcbHIc1F6swhIoWbkFsS/user-dvcNG9YUCv8srEVF1LN250bv/img-Nrhz2ojGrZXBkWoOMnvZrYXx.png?st=2024-04-18T18%3A24%3A06Z&se=2024-04-18T20%3A24%3A06Z&sp=r&sv=2021-08-06&sr=b&rscd=inline&rsct=image/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2024-04-17T22%3A32%3A23Z&ske=2024-04-18T22%3A32%3A23Z&sks=b&skv=2021-08-06&sig=AH5PvmftgbhhZHm1ZyCUtcUw%2BdboorxzV%2BDRUruIHVM%3D
THOUGHT: I've generated an image of a beaver for you. You can view the image by clicking on the link below:

[View Beaver Image](https://oaidalleapiprodscus.blob.core.windows.net/private/org-OIsLdcbHIc1F6swhIoWbkFsS/user-dvcNG9YUCv8srEVF1LN250bv/img-Nrhz2ojGrZXBkWoOMnvZrYXx.png?st=2024-04-18T18%3A24%3A06Z&se=2024-04-18T20%3A24%3A06Z&sp=r&sv=2021-08-06&sr=b&rscd=inline&rsct=image/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2024-04-17T22%3A32%3A23Z&ske=2024-04-18T22%3A32%3A23Z&sks=b&skv=2021-08-06&sig=AH5PvmftgbhhZHm1ZyCUtcUw%2BdboorxzV%2BDRUruIHVM%3D) 

Please note, the link will expire after a certain duration, so be sure to view or download the image promptly.
agent run ID 0 finished!
Done

Complete agent

The most recent version is available on the GitHub repository.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Uncomment this line to use console.log
// import "hardhat/console.sol";
import "./interfaces/IOracle.sol";

contract Agent {

    string public prompt;

    struct Message {
        string role;
        string content;
    }

    struct AgentRun {
        address owner;
        Message[] messages;
        uint responsesCount;
        uint8 max_iterations;
        bool is_finished;
    }

    mapping(uint => AgentRun) public agentRuns;
    uint private agentRunCount;

    event AgentRunCreated(address indexed owner, uint indexed runId);

    address private owner;
    address public oracleAddress;

    event OracleAddressUpdated(address indexed newOracleAddress);

    IOracle.OpenAiRequest private config;

    constructor(
        address initialOracleAddress,         
        string memory systemPrompt
    ) {
        owner = msg.sender;
        oracleAddress = initialOracleAddress;
        prompt = systemPrompt;

        config = IOracle.OpenAiRequest({
            model : "gpt-4-turbo-preview",
            frequencyPenalty : 21, // > 20 for null
            logitBias : "", // empty str for null
            maxTokens : 1000, // 0 for null
            presencePenalty : 21, // > 20 for null
            responseFormat : "{\"type\":\"text\"}",
            seed : 0, // null
            stop : "", // null
            temperature : 10, // Example temperature (scaled up, 10 means 1.0), > 20 means null
            topP : 101, // Percentage 0-100, > 100 means null
            tools : "[{\"type\":\"function\",\"function\":{\"name\":\"web_search\",\"description\":\"Search the internet\",\"parameters\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Search query\"}},\"required\":[\"query\"]}}},{\"type\":\"function\",\"function\":{\"name\":\"image_generation\",\"description\":\"Generates an image using Dalle-2\",\"parameters\":{\"type\":\"object\",\"properties\":{\"prompt\":{\"type\":\"string\",\"description\":\"Dalle-2 prompt to generate an image\"}},\"required\":[\"prompt\"]}}}]",
            toolChoice : "auto", // "none" or "auto"
            user : "" // null
        });
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Caller is not owner");
        _;
    }

    modifier onlyOracle() {
        require(msg.sender == oracleAddress, "Caller is not oracle");
        _;
    }

    function setOracleAddress(address newOracleAddress) public onlyOwner {
        require(msg.sender == owner, "Caller is not the owner");
        oracleAddress = newOracleAddress;
        emit OracleAddressUpdated(newOracleAddress);
    }

    function runAgent(string memory query, uint8 max_iterations) public returns (uint i) {
        AgentRun storage run = agentRuns[agentRunCount];

        run.owner = msg.sender;
        run.is_finished = false;
        run.responsesCount = 0;
        run.max_iterations = max_iterations;

        Message memory systemMessage;
        systemMessage.content = prompt;
        systemMessage.role = "system";
        run.messages.push(systemMessage);

        Message memory newMessage;
        newMessage.content = query;
        newMessage.role = "user";
        run.messages.push(newMessage);

        uint currentId = agentRunCount;
        agentRunCount = agentRunCount + 1;

        IOracle(oracleAddress).createOpenAiLlmCall(currentId, config);
        emit AgentRunCreated(run.owner, currentId);

        return currentId;
    }

    function onOracleOpenAiLlmResponse(
        uint runId,
        IOracle.OpenAiResponse memory response,
        string memory errorMessage
    ) public onlyOracle {
        AgentRun storage run = agentRuns[runId];

        if (!compareStrings(errorMessage, "")) {
            Message memory newMessage;
            newMessage.role = "assistant";
            newMessage.content = errorMessage;
            run.messages.push(newMessage);
            run.responsesCount++;
            run.is_finished = true;
            return;
        }
        if (run.responsesCount >= run.max_iterations) {
            run.is_finished = true;
            return;
        }
        if (!compareStrings(response.content, "")) {
            Message memory assistantMessage;
            assistantMessage.content = response.content;
            assistantMessage.role = "assistant";
            run.messages.push(assistantMessage);
            run.responsesCount++;
        }
        if (!compareStrings(response.functionName, "")) {
            IOracle(oracleAddress).createFunctionCall(runId, response.functionName, response.functionArguments);
            return;
        }
        run.is_finished = true;
    }

    function onOracleFunctionResponse(
        uint runId,
        string memory response,
        string memory errorMessage
    ) public onlyOracle {
        AgentRun storage run = agentRuns[runId];
        require(
            !run.is_finished, "Run is finished"
        );
        string memory result = response;
        if (!compareStrings(errorMessage, "")) {
            result = errorMessage;
        }
        Message memory newMessage;
        newMessage.role = "user";
        newMessage.content = result;
        run.messages.push(newMessage);
        run.responsesCount++;
        IOracle(oracleAddress).createOpenAiLlmCall(runId, config);
    }

    function getMessageHistoryContents(uint agentId) public view returns (string[] memory) {
        string[] memory messages = new string[](agentRuns[agentId].messages.length);
        for (uint i = 0; i < agentRuns[agentId].messages.length; i++) {
            messages[i] = agentRuns[agentId].messages[i].content;
        }
        return messages;
    }

    function getMessageHistoryRoles(uint agentId) public view returns (string[] memory) {
        string[] memory roles = new string[](agentRuns[agentId].messages.length);
        for (uint i = 0; i < agentRuns[agentId].messages.length; i++) {
            roles[i] = agentRuns[agentId].messages[i].role;
        }
        return roles;
    }

    function isRunFinished(uint runId) public view returns (bool) {
        return agentRuns[runId].is_finished;
    }

    function compareStrings(string memory a, string memory b) private pure returns (bool) {
        return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b))));
    }
}

Final run

Agent's task: Generate an image of current weather conditions in New York
Max iterations: 10
Task sent, tx hash: 0xd19bdd9539d5233ecef968319462362d8128eb2caabe2bf5df61977760e342e1
Agent started with task: "Generate an image of current weather conditions in New York"
Created agent run ID: 1
STEP: You are a helpful assistant
STEP: Generate an image of current weather conditions in New York
STEP: [{"title": "Weather Forecast and Conditions for New York City, NY", "link": "https://weather.com/weather/today/l/96f2f84af9a5f5d452eb0574d4e4d8a840c71b05e22264ebdc0056433a642c84", "snippet": "New York City, NY. As of 7:54 am EDT. 46\u00b0. Drizzle. Day 49\u00b0 \u2022 Night 45\u00b0. Small Craft Advisory. Latest News. Why 2024 Will Be One Of The Most Active Seasons ...", "position": 1}, {"title": "Hourly Weather Forecast for New York City, NY", "link": "https://weather.com/weather/hourbyhour/l/f892433d7660da170347398eb8e3d722d8d362fe7dd15af16ce88324e1b96e70", "snippet": "Wednesday, April 17 \u00b7 1 am. 57\u00b0. 1%. Partly Cloudy. Feels Like57\u00b0. WindNNE 3 mph. Humidity46%. UV Index0 of 11 \u00b7 2 am. 56\u00b0. 2%. Mostly Clear. Feels Like56\u00b0.", "position": 2}, {"title": "Current Weather - New York, NY - AccuWeather", "link": "https://www.accuweather.com/en/us/new-york/10021/current-weather/349727", "snippet": "New York, NY \u00b7 Current Weather. 7:15 AM. 47\u00b0F.", "position": 3}, {"title": "Hourly Weather Forecast for Manhattan, NY", "link": "https://weather.com/weather/hourbyhour/l/Manhattan+NY?canonicalCityId=fc47c333c5d13e34e34c9fdb6e047ceb70f7891e01bc9e1d574b5f93f58aa76d", "snippet": "Friday, April 19 \u00b7 1 am. 47\u00b0. 4%. Mostly Cloudy. Feels Like45\u00b0. WindNE 5 mph. Humidity65% \u00b7 2 am. 47\u00b0. 4%. Mostly Cloudy. Feels Like45\u00b0. WindENE 5 mph. Humidity ...", "position": 4}, {"title": "New York, NY Weather Forecast - AccuWeather", "link": "https://www.accuweather.com/en/us/new-york/10021/weather-forecast/349727", "snippet": "10-Day Weather Forecast ; Today. 4/18. 50\u00b0 44\u00b0 ; Fri. 4/19. 56\u00b0 48\u00b0 ; Sat. 4/20. 67\u00b0 45\u00b0 ; Sun. 4/21. 60\u00b0 46\u00b0 ; Mon. 4/22. 65\u00b0 49\u00b0.", "sitelinks": [{"title": "Current Weather", "link": "https://www.accuweather.com/en/us/new-york/10021/current-weather/349727"}, {"title": "Daily", "link": "https://www.accuweather.com/en/us/new-york/10021/daily-weather-forecast/349727"}, {"title": "Hourly", "link": "https://www.accuweather.com/en/us/new-york/10021/hourly-weather-forecast/349727"}, {"title": "MinuteCast", "link": "https://www.accuweather.com/en/us/new-york/10021/minute-weather-forecast/349727"}], "position": 5}, {"title": "Weather Forecast and Conditions for Manhattan, NY", "link": "https://weather.com/weather/today/l/New+York+NY+USNY0996", "snippet": "Manhattan, NY. As of 11:01 am EDT. 61\u00b0. Partly Cloudy. Day 61\u00b0 \u2022 Night 48\u00b0. Small Craft Advisory. Latest News. 'Everything's Gone'- Tornado Survivor ...", "position": 6}, {"title": "Current WeatherNew York, NY - The Weather Network", "link": "https://www.theweathernetwork.com/en/city/us/new-york/new-york/current", "snippet": "Current WeatherNew York, NY. New York, NY. Updated 13 minutes ago. 9. \u00b0C. Light rain. Feels 6. H: 10\u00b0 L: 7\u00b0. Hourly. Full 72 hours \u00b7 6am. Cloudy. 9\u00b0.", "sitelinks": [{"title": "Hourly", "link": "https://www.theweathernetwork.com/us/hourly-weather-forecast/new-york/new-york"}, {"title": "14 Day", "link": "https://www.theweathernetwork.com/us/14-day-weather-trend/new-york/new-york"}, {"title": "7 Day", "link": "https://www.theweathernetwork.com/us/36-hour-weather-forecast/new-york/new-york"}], "position": 7}, {"title": "Accutrack Radar | New York Weather News - abc7NY", "link": "https://abc7ny.com/weather/", "snippet": "Today's Weather. New York City, NY. Current; Tonight; Tomorrow. CLOUDY. 50\u00b0. Feels Like. 45\u00b0. Sunrise. 6:13 AM. Humidity. 81%. Sunset. 7:38 PM. Windspeed. ENE ...", "sitelinks": [{"title": "Latest AccuWeather Forecast", "link": "https://abc7ny.com/nyc-weather-accuweather-forecast-new-york-city-tri-state/27410/"}, {"title": "AccuTrack Radar", "link": "https://abc7ny.com/weather/doppler/accutrack-radar/"}, {"title": "Weather Alerts", "link": "https://abc7ny.com/weather/alerts/"}, {"title": "New Jersey", "link": "https://abc7ny.com/weather/doppler/new-jersey/"}], "position": 8}, {"title": "Weather - NBC New York", "link": "https://www.nbcnewyork.com/weather/", "snippet": "Click to search by city or zip... New York City, NY. 47\u00b0. Light Drizzle. Feels like40\u00b0. Humidity90%. Precip0.09%. Wind16MPH. Temperature Precipitation. 47\u00ba.", "sitelinks": [{"title": "Maps and Radar", "link": "https://www.nbcnewyork.com/weather/maps/"}, {"title": "Weather", "link": "https://www.nbcnewyork.com/tag/weather/"}, {"title": "Weather Alerts", "link": "https://www.nbcnewyork.com/weather/severe-weather-alerts/"}, {"title": "School Closings", "link": "https://www.nbcnewyork.com/weather/school-closings/"}], "position": 9}, {"title": "New York City, NY Hourly Weather Forecast", "link": "https://www.wunderground.com/hourly/us/ny/new-york-city", "snippet": "New York City, NY Hourly Weather Forecaststar_ratehome ; 1:00 pm, Cloudy, 46 \u00b0F ; 2:00 pm, Cloudy, 47 \u00b0F ; 3:00 pm, Cloudy, 48 \u00b0F ; 4:00 pm, Cloudy, 48 \u00b0F ...", "position": 10}]
STEP: https://oaidalleapiprodscus.blob.core.windows.net/private/org-OIsLdcbHIc1F6swhIoWbkFsS/user-dvcNG9YUCv8srEVF1LN250bv/img-EniOzeTyLedEnBrZmo6jh0M7.png?st=2024-04-18T18%3A30%3A43Z&se=2024-04-18T20%3A30%3A43Z&sp=r&sv=2021-08-06&sr=b&rscd=inline&rsct=image/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2024-04-17T22%3A37%3A08Z&ske=2024-04-18T22%3A37%3A08Z&sks=b&skv=2021-08-06&sig=PJUQuHsp52omwe6DXECOQK2M1pVj%2B9tW3LikbTkX2uE%3D
THOUGHT: I've generated an image representing the current weather conditions in New York as you requested. You can view the image by clicking the link below:

[View Image](https://oaidalleapiprodscus.blob.core.windows.net/private/org-OIsLdcbHIc1F6swhIoWbkFsS/user-dvcNG9YUCv8srEVF1LN250bv/img-EniOzeTyLedEnBrZmo6jh0M7.png?st=2024-04-18T18%3A30%3A43Z&se=2024-04-18T20%3A30%3A43Z&sp=r&sv=2021-08-06&sr=b&rscd=inline&rsct=image/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2024-04-17T22%3A37%3A08Z&ske=2024-04-18T22%3A37%3A08Z&sks=b&skv=2021-08-06&sig=PJUQuHsp52omwe6DXECOQK2M1pVj%2B9tW3LikbTkX2uE%3D)
agent run ID 1 finished!
Done

What’s Next?

Congratulations on your own on-chain Agent! Explore further:

  • Implement a more advanced agent by adding retrieval-augmented generation to your contract.
  • Dive deeper into the Galadriel documentation, particularly the How It Works section, to understand the underlying technology.
  • Explore other Use Cases to get inspired for your next project.

Happy building!