Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anthropic support #112

Merged
merged 24 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/e2e_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
name: e2e tests
on:
pull_request:
branches:
- main
jobs:
e2e-tests:
runs-on: ubuntu-latest
env:
NETWORK: "localhost"
ORACLE_ADDRESS: "0x5FbDB2315678afecb367f032d93F642f64180aa3"
TEST_CONTRACT_ADDRESS: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788"
TEST_CONTRACT_ADDRESS: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e"
IMAGE_URL: "https://picsum.photos/200/300"
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -71,6 +69,7 @@ jobs:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
PINATA_GATEWAY_TOKEN: ${{ secrets.PINATA_GATEWAY_TOKEN }}
PINATA_API_JWT: ${{ secrets.PINATA_API_JWT }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- name: "OpenAI gpt-4-turbo-preview"
run:
cd contracts && npx hardhat openai --contract-address ${{ env.TEST_CONTRACT_ADDRESS }} --model gpt-4-turbo-preview --message "Who is the president of USA?" --network ${{ env.NETWORK }}
Expand Down Expand Up @@ -111,6 +110,11 @@ jobs:
cd contracts && npx hardhat groq --contract-address ${{ env.TEST_CONTRACT_ADDRESS }} --model gemma-7b-it --message "Who is the president of USA?" --network ${{ env.NETWORK }}
env:
PRIVATE_KEY_LOCALHOST: ${{ secrets.PRIVATE_KEY }}
- name: "Anthropic claude-3-5-sonnet-20240620"
run:
cd contracts && npx hardhat llm --contract-address ${{ env.TEST_CONTRACT_ADDRESS }} --model claude-3-5-sonnet-20240620 --message "Who is the president of USA?" --network ${{ env.NETWORK }}
env:
PRIVATE_KEY_LOCALHOST: ${{ secrets.PRIVATE_KEY }}
- name: "OpenAI Image Generation"
run:
cd contracts && npx hardhat image_generation --contract-address ${{ env.TEST_CONTRACT_ADDRESS }} --query "Red rose" --network ${{ env.NETWORK }}
Expand Down
266 changes: 266 additions & 0 deletions contracts/contracts/AnthropicChatGpt.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

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

// @title ChatGpt
// @notice This contract interacts with teeML oracle to handle chat interactions using the Anthropic model.
contract AnthropicChatGpt {

struct ChatRun {
address owner;
IOracle.Message[] messages;
uint messagesCount;
}

// @notice Mapping from chat ID to ChatRun
mapping(uint => ChatRun) public chatRuns;
uint private chatRunsCount;

// @notice Event emitted when a new chat is created
event ChatCreated(address indexed owner, uint indexed chatId);

// @notice Address of the contract owner
address private owner;

// @notice Address of the oracle contract
address public oracleAddress;

// @notice Configuration for the LLM request
IOracle.LlmRequest private config;

// @notice CID of the knowledge base
string public knowledgeBase;

// @notice Mapping from chat ID to the tool currently running
mapping(uint => string) public toolRunning;

// @notice Event emitted when the oracle address is updated
event OracleAddressUpdated(address indexed newOracleAddress);

// @param initialOracleAddress Initial address of the oracle contract
constructor(address initialOracleAddress) {
owner = msg.sender;
oracleAddress = initialOracleAddress;

config = IOracle.LlmRequest({
model : "claude-3-5-sonnet-20240620",
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\":\"code_interpreter\",\"description\":\"Evaluates python code in a sandbox environment. The environment resets on every execution. You must send the whole script every time and print your outputs. Script should be pure python code that can be evaluated. It should be in python format NOT markdown. The code should NOT be wrapped in backticks. All python packages including requests, matplotlib, scipy, numpy, pandas, etc are available. Output can only be read from stdout, and stdin. Do not use things like plot.show() as it will not work. print() any output and results so you can capture the output.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"The pure python script to be evaluated. The contents will be in main.py. It should not be in markdown format.\"}},\"required\":[\"code\"]}}}]",
toolChoice : "auto", // "none" or "auto"
user : "" // null
});
}

// @notice Ensures the caller is the contract owner
modifier onlyOwner() {
require(msg.sender == owner, "Caller is not owner");
_;
}

// @notice Ensures the caller is the oracle contract
modifier onlyOracle() {
require(msg.sender == oracleAddress, "Caller is not oracle");
_;
}

// @notice Sets a new oracle address
// @param newOracleAddress The new oracle address
function setOracleAddress(address newOracleAddress) public onlyOwner {
oracleAddress = newOracleAddress;
emit OracleAddressUpdated(newOracleAddress);
}

// @notice Starts a new chat
// @param message The initial message to start the chat with
// @return The ID of the newly created chat
function startChat(string memory message) public returns (uint) {
ChatRun storage run = chatRuns[chatRunsCount];

run.owner = msg.sender;
IOracle.Message memory newMessage = IOracle.Message({
role: "user",
content: new IOracle.Content[](1)
});
newMessage.content[0].contentType = "text";
newMessage.content[0].value = message;
run.messages.push(newMessage);
run.messagesCount++;

uint currentId = chatRunsCount;
chatRunsCount++;

IOracle(oracleAddress).createLlmCall(currentId, config);
emit ChatCreated(msg.sender, currentId);

return currentId;
}

// @notice Handles the response from the oracle for an LLM call
// @param runId The ID of the chat run
// @param response The response from the oracle
// @dev Called by teeML oracle
function onOracleLlmResponse(
uint runId,
IOracle.LlmResponse memory response,
string memory errorMessage
) public onlyOracle {
ChatRun storage run = chatRuns[runId];
require(
keccak256(abi.encodePacked(run.messages[run.messagesCount - 1].role)) == keccak256(abi.encodePacked("user")),
"No message to respond to"
);

if (!compareStrings(errorMessage, "")) {
IOracle.Message memory newMessage = IOracle.Message({
role: "assistant",
content: new IOracle.Content[](1)
});
newMessage.content[0].contentType = "text";
newMessage.content[0].value = errorMessage;
run.messages.push(newMessage);
run.messagesCount++;
} else {
if (!compareStrings(response.functionName, "")) {
toolRunning[runId] = response.functionName;
IOracle(oracleAddress).createFunctionCall(runId, response.functionName, response.functionArguments);
} else {
toolRunning[runId] = "";
}
IOracle.Message memory newMessage = IOracle.Message({
role: "assistant",
content: new IOracle.Content[](1)
});
newMessage.content[0].contentType = "text";
newMessage.content[0].value = response.content;
run.messages.push(newMessage);
run.messagesCount++;
}
}

// @notice Handles the response from the oracle for a function call
// @param runId The ID of the chat run
// @param response The response from the oracle
// @param errorMessage Any error message
// @dev Called by teeML oracle
function onOracleFunctionResponse(
uint runId,
string memory response,
string memory errorMessage
) public onlyOracle {
require(
!compareStrings(toolRunning[runId], ""),
"No function to respond to"
);
ChatRun storage run = chatRuns[runId];
if (compareStrings(errorMessage, "")) {
IOracle.Message memory newMessage = IOracle.Message({
role: "user",
content: new IOracle.Content[](1)
});
newMessage.content[0].contentType = "text";
newMessage.content[0].value = response;
run.messages.push(newMessage);
run.messagesCount++;
IOracle(oracleAddress).createLlmCall(runId, config);
}
}

// @notice Handles the response from the oracle for a knowledge base query
// @param runId The ID of the chat run
// @param documents The array of retrieved documents
// @dev Called by teeML oracle
function onOracleKnowledgeBaseQueryResponse(
uint runId,
string[] memory documents,
string memory /*errorMessage*/
) public onlyOracle {
ChatRun storage run = chatRuns[runId];
require(
keccak256(abi.encodePacked(run.messages[run.messagesCount - 1].role)) == keccak256(abi.encodePacked("user")),
"No message to add context to"
);
// Retrieve the last user message
IOracle.Message storage lastMessage = run.messages[run.messagesCount - 1];

// Start with the original message content
string memory newContent = lastMessage.content[0].value;

// Append "Relevant context:\n" only if there are documents
if (documents.length > 0) {
newContent = string(abi.encodePacked(newContent, "\n\nRelevant context:\n"));
}

// Iterate through the documents and append each to the newContent
for (uint i = 0; i < documents.length; i++) {
newContent = string(abi.encodePacked(newContent, documents[i], "\n"));
}

// Finally, set the lastMessage content to the newly constructed string
lastMessage.content[0].value = newContent;

// Call LLM
IOracle(oracleAddress).createLlmCall(runId, config);
}

// @notice Adds a new message to an existing chat run
// @param message The new message to add
// @param runId The ID of the chat run
function addMessage(string memory message, uint runId) public {
ChatRun storage run = chatRuns[runId];
require(
keccak256(abi.encodePacked(run.messages[run.messagesCount - 1].role)) == keccak256(abi.encodePacked("assistant")),
"No response to previous message"
);
require(
run.owner == msg.sender, "Only chat owner can add messages"
);

IOracle.Message memory newMessage = IOracle.Message({
role: "user",
content: new IOracle.Content[](1)
});
newMessage.content[0].contentType = "text";
newMessage.content[0].value = message;
run.messages.push(newMessage);
run.messagesCount++;
// If there is a knowledge base, create a knowledge base query
if (bytes(knowledgeBase).length > 0) {
IOracle(oracleAddress).createKnowledgeBaseQuery(
runId,
knowledgeBase,
message,
3
);
} else {
// Otherwise, create an LLM call
IOracle(oracleAddress).createLlmCall(runId, config);
}
}

// @notice Retrieves the message history of a chat run
// @param chatId The ID of the chat run
// @return An array of messages
// @dev Called by teeML oracle
function getMessageHistory(uint chatId) public view returns (IOracle.Message[] memory) {
return chatRuns[chatId].messages;
}

// @notice Compares two strings for equality
// @param a The first string
// @param b The second string
// @return True if the strings are equal, false otherwise
function compareStrings(string memory a, string memory b) private pure returns (bool) {
return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b))));
}
}
42 changes: 42 additions & 0 deletions contracts/contracts/ChatOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ contract ChatOracle is IOracle {
// @dev Default is OpenAI
mapping(uint => string) public promptType;

// @notice Mapping of prompt ID to the LLM configuration
mapping(uint => IOracle.LlmRequest) public llmConfigurations;

// @notice Mapping of prompt ID to the OpenAI configuration
mapping(uint => IOracle.OpenAiRequest) public openAiConfigurations;

Expand Down Expand Up @@ -213,6 +216,45 @@ contract ChatOracle is IOracle {
);
}

// @notice Creates a new LLM call
// @param promptCallbackId The callback ID for the LLM call
// @return The ID of the created prompt
function createLlmCall(uint promptCallbackId, IOracle.LlmRequest memory request) public returns (uint) {
uint promptId = promptsCount;
callbackAddresses[promptId] = msg.sender;
promptCallbackIds[promptId] = promptCallbackId;
isPromptProcessed[promptId] = false;
promptType[promptId] = promptTypes.defaultType;

promptsCount++;

llmConfigurations[promptId] = request;
emit PromptAdded(promptId, promptCallbackId, msg.sender);

return promptId;
}

// @notice Adds a response to a prompt
// @param promptId The ID of the prompt
// @param promptCallBackId The callback ID for the prompt
// @param response The LLM response
// @param errorMessage Any error message
// @dev Called by teeML oracle
function addResponse(
uint promptId,
uint promptCallBackId,
IOracle.LlmResponse memory response,
string memory errorMessage
) public onlyWhitelisted {
require(!isPromptProcessed[promptId], "Prompt already processed");
isPromptProcessed[promptId] = true;
IChatGpt(callbackAddresses[promptId]).onOracleLlmResponse(
promptCallBackId,
response,
errorMessage
);
}

// @notice Marks a prompt as processed
// @param promptId The ID of the prompt
// @dev Called by teeML oracle
Expand Down
Loading
Loading