-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagent.sol
More file actions
433 lines (366 loc) · 17.5 KB
/
agent.sol
File metadata and controls
433 lines (366 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
// ========= HIGH LEVEL INTERFACES ========
interface IERCAgentTool {
/// @notice describes the various types of input parameters a tool can have.
/// Right now, only primitive types are supported.
enum ParamType {
STRING,
ADDRESS,
BOOL,
INT,
UINT,
STRING_ARRAY,
ADDRESS_ARRAY,
BOOL_ARAY,
INT_ARRAY,
UINT_ARRAY
}
/// @notice Describes all parameters a tool expects as input.
struct InputDescription {
ParamDescription[] paramDescriptions;
}
/// @notice Describes a single parameter for a tool.
struct ParamDescription {
ParamType paramType;
string name;
string description;
}
/// @notice Describes a parameter that was generated by the agent for a tool.
struct ParamValue {
ParamType paramType;
bytes value;
}
/// @notice Describes all parameters that was generated by the agent for a tool.
struct Input {
/// @notice the generated parameters for the tool in order, based on the InputDescription.
/// You can use this when you want to do custom checks, or overrides before passing them to the tool.
ParamValue[] params;
/// @notice the generated parameter values in abi-encoded format, based on the InputDescription.
/// You can use this when the InputDescription matches with the function signature 1:1.
bytes abiEncodedParams;
}
/// @notice Returns the name of this tool, should be short and meaningful.
/// @return Name of the tool.
function name() external view returns (string memory);
/// @notice Returns the description of this tool, when it should be used
/// and what it does at a high level.
/// @return Description of the tool.
function description() external view returns (string memory);
/// @notice Returns the type and rough format of the input the tool expects.
/// Should be a succint description of what clients need to provide when
/// calling this tool.
/// @return InputDescription of the input to this tool.
function inputDescription() external view returns (InputDescription memory);
/// @notice Runs the tool with the given input and returns its final answer
/// to the given task.
/// @param input The input to this tool that is generated using the inputDescription.
/// @param resultHandler The contract that will receive the result of this tool execution,
/// must support the IERCAgentToolClient interface.
/// @return runId when the tool's execution is synchronous, the runId will be -1.
/// When it is asynchronous, a non-negative runId will be returned that will be passed to
/// the result handler once the operation is ready.
/// @return result only present when the tool was executed synchronously and runId is -1.
/// Do not use unless the runId returned was -1.
function run(Input memory input, address resultHandler) external returns (int256 runId, string memory result);
}
/// ========= SYNCHRONOUS AGENT IMPLEMENTATION ==========
/// @notice Implements a synchronous agent that's backed by an on-chain agentExecutor contract
abstract contract IERCAgent is IERCAgentTool {
/// @notice Logged when the agent completes an execution triggered by calling run.
/// @param runId the ID of the run
/// @param executionSteps contains any additional set of details about
/// what actions the agent took and how it completed the task, in order.
/// May be empty.
/// @param requester the address that requested this execution
/// @param answer the answer returned by the agent
event AgentRunResult(
int256 indexed runId,
address requester,
string[] executionSteps,
string answer);
address agentExecutorContract;
string modelId;
string agentName;
string agentDescription;
string basePrompt;
IERCAgentTool.InputDescription agentInputDescription;
IERCAgentTool[] tools;
uint16 agentMaxIterations;
int256 currentRunId;
/// @notice Creates a new agent
/// @param _agentExecutorContract points to a contract that implements IERCAgentExecutor
/// @param _modelId an identifier for the model that should be used for agent execution
/// can be an ID, name, or hash, depending on what the IERCAgentExecutor implementation details
/// @param _name the name of the agent
/// @param _description the description of the agent
/// @param _basePrompt the base prompt for the agent that describes its task to the LLM
/// @param _tools the tools the agent should have access to
/// @param _agentMaxIterations the maximum number of iterations an agent should take
/// when running a particular task. One iteration includes one use of a tool. Use
/// this to put an upper limit on the runtime of the agent in case it gets stuck.
constructor(
address _agentExecutorContract,
string memory _modelId,
string memory _name,
string memory _description,
string memory _inputDescription,
string memory _basePrompt,
IERCAgentTool[] memory _tools,
uint16 _agentMaxIterations
) {
agentExecutorContract = _agentExecutorContract;
modelId = _modelId;
agentName = _name;
agentDescription = _description;
basePrompt = _basePrompt;
IERCAgentTool.ParamDescription memory promptParam =
IERCAgentTool.ParamDescription(IERCAgentTool.ParamType.STRING, "prompt", _inputDescription);
IERCAgentTool.ParamDescription[] memory params =
new IERCAgentTool.ParamDescription[](1);
params[0] = promptParam;
agentInputDescription = IERCAgentTool.InputDescription(params);
tools = _tools;
agentMaxIterations = _agentMaxIterations;
currentRunId = 0;
}
/// @notice Returns the name of this agent, should be short and meaningful.
/// @return The name of the agent.
function name() external view returns (string memory) {
return agentName;
}
/// @notice Returns the description of this agent, when it should be used
/// and what it does at a high level.
/// @return The description of the agent.
function description() external view returns (string memory) {
return agentDescription;
}
/// @notice Returns the type and rough format of the input the agent expects.
/// Should be a succint description of what clients need to provide when
/// calling this agent.
/// @return InputDescription of the input this agent.
function inputDescription() external view returns (IERCAgentTool.InputDescription memory) {
return agentInputDescription;
}
/// @notice Runs the agent with the given input and returns its final answer
/// to the given task.
/// @param input The input to this agent that is generated using the inputDescription.
/// @param resultHandler The contract that will receive the result of this agent execution,
/// must support the IERCAgentClient interface.
/// @return runId for this request that will be used when providing the result
/// through the client interface.
/// @return result, only present when the tool was executed synchronously and runId is -1.
/// Do not use unless the runId returned was -1.
function run(IERCAgentTool.Input memory input, address resultHandler) external virtual returns (int256, string memory) {
require(input.params.length == 1, "Agent always expects a single parameter");
IERCAgentExecutor agentExecutor = IERCAgentExecutor(agentExecutorContract);
string[] memory agentReasoning = new string[](agentMaxIterations);
(string memory prompt) = abi.decode(input.params[0].value, (string));
currentRunId++;
uint16 currentIteration = 0;
for (; currentIteration < agentMaxIterations; currentIteration++) {
AgentIterationResult memory iterationResult = agentExecutor.runNextIteration(
modelId,
basePrompt,
tools,
agentReasoning,
prompt
);
if (iterationResult.isFinalAnswer) {
// agent is done, emit event and return answer
string[] memory executionSteps = new string[](currentIteration);
for (uint i = 0; i < currentIteration; i++) {
executionSteps[i] = agentReasoning[i];
}
emit AgentRunResult(
currentRunId,
msg.sender,
executionSteps,
iterationResult.finalAnswer
);
return (-1, iterationResult.finalAnswer);
} else {
IERCAgentTool tool = iterationResult.tool;
(bool success, bytes memory returnValue) = address(tool).call(abi.encodeWithSelector(
IERCAgentTool.run.selector, iterationResult.toolInput.abiEncodedParams));
require(success, "Tool call failed");
(int256 runId, string memory result) = abi.decode(returnValue, (int256, string));
require(runId == -1, "Only synchronous tools supported");
agentReasoning[currentIteration] = string.concat(
iterationResult.agentReasoning,
"Observation: ",
result,
"\n");
}
}
revert("Agent failed to produce final answer within agentMaxIterations");
}
}
/// @notice Used to receive the result of asynchronous agent executions
interface IERCAgentClient {
/// @notice Used to pass the result of the agent invocation back to the caller.
/// @dev implementations must verify that the sender of this message is the agent
/// that they originally issued the request for
/// @param runId the runId that was returned by the run call of agent
/// @param result the final answer and result of the requested task from the agent
function handleAgentResult(int256 runId, string memory result) external;
/// @notice check supported interfaces, adhereing to ERC165.
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
/// @notice represents the result of a single iteration of the agent reasoning loop
struct AgentIterationResult {
bool isFinalAnswer;
string finalAnswer; // only present when isFinalAnswer == true
// below are only present when isFinalAnswer == false
IERCAgentTool tool;
IERCAgentTool.Input toolInput;
string agentReasoning;
}
// can be implemented as a precompile
interface IERCAgentExecutor {
function runNextIteration(
string memory modelId,
string memory basePrompt,
IERCAgentTool[] memory tools,
string[] memory agentReasoning,
string memory prompt) external returns (AgentIterationResult memory);
}
// ======= EXAMPLE AGENT AND TOOLS ==========
/// @notice interface for turning a contract function's return value into a human-readable string
/// so the agent can understand what the tool's execution resulted in.
interface ToolResultConverter {
function convertToString(bytes memory result) external returns (string memory);
}
/// @notice Simple wrapper tool implementation that can be used to expose simple functions
/// on smart contracts as tools.
contract SimpleSmartContractTool is IERCAgentTool {
string toolName;
string toolDescription;
address toolContract;
bytes4 toolSelector;
IERCAgentTool.InputDescription toolInputDescription;
ToolResultConverter toolResultConverter;
bool useStaticResult;
string staticResult;
// this implementation only supports singular return types
IERCAgentTool.ParamType outputType;
/// @notice Creates a new tool.
/// @param _name of the tool
/// @param _description of the tool
/// @param _toolContract address of the contract that implements the tool
/// @param _toolSelector selector of the function on _toolContract that the tool should use
/// @param _inputDescription must contain all parameters of the function _toolSelector is pointing
/// to. Right now, only supports primitive data types.
/// @param _useStaticResult when true, the tool invocation will return a static result string to the
/// agent.
/// @param _staticResult the result string to return when _useStaticResult is set to true.
/// Otherwise ignored.
/// @param _toolResultConverter when _useStaticResult is false, the return bytes from the contract
/// invocation will be passed to this converter to turn it into a human-readable format for the agent
/// to be able to reason about the result.
constructor(
string memory _name,
string memory _description,
address _toolContract,
bytes4 _toolSelector,
IERCAgentTool.InputDescription memory _inputDescription,
bool _useStaticResult,
string memory _staticResult,
ToolResultConverter _toolResultConverter
) {
toolName = _name;
toolDescription = _description;
toolContract = _toolContract;
toolSelector = _toolSelector;
toolInputDescription = _inputDescription;
toolResultConverter = _toolResultConverter;
useStaticResult = _useStaticResult;
staticResult = _staticResult;
}
function name() external view returns (string memory) {
return toolName;
}
function description() external view returns (string memory) {
return toolDescription;
}
function inputDescription() external view returns (IERCAgentTool.InputDescription memory) {
return toolInputDescription;
}
function run(IERCAgentTool.Input memory input, address resultHandler) external virtual returns (int256, string memory) {
bytes memory callData = abi.encodeWithSelector(toolSelector, input.abiEncodedParams);
(bool success, bytes memory returnValue) = toolContract.call(callData);
require(success, "Tool call failed");
if (useStaticResult == true) {
return (-1, staticResult);
} else {
return (-1, toolResultConverter.convertToString(returnValue));
}
}
}
// demo contract
interface Pool {
function deploy(address asset, uint256 amount) external;
function withdraw(address asset, uint256 amount) external;
function balance(address asset) external returns (uint256);
}
// import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
contract ViewBalanceResultConverter is ToolResultConverter {
function convertToString(bytes memory result) external override returns (string memory) {
(uint256 balance) = abi.decode(result, (uint256));
return string.concat("The balance is: ", Strings.toString(balance));
}
}
/// @notice demo agent
contract WalletAgent is IERCAgent {
constructor() IERCAgent(
address(0x19),
"llama_model_3",
"Wallet Agent",
"Use this to deploy or withdraw tokens from a liquidity pool",
"The action you want to take, the address of the tokens, and the amount",
string(abi.encodePacked(
"You are an agent deployed on an Ethereum blockchain, responsible for managing a user's wallet. ",
"The wallet's owner will give you instructons in simple terms, ",
"and your goal is to execute the instructions from the user, given the list of tools you can use...")),
new IERCAgentTool[](4),
10
) {
ParamDescription[] memory deployParams = new ParamDescription[](2);
deployParams[0] = ParamDescription(ParamType.ADDRESS, "asset", "address of the token to deposit");
deployParams[1] = ParamDescription(ParamType.INT, "amount", "amount of tokens to deposit");
tools[0] = new SimpleSmartContractTool(
"Deploy",
"Deploy funds into the pool",
address(0x123),
Pool.deploy.selector,
IERCAgentTool.InputDescription(deployParams),
true,
"Successfully deployed",
ToolResultConverter(address(0)));
ParamDescription[] memory withdrawParams = new ParamDescription[](2);
withdrawParams[0] = ParamDescription(ParamType.ADDRESS, "asset", "address of the token to withdraw");
withdrawParams[1] = ParamDescription(ParamType.INT, "amount", "amount of tokens to withdraw");
tools[1] = new SimpleSmartContractTool(
"Withdraw",
"Withdraw funds from the pool",
address(0x123),
Pool.withdraw.selector,
IERCAgentTool.InputDescription(withdrawParams),
true,
"Successfully withdrawn",
ToolResultConverter(address(0)));
ParamDescription[] memory viewBalanceParams = new ParamDescription[](1);
viewBalanceParams[0] = ParamDescription(ParamType.ADDRESS, "asset", "address of the token to view balance for");
tools[2] = new SimpleSmartContractTool(
"ViewBalance",
"See user's balance in the pool",
address(0x123),
Pool.balance.selector,
IERCAgentTool.InputDescription(viewBalanceParams),
false,
"",
new ViewBalanceResultConverter());
// reusing existing tool
tools[3] = IERCAgentTool(address(0x12));
}
}