LogoLogo
  • Learn
    • Introduction
      • AegisAI: Modular AI-Specific Layer 1 Blockchain
      • The Need for Decentralized AI
      • Features of AegisAI
      • Typical Use Cases
      • Vision: Building a Fair and Open AI Ecosystem
    • Overview of AegisAI
      • Dual-layer Architecture
      • Lifecycle of an AI Task
      • PoS+W Inference Concensys
      • Reward Mechanism
      • Composability
      • Staking-equivalent License
  • How-to Guides
    • Run a Node
    • Develop a DeAI Application
      • Quick Start Guide
      • AegisAI for EVM Developers
      • AegisAI for Solana Developers
      • AegisAI RPC API
      • Build Your First AI dApp
  • Reference
    • AegisAI LLM Inference Specifications
  • Community
    • Twitter
    • Telegram
Powered by GitBook
On this page
  • Overview
  • Step 1: Inherit the IRequestCallbackHandler Interface
  • Step 2: Contract Initialization and Security Callback Handling
  • Step 3: Building AI Requests
  • Step 4: Consensus Node Pre-processing Requests
  • Step 5: Callback Process Function
  • Complete Process Example
  1. How-to Guides
  2. Develop a DeAI Application

Build Your First AI dApp

This tutorial aims to guide developers step by step in writing an AegisAI application contract. We'll use AegisAIOracle as an EVM example to demonstrate how to leverage AegisAIEndpoint to implement decentralized AI functionality.

Overview

AegisAIEndpoint serves as a middleware layer responsible for receiving AI requests, forwarding requests to AI nodes for processing through an AI network consensus mechanism, and returning the final results to users. Below, we will build an AegisAI application contract called AegisAIOracle step by step, which processes AI tasks related to data querying.

Step 1: Inherit the IRequestCallbackHandler Interface

First, your application contract needs to inherit the IRequestCallbackHandler interface. This interface defines the process function, which AegisAIEndpoint will call after processing a request to return the results to your contract.

pragma solidity ^0.8.24;

import "./interfaces/IRequestCallbackHandler.sol";

contract AegisAIOracle is IRequestCallbackHandler {

    // ...

}
  • IRequestCallbackHandler: Defines the interface with the process function that your contract must implement.

Step 2: Contract Initialization and Security Callback Handling

In your application contract, you need to initialize necessary parameters, such as the address of the AegisAIEndpoint contract. Additionally, to ensure that only the AegisAIEndpoint contract can call your contract, you need to implement a secure callback handling mechanism.

Initialization

    address public endpoint; // Endpoint contract address

    function initialize(address admin, address _endpoint) external initializer {
        __ReentrancyGuard_init();
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        endpoint = _endpoint;
    }
  • endpoint: The address of the AegisAIEndpoint contract, with which your contract will interact.

Secure Callback Handling

To ensure that only AegisAIEndpoint can call your contract's process function, you need to implement a modifier onlyEndpoint and use it in the process function.

modifier onlyEndpoint() {
    if (msg.sender != endpoint) revert EndpointRequired();
    _;
}

function process(
    ResponsePacket memory resp
) external payable override onlyEndpoint {
    // ...
}

Step 3: Building AI Requests

To enable AegisAIOracle to handle AI requests, you first need to allow building a request. We provide two functions, requestWithPrompts and request, to initiate requests.

Using the requestWithPrompts Function

This function allows you to specify a URL and a set of prompts to more precisely control the behavior of the AI model.

function requestWithPrompts(
    string calldata url,
    string calldata prompts,
    uint64 deadline
) external payable nonReentrant returns (bytes32 requestId) {
    if (bytes(prompts).length == 0) revert EmptyPrompts();
    return _request(url, prompts, deadline);
}
  • url: The URL or context reference for the request, used to specify the data source that the AI model needs to process.

  • prompts: AI instructions or queries that guide the AI model on how to process the data.

  • deadline: The deadline for the request, after which the request will be considered expired.

  • Return value: requestId, a unique identifier for the request.

Using the request Function

This function allows you to specify only a URL, suitable for simple requests that don't need prompts.

function request(
    string calldata url,
    uint64 deadline
) external payable nonReentrant returns (bytes32 requestId) {
    return _request(url, "", deadline);
}
  • url: The URL or context reference for the request, used to specify the data source that the AI model needs to process.

  • deadline: The deadline for the request, after which the request will be considered expired.

  • Return value: requestId, a unique identifier for the request.

Internal Function _request

Both requestWithPrompts and request functions call the internal function _request to create and store requests.

function _request(
    string memory url,
    string memory prompts,
    uint64 deadline
) private returns (bytes32) {
    if (bytes(url).length == 0) revert EmptyURL();
    bytes32 requestId = keccak256(abi.encodePacked(url, prompts));
    if (deadline <= block.timestamp) {
        revert RequestExpired(requestId, deadline, block.timestamp);
    }
    if (
        receiptLookup[requestId].statusCode == StatusCode.REQUEST_PENDING ||
        receiptLookup[requestId].statusCode == StatusCode.REQUEST_SUCCESS
    ) revert ResponseAlreadyExists(requestId);
    receiptLookup[requestId].statusCode = StatusCode.REQUEST_PENDING;
    receiptLookup[requestId].totalValidatorCount = getRoleMemberCount(
        VALIDATOR_ROLE
    );
    // Create and store request
    requestLookup[requestId] = Request({
        sender: msg.sender,
        deadline: deadline,
        url: url,
        prompts: prompts,
        value: msg.value
    });
    emit RequestSent(requestId, requestLookup[requestId]);
    return requestId;
}
  • This function primarily checks parameters and status, then temporarily stores the request and updates the request status.

Call Example

IAegisAIOracle oracle = IAegisAIOracle(oracleAddress);
bytes32 requestId = oracle.requestWithPrompts{value: msg.value}(
    "https://example.com/data",
    "Summarize this article",
    block.timestamp + 3600 // Expires after 1 hour
);

Step 4: Consensus Node Pre-processing Requests

For safer and more accurate request processing, the AegisAIOracle contract has designed a consensus network to pre-process user AI requests and submit them to the AI network once a consensus threshold is reached. This mechanism ensures result reliability and decentralization.

Role of Consensus Nodes

In the AegisAIOracle contract, consensus nodes consist of addresses with VALIDATOR_ROLE permissions:

bytes32 public constant VALIDATOR_ROLE = keccak256("VALIDATOR");

These validators must be authorized to participate in the request processing workflow.

Validator Response Process

Validators process requests by calling the response function:

function response(
    bytes32 requestId,
    string calldata ipfsCid
) external nonReentrant {
    //...
}

Consensus Mechanism

When enough validators respond to the same request, the system triggers the consensus mechanism:

// Check if consensus threshold is reached
uint256 requiredResponses = (getRoleMemberCount(VALIDATOR_ROLE) + 1) /
    2;
if (resp.validators.length >= requiredResponses) {
    resp.isSentRequest = true;
    // ... Submit request to endpoint
}

In the code, after more than half of the validators respond, results can be aggregated and sent to the Endpoint.

Aggregate Results and Submit to AI Network

Once the consensus threshold is reached, the contract aggregates all responses provided by validators and builds a prompt containing all results:

bytes memory prompts = PromptsBuilder.newPrompts(templateIpfsCid);
for (uint256 i = 0; i < resp.ipfsCids.length; ++i) {
    prompts = prompts.appendParam(
        PromptsBuilder.Param({
            paramType: PromptsBuilder.IPFS_CID,
            payload: resp.ipfsCids[i]
        })
    );
}
// Submit request to endpoint
bytes32 endpointRequestHash = IAegisAIEndpoint(endpoint)
    .submitRequest{value: req.value}(
    RequestParams({
        prompts: string(prompts),
        schema: bytes(SCHEMA),
        modelOptions: bytes(MODEL_OPTIONS),
        targetCount: 1,
        refundAddress: req.sender,
        options: new bytes(0)
    })
);

This process primarily includes:

  1. Using PromptsBuilder to create new prompts, adding all IPFS CIDs provided by validators to the prompts.

  2. Calling the submitRequest function of AegisAIEndpoint to send the aggregated request to the AI network.

  3. Recording the endpointRequestHash and updating the request status.

Step 5: Callback Process Function

Next, you need to implement the process function. This function is key to integrating your application contract with AegisAI. After processing a request, AegisAIEndpoint calls this function and passes the results through the ResponsePacket parameter.

struct ResponsePacket {
    bytes32 requestHash;
    bytes payload;
    uint64 status;  // 0 failed, 1 success.
    uint64 confirmations;
}

function process(
    ResponsePacket memory resp
) external payable override onlyEndpoint {
    // 1. Check request status
    if (resp.status == 0) {
        // Request failed, handle error
        // For example, log errors or trigger rollback operations
        // ...
        return;
    }
    // 2. Process successful response
    // Get result data from payload
    string memory ipfsCid = string(resp.payload);
    // ...
}
  • ResponsePacket: Contains response data returned by AegisAIEndpoint. Let's look at each field of ResponsePacket in detail:

  • requestHash: bytes32 type, the request hash returned by AegisAIEndpoint. You can use this hash to track the status of the request.

  • payload: bytes type, contains the result data processed by the AI model. Typically, this data is an IPFS CID pointing to a result file stored on IPFS.

  • status: uint64 type, indicates the status of the request. 0 means the request failed, 1 means the request was successful. Be sure to check this field to ensure the request completed successfully.

  • confirmations: uint64 type, indicates the number of times the request has been confirmed.

Tip: In the process function, first check the value of resp.status. If resp.status is 0, it means the request failed, and you need to handle the error accordingly, such as logging an error or triggering a rollback. Only when resp.status is 1 can you safely process the data in resp.payload.

Responding to Request Results

When AegisAIEndpoint receives a response from the AI network, it automatically calls the process function you implemented in Step 3. At this point, you can perform a series of validations and verify the data responded to by the AI network:

function process(
    ResponsePacket memory resp
) external payable onlyEndpoint nonReentrant {
    // Verify request validity
    bytes32 requestId = endpointRequestLookup[resp.requestHash];
    if (requestId == bytes32(0)) {
        revert EndpointRequestNotFound(resp.requestHash);
    }
    if (receiptLookup[requestId].requestHash != resp.requestHash)
        revert EndpointRequestNotFound(resp.requestHash);
    
    // Check if response status is successful
    if (resp.status == 0) {
        _closeRequest(
            requestId,
            resp.requestHash,
            "",
            StatusCode.REQUEST_FAILED,
            ErrorCode.INVALID_RESPONSE
        );
        return;
    } 
    // Check if request has expired
    Request storage req = requestLookup[requestId];
    if (block.timestamp > req.deadline) {
        _closeRequest(
            requestId,
            resp.requestHash,
            "",
            StatusCode.REQUEST_FAILED,
            ErrorCode.ENDPOINT_RESPONSE_EXPIRED
        );
        return;
    }
    // Check if payload is empty
    if (resp.payload.length == 0) {
        _closeRequest(
            requestId,
            resp.requestHash,
            "",
            StatusCode.REQUEST_FAILED,
            ErrorCode.INVALID_PAYLOAD
        );
        return;
    }
}

These validations include:

  1. Verifying request validity

  2. Checking if response status is successful (resp.status == 1)

  3. Confirming the request has not expired

  4. Verifying the response payload is not empty

Processing Valid Response Results

If the response passes all validations, the contract extracts the IPFS CID from resp.payload and stores it as the final result:

string memory ipfsCid = string(resp.payload);
_closeRequest(
    requestId,
    resp.requestHash,
    ipfsCid,
    StatusCode.REQUEST_SUCCESS,
    ErrorCode.NONE
);
emit ResponseFinalized(requestId, resp.requestHash, ipfsCid);

The AI network's response typically contains an IPFS CID pointing to the complete results stored on IPFS.

Completing Request Processing

After processing the response, the contract calls the _closeRequest function to clean up request-related storage and update the status:

function _closeRequest(
    bytes32 requestId,
    bytes32 requestHash,
    string memory payload,
    StatusCode statusCode,
    ErrorCode errorCode
) internal {
    receiptLookup[requestId].response = payload;
    receiptLookup[requestId].statusCode = statusCode;
    receiptLookup[requestId].errorCode = errorCode;
    if (errorCode != ErrorCode.NONE) {
        emit ResponseFailed(requestId, errorCode);
    }
    delete requestLookup[requestId];
    delete responseLookup[requestId];
    if (requestHash != bytes32(0)) {
        delete endpointRequestLookup[requestHash];
    }
}

Querying Results

Once a request has been processed, users can query results using the query function:

// Query by request ID
function query(bytes32 requestId) public view returns (string memory) {
    Receipt storage receipt = receiptLookup[requestId];
    if (receipt.statusCode != StatusCode.REQUEST_SUCCESS) {
        revert ResponseUnavailable(requestId, receipt.errorCode);
    }
    return receipt.response;
}

// Query by URL
function query(string calldata url) external view returns (string memory) {
    return query(keccak256(abi.encodePacked(url)));
}

// Query by URL and prompts
function queryWithPrompts(
    string calldata url,
    string calldata prompts
) external view returns (string memory) {
    return query(keccak256(abi.encodePacked(url, prompts)));
}

The contract provides three query methods:

  1. Query directly by request ID

  2. Query by URL (suitable for requests without prompts)

  3. Query by URL and prompts combination (suitable for requests with prompts)

Complete Process Example

Let's illustrate the entire process with an example:

  1. User calls requestWithPrompts to make a request

    bytes32 requestId = oracle.requestWithPrompts{value: 0.01 ether}(
        "https://example.com/data",
        "Analyze the key points of this article",
        block.timestamp + 3600
    );
  2. Validator nodes detect the request, process the data, and call the response function to submit results

    oracle.response(requestId, "QmXyz123..."); // IPFS CID
  3. After reaching the consensus threshold, the request is submitted to AegisAIEndpoint

  4. The AI network processes the request and returns results

  5. AegisAIEndpoint calls the process function to return results to AegisAIOracle

  6. Users can query the processing results

    string memory result = oracle.queryWithPrompts(
        "https://example.com/data",
        "Analyze the key points of this article"
    );
  7. The result is an IPFS CID pointing to a file containing AI-generated content

    // Example return value
    "QmAbcXyz789..."
  8. Client applications can use this CID to retrieve the complete AI-generated content from IPFS

In this way, the AegisAI application implements a decentralized AI request processing workflow, from users initiating requests, to validators pre-processing and reaching consensus, to AI network processing, and finally returning results to the blockchain, forming a complete closed-loop system.

PreviousAegisAI RPC APINextReference

Last updated 2 months ago