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 theprocess
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:
Using
PromptsBuilder
to create new prompts, adding all IPFS CIDs provided by validators to the prompts.Calling the
submitRequest
function ofAegisAIEndpoint
to send the aggregated request to the AI network.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:
Verifying request validity
Checking if response status is successful (
resp.status == 1
)Confirming the request has not expired
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:
Query directly by request ID
Query by URL (suitable for requests without prompts)
Query by URL and prompts combination (suitable for requests with prompts)
Complete Process Example
Let's illustrate the entire process with an example:
User calls
requestWithPrompts
to make a requestbytes32 requestId = oracle.requestWithPrompts{value: 0.01 ether}( "https://example.com/data", "Analyze the key points of this article", block.timestamp + 3600 );
Validator nodes detect the request, process the data, and call the
response
function to submit resultsoracle.response(requestId, "QmXyz123..."); // IPFS CID
After reaching the consensus threshold, the request is submitted to AegisAIEndpoint
The AI network processes the request and returns results
AegisAIEndpoint calls the
process
function to return results to AegisAIOracleUsers can query the processing results
string memory result = oracle.queryWithPrompts( "https://example.com/data", "Analyze the key points of this article" );
The result is an IPFS CID pointing to a file containing AI-generated content
// Example return value "QmAbcXyz789..."
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.
Last updated