├── .env.example ├── .envrc ├── .gitignore ├── README.md ├── package.json ├── public ├── AI-Agent-Contract.jpg ├── ai-agent-architecture.jpg └── powered-by-openai-badge-filled-on-light.svg ├── scripts ├── publish.ts └── setSecrets.ts ├── secrets └── default.json ├── src ├── httpSupport.ts └── index.ts ├── tests ├── test.ts └── testSupport.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | THIRDWEB_API_KEY="aeq16PxRYFMEtM6D3aK4FIM2-BYlUrLyVXkoiUkvbH14ME75ywMr9xBMGGyGrXXr2sY-sFGTQolIjcbqKrXzZA" 2 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export NODE_VERSION_PREFIX=v 2 | export NODE_VERSIONS=~/.nvm/versions/node 3 | use node 20.12.2 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | package-lock.json 6 | typechain 7 | typechain-types 8 | # Default ignored files 9 | /shelf/ 10 | /workspace.xml 11 | # Editor-based HTTP Client requests 12 | /httpRequests/ 13 | 14 | # Hardhat files 15 | cache 16 | artifacts 17 | 18 | # Phat 19 | .phat 20 | dist 21 | polkadot-account.json 22 | 23 | tmp 24 | 25 | logs 26 | secrets 27 | latestDeployment.json 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

AI Agent Contract Template with OpenAI

4 | 5 |
6 |
7 | 8 | 9 |
10 |
11 |

12 | Host your AI Agent Contract on Phala's decentralized serverless cloud. 13 |
14 | Explore the docs » 15 |
16 |
17 | View Demo 18 | · 19 | Report Bug 20 | · 21 | Discord 22 |

23 | 24 |

Architecure Overview

25 | 26 |
27 | 28 | ## 🤖 What Is This?! 29 | 30 |
31 | 32 |
33 | 34 | The OpenAI AI Agent template is a **MINIMAL** template to build an AI Agent that can be hosted on Phala Network's decentralized hosting protocol. Unlike Vercel or other FaaS, it allows you to publish your AI Agent compiled code to IPFS and hosts it on a fully decentralized FaaS cloud with the following benefits: 35 | 36 | - 💨 Ship Fast: Build and ship with familiar toolchain in minutes 37 | - ⛑️ Secure: Execution guarded by rock solid TEE / Intel SGX 38 | - 🔒 Private: Host API keys and user privacy at ease 39 | - 💎 Unstoppable: Powered by IPFS and Phala's 35k+ decentralized TEE workers 40 | 41 | [//]: # () 42 | 43 | ## Getting Started 44 | ### Prepare 45 | Install dependencies 46 | ```shell 47 | npm install 48 | ``` 49 | 50 | ### Testing Locally 51 | Create `.env` file with the default ThirdWeb API key for publishing your Agent Contract to IPFS 52 | ```shell 53 | cp .env.example .env 54 | ``` 55 | 56 | Build your Agent 57 | ```shell 58 | npm run build 59 | ``` 60 | 61 | Test your Agent locally 62 | ```shell 63 | npm run test 64 | ``` 65 | 66 | Expected Test Results 67 | ```shell 68 | INPUT: {"method":"GET","path":"/ipfs/CID","queries":{"chatQuery":["When did humans land on the moon?"],"openAiModel":["gpt-4o"]},"secret":{"openaiApiKey":"YOUR_API_KEY"},"headers":{}} 69 | GET RESULT: { 70 | status: 200, 71 | body: `{"message":"Humans first landed on the moon on July 20, 1969, during NASA's Apollo 11 mission. Astronauts Neil Armstrong and Edwin \\"Buzz\\" Aldrin became the first and second humans to walk on the lunar surface while Michael Collins remained in lunar orbit aboard the Command Module. Neil Armstrong famously declared, \\"That's one small step for man, one giant leap for mankind,\\" as he stepped onto the moon."}`, 72 | headers: { 73 | 'Content-Type': 'application/json', 74 | 'Access-Control-Allow-Origin': '*' 75 | } 76 | } 77 | INPUT: {"method":"POST","path":"/ipfs/CID","queries":{"chatQuery":["When did humans land on the moon?"],"openAiModel":["gpt-4o"]},"secret":{"openaiApiKey":"YOUR_API_KEY"},"headers":{},"body":"{}"} 78 | POST RESULT: { 79 | status: 200, 80 | body: '{"message":"Not Implemented"}', 81 | headers: { 82 | 'Content-Type': 'application/json', 83 | 'Access-Control-Allow-Origin': '*' 84 | } 85 | } 86 | Now you are ready to publish your agent, add secrets, and interact with your agent in the following steps: 87 | - Execute: 'npm run publish-agent' 88 | - Set secrets: 'npm run set-secrets' 89 | - Go to the url produced by setting the secrets (e.g. https://wapo-testnet.phala.network/ipfs/QmPQJD5zv3cYDRM25uGAVjLvXGNyQf9Vonz7rqkQB52Jae?key=b092532592cbd0cf) 90 | ``` 91 | 92 | ### Publish Your AI Agent 93 | Upload your compiled AI Agent code to IPFS. 94 | ```shell 95 | npm run publish-agent 96 | ``` 97 | 98 | Upon a successful upload, the command should show the URL to access your AI Agent. 99 | ```shell 100 | ✓ Compiled successfully. 101 | 76.86 KB dist/index.js 102 | Running command: npx thirdweb upload dist/index.js 103 | This may require you to log into thirdweb and will take some time to publish to IPFS... 104 | 105 | $$\ $$\ $$\ $$\ $$\ 106 | $$ | $$ | \__| $$ | $$ | 107 | $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ 108 | \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ 109 | $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | 110 | $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | 111 | \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | 112 | \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ 113 | 114 | 💎 thirdweb v0.14.12 💎 115 | 116 | - Uploading file to IPFS. This may take a while depending on file sizes. 117 | 118 | ✔ Successfully uploaded file to IPFS. 119 | ✔ Files stored at the following IPFS URI: ipfs://QmdBwRk3YG78thoe9phf43CLAzShYUEiPHnWBu6bkqsJVY 120 | ✔ Open this link to view your upload: https://b805a9b72767504353244e0422c2b5f9.ipfscdn.io/ipfs/bafybeig4up5mjx6kvkm5dizsv2njpkzk7pileis3kfheyd5wff2m3wbtrm/ 121 | 122 | Agent Contract deployed at: https://wapo-testnet.phala.network/ipfs/QmdBwRk3YG78thoe9phf43CLAzShYUEiPHnWBu6bkqsJVY 123 | 124 | If your agent requires secrets, ensure to do the following: 125 | 1) Edit the ./secrets/default.json file or create a new JSON file in the ./secrets folder and add your secrets to it. 126 | 2) Run command: 'npm run set-secrets' or 'npm run set-secrets [path-to-json-file]' 127 | Logs folder created. 128 | Deployment information updated in ./logs/latestDeployment.json 129 | ``` 130 | 131 | > :information_source: Note that your latest deployment information will be logged to in file [`./logs/latestDeployment.json`](./logs/latestDeployment.json). This file is updated every time you publish a new Agent Contract to IPFS. This file is also used to get the IPFS CID of your Agent Contract when setting secrets for your Agent Contract. 132 | > 133 | > Here is an example: 134 | > ```json 135 | > { 136 | > "date": "2024-08-29T20:28:20.081Z", 137 | > "cid": "QmYzBTdQNPewdhD9GdBJ9TdV7LVhrh9YVRiV8aBup7qZGu", 138 | > "url": "https://wapo-testnet.phala.network/ipfs/QmYzBTdQNPewdhD9GdBJ9TdV7LVhrh9YVRiV8aBup7qZGu" 139 | > } 140 | > ``` 141 | 142 |
143 | New to Thirdweb? 144 | We use thirdweb Storage to host IPFS contents. If you are new to thirdweb, the command will guide you to create your account or login to your existing account from the browser. 145 |
146 | 147 | > **Did Thirdweb fail to publish?** 148 | > 149 | > If ThirdWeb fails to publish, please signup for your own ThirdWeb account to publish your Agent Contract to IPFS. Signup or login at https://thirdweb.com/dashboard/ 150 | > 151 | > Whenever you log into ThirdWeb, create a new API key and replace the default API Key with yours in the [.env](./.env) file. 152 | > 153 | > ``` 154 | > THIRDWEB_API_KEY="YOUR_THIRDWEB_API_KEY" 155 | > ``` 156 | 157 | ### Access the Published AI Agent 158 | 159 | Once published, your AI Agent is available at the URL: `https://wapo-testnet.phala.network/ipfs/`. You can get it from the "Publish to IPFS" step. 160 | 161 | You can test it with `curl`. 162 | 163 | ```bash 164 | curl https://wapo-testnet.phala.network/ipfs/ 165 | ``` 166 | 167 | ### Add Secrets 168 | 169 | By default, all the compiled JS code is visible for anyone to view if they look at IPFS CID. This makes private info like API keys, signer keys, etc. vulnerable to be stolen. To protect devs from leaking keys, we have added a field called `secret` in the `Request` object. It allows you to store secrets in a vault for your AI Agent to access. 170 | 171 | To add your secrets, 172 | 1) Edit the [default.json](./secrets/default.json) file or create a new JSON file in the `./secrets` folder and add your secrets to it. 173 | ```json 174 | { 175 | "openaiApiKey": "YOUR_OPENAI_API_KEY" 176 | } 177 | ``` 178 | 2) Run command to set the secrets 179 | ```shell 180 | npm run set-secrets 181 | # or if you have a custom JSON file 182 | npm run set-secrets 183 | ``` 184 | Expected output: 185 | ```shell 186 | Use default secrets... 187 | Storing secrets... 188 | % Total % Received % Xferd Average Speed Time Time Time Current 189 | Dload Upload Total Spent Left Speed 190 | 100 205 0 68 100 137 105 213 --:--:-- --:--:-- --:--:-- 319 191 | {"token":"37a0f3f344a3bbf7","key":"343e2a7dc130fedf","succeed":true} 192 | 193 | Secrets set successfully. Go to the URL below to interact with your agent: 194 | https://wapo-testnet.phala.network/ipfs/QmYzBTdQNPewdhD9GdBJ9TdV7LVhrh9YVRiV8aBup7qZGu?key=343e2a7dc130fedf 195 | Log entry added to secrets.log 196 | ``` 197 | 198 | > :information_source: Note that all your secrets will be logged in file [`./logs/secrets.log`](./logs/secrets.log). This file is updated every time you add new secrets to your Agent Contract. If you have not published an Agent Contract, yet, this command will fail since there is not a CID to map the secrets to. 199 | > 200 | > Here is an example: 201 | > ```text 202 | > 2024-08-29T20:30:35.480Z, CID: [QmYzBTdQNPewdhD9GdBJ9TdV7LVhrh9YVRiV8aBup7qZGu], Token: [37a0f3f344a3bbf7], Key: [343e2a7dc130fedf], URL: [https://wapo-testnet.phala.network/ipfs/QmYzBTdQNPewdhD9GdBJ9TdV7LVhrh9YVRiV8aBup7qZGu?key=343e2a7dc130fedf] 203 | > ``` 204 | 205 | The API returns a `token` and a `key`. The `key` is the id of your secret. It can be used to specify which secret you are going to pass to your frame. The `token` can be used by the developer to access the raw secret. You should never leak the `token`. 206 | 207 | To verify the secret, run the following command where `key` and `token` are replaced with the values from adding your `secret` to the vault. 208 | ```shell 209 | curl https://wapo-testnet.phala.network/vaults// 210 | ``` 211 | 212 | Expected output: 213 | ```shell 214 | {"data":{"openaiApiKey":""},"succeed":true} 215 | ``` 216 | 217 | ### Access Queries 218 | To help create custom logic, we have an array variable named `queries` that can be accessed in the `Request` class. To access the `queries` array variable `chatQuery` value at index `0`, the syntax will look as follows: 219 | ```typescript 220 | const query = req.queries.chatQuery[0] as string; 221 | ``` 222 | The example at https://wapo-testnet.phala.network/ipfs/QmcQbQzJop8m9uqGJTRmxVCEPoWtzx6NkNJRA12CZ6vFyT?key=b7fd2b6c4bf008cc&chatQuery=When%20did%20humans%20land%20on%20the%20moon will have a value of `When did humans land on the moon`. `queries` can have any field name, so `chatQuery` is just an example of a field name and not a mandatory name, but remember to update your `index.ts` file logic to use your expected field name. 223 | 224 | ### Debugging 225 | 226 | To debug your agent, you can use the following command: 227 | ```shell 228 | curl https://wapo-testnet.phala.network/logs/all/ipfs/ 229 | ``` 230 | 231 | After executing this command then you should see some output in the terminal to show the logs of requests to your agent. 232 | ```shell 233 | 2024-09-04T03:18:34.758Z [95f5ec53-3d71-4bb5-bbb6-66065211102c] [REPORT] END Request: Duration: 166ms 234 | 2024-09-04T03:18:34.758Z [95f5ec53-3d71-4bb5-bbb6-66065211102c] [INFO] 'Is signature valid? ' true 235 | 2024-09-04T03:18:34.758Z [95f5ec53-3d71-4bb5-bbb6-66065211102c] [INFO] 'Verifying Signature with PublicKey ' '0xC1BF8dB4D06416c43Aca3deB289CF7CC0aAFF540' 236 | 2024-09-04T03:18:34.758Z [95f5ec53-3d71-4bb5-bbb6-66065211102c] [REPORT] START Request: GET /ipfs/QmfLpQjxAMsppUX9og7xpmfSKZAZ8zuWJV5g42DmpASSWz?key=0e26a64a1e805bfd&type=verify&data=tintinland%20message%20to%20sign&signature=0x34c4d8c83406e7a292ecc940d60b34c9b11024db10a8872c753b9711cd6dbc8f746da8be9bc2ae0898ebf8f49f48c2ff4ba2a851143c3e4b371647eed32f707b1b 237 | 2024-09-04T03:17:15.238Z [768b6fda-f9f1-463f-86bd-a948e002bf80] [REPORT] END Request: Duration: 183ms 238 | 2024-09-04T03:17:15.238Z [768b6fda-f9f1-463f-86bd-a948e002bf80] [INFO] 'Signature: 0x34c4d8c83406e7a292ecc940d60b34c9b11024db10a8872c753b9711cd6dbc8f746da8be9bc2ae0898ebf8f49f48c2ff4ba2a851143c3e4b371647eed32f707b1b' 239 | 2024-09-04T03:17:15.238Z [768b6fda-f9f1-463f-86bd-a948e002bf80] [INFO] 'Signing data [tintinland message to sign] with Account [0xC1BF8dB4D06416c43Aca3deB289CF7CC0aAFF540]' 240 | 2024-09-04T03:17:15.238Z [768b6fda-f9f1-463f-86bd-a948e002bf80] [REPORT] START Request: GET /ipfs/QmfLpQjxAMsppUX9og7xpmfSKZAZ8zuWJV5g42DmpASSWz?key=0e26a64a1e805bfd&type=sign&data=tintinland%20message%20to%20sign 241 | 2024-09-04T03:16:38.507Z [3717d307-bff0-4fc0-bc98-8f66c33dd46f] [REPORT] END Request: Duration: 169ms 242 | 2024-09-04T03:16:38.507Z [3717d307-bff0-4fc0-bc98-8f66c33dd46f] [REPORT] START Request: GET /ipfs/QmfLpQjxAMsppUX9og7xpmfSKZAZ8zuWJV5g42DmpASSWz?key=0e26a64a1e805bfd 243 | 2024-09-04T03:15:00.375Z [793f58f9-f24f-4580-8ebc-04debb7d727f] [REPORT] END Request: Duration: 158ms 244 | 2024-09-04T03:15:00.375Z [793f58f9-f24f-4580-8ebc-04debb7d727f] [REPORT] START Request: GET /ipfs/QmfLpQjxAMsppUX9og7xpmfSKZAZ8zuWJV5g42DmpASSWz?key=0e26a64 245 | a1e805bfd 246 | ``` 247 | To create logs in your Agent Contract, you can use the following syntax in your `index.ts` file. 248 | ```typescript 249 | // info logs 250 | console.log('info log message!') 251 | // error logs 252 | console.error('error log message!') 253 | ``` 254 | For more information check the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/console) on `console` object. 255 | 256 | ## FAQ 257 | 258 |
259 | What packages can I use in the AI Agent server? 260 |
    261 |
  • Most of the npm packages are supported: viem, onchainkit, ….
  • 262 |
  • Some packages with some advanced features are not supported:
  • 263 |
      264 |
    • Memory usage over 100MB
    • 265 |
    • Web Assembly
    • 266 |
    • Browser only features: local storage, service workers, etc
    • 267 |
    268 |
269 |
270 | 271 |
272 | What’s the spec of the Javascript runtime? 273 |
    274 |
  • The code runs inside a tailored QuickJS engine
  • 275 |
  • Available features: ES2023, async, fetch, setTimeout, setInterval, bigint
  • 276 |
  • Tech spec doc
  • 277 |
278 |
279 | 280 |
281 | Why is the serverless platform secure? 282 |
    283 |
  • Your AI Agent code on is fully secure, private, and permissionless. Nobody can manipulate your program, steal any data from it, or censor it.
  • 284 |
  • Security: The code is executed in the decentralized TEE network running on Phala Network. It runs code inside a secure blackbox (called enclave) created by the CPU. It generates cryptographic proofs verifiable on Phala blockchain. It proves that the hosted code is exactly the one you deployed.
  • 285 |
  • Privacy: You can safely put secrets like API keys or user privacy on Phala Network. The code runs inside TEE hardware blackboxs. The memory of the program is fully encrypted by the TEE. It blocks any unauthorized access to your data.
  • 286 |
  • Learn more at Phala Network Homepage
  • 287 |
288 | 289 |
290 | What's TEE / Intel SGX? 291 |
295 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-agent-openai-template", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "engines": { 6 | "node": ">=18" 7 | }, 8 | "scripts": { 9 | "build": "phat-fn build --experimentalAsync", 10 | "test": "tsx tests/test.ts", 11 | "set-secrets": "tsx scripts/setSecrets.ts", 12 | "lint": "tsc --noEmit", 13 | "publish-agent": "phat-fn build --experimentalAsync && tsx scripts/publish.ts" 14 | }, 15 | "devDependencies": { 16 | "@phala/fn": "^0.2.22", 17 | "blakejs": "^1.2.1", 18 | "crypto-js": "^4.2.0", 19 | "dotenv": "^16.4.5", 20 | "js-sha3": "^0.9.3", 21 | "thirdweb": "^5.32.3", 22 | "tsx": "^4.7.1", 23 | "typescript": "^5.3.3", 24 | "wyhash": "^1.0.0" 25 | }, 26 | "dependencies": { 27 | "openai": "^4.56.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/AI-Agent-Contract.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phala-Network/ai-agent-template-openai/d0e9e68074199d92b1e0b7b0c00a3d6ac676822f/public/AI-Agent-Contract.jpg -------------------------------------------------------------------------------- /public/ai-agent-architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phala-Network/ai-agent-template-openai/d0e9e68074199d92b1e0b7b0c00a3d6ac676822f/public/ai-agent-architecture.jpg -------------------------------------------------------------------------------- /public/powered-by-openai-badge-filled-on-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/publish.ts: -------------------------------------------------------------------------------- 1 | // Call Thirdweb upload command to deploy compiled frame 2 | import { spawn } from 'child_process' 3 | import { writeFileSync, existsSync, mkdirSync } from 'fs'; 4 | import 'dotenv/config' 5 | 6 | function ensureLogsFolderExists() { 7 | const logsFolder = './logs'; 8 | if (!existsSync(logsFolder)) { 9 | mkdirSync(logsFolder); 10 | console.log('Logs folder created.'); 11 | } 12 | } 13 | 14 | function updateDeploymentLog(cid: string) { 15 | ensureLogsFolderExists(); 16 | 17 | const gatewayUrl = 'https://wapo-testnet.phala.network'; 18 | const deploymentInfo = { 19 | date: new Date().toISOString(), 20 | cid: cid, 21 | url: `${gatewayUrl}/ipfs/${cid}` 22 | }; 23 | 24 | writeFileSync('./logs/latestDeployment.json', JSON.stringify(deploymentInfo, null, 2), 'utf-8'); 25 | console.log('Deployment information updated in ./logs/latestDeployment.json'); 26 | } 27 | 28 | try { 29 | const gatewayUrl = 'https://wapo-testnet.phala.network' 30 | const command = `npx thirdweb upload dist/index.js -k ${process.env.THIRDWEB_API_KEY}` 31 | const childProcess = spawn(command, { shell: true }) 32 | console.log(`Running command: npx thirdweb upload dist/index.js`) 33 | console.log(`This may require you to log into thirdweb and will take some time to publish to IPFS...`) 34 | childProcess.stdout.on('data', (data) => { 35 | process.stdout.write(data) 36 | }) 37 | 38 | let stderr = '' 39 | childProcess.stderr.on('data', (data) => { 40 | process.stderr.write(data) 41 | stderr += data 42 | }) 43 | 44 | childProcess.on('close', (code) => { 45 | if (code === 0) { 46 | const regex = /ipfs:\/\/([a-zA-Z0-9]+)/; 47 | const match = stderr.match(regex); 48 | 49 | if (match) { 50 | const ipfsCid = match[1]; 51 | console.log(`\nAgent Contract deployed at: ${gatewayUrl}/ipfs/${ipfsCid}`); 52 | console.log(`\nIf your agent requires secrets, ensure to do the following:\n1) Edit the ./secrets/default.json file or create a new JSON file in the ./secrets folder and add your secrets to it.\n2) Run command: 'npm run set-secrets' or 'npm run set-secrets [path-to-json-file]'`); 53 | 54 | // Update the deployment log 55 | updateDeploymentLog(ipfsCid); 56 | } else { 57 | console.log('IPFS CID not found'); 58 | } 59 | } else { 60 | console.log(`Command exited with code ${code}`) 61 | } 62 | }); 63 | } catch (error) { 64 | console.error('Error:', error) 65 | } 66 | -------------------------------------------------------------------------------- /scripts/setSecrets.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | import { readFileSync, appendFileSync } from 'fs' 3 | import 'dotenv/config' 4 | 5 | // Function to read and parse JSON file 6 | function readJsonFile(filePath: string): any { 7 | try { 8 | const fileContents = readFileSync(filePath, 'utf-8') 9 | return JSON.parse(fileContents); 10 | } catch (error) { 11 | console.error('Error reading or parsing JSON file:', error.message) 12 | process.exit(1) 13 | } 14 | } 15 | 16 | // Function to log the details to a file 17 | function logToFile(cid: string, token: string, key: string, url: string) { 18 | const logEntry = `${new Date().toISOString()}, CID: [${cid}], Token: [${token}], Key: [${key}], URL: [${url}]\n`; 19 | appendFileSync('./logs/secrets.log', logEntry, 'utf-8'); 20 | console.log('Log entry added to secrets.log'); 21 | } 22 | 23 | let jsonFilePath = './secrets/default.json'; 24 | 25 | // Get the command-line arguments 26 | const args = process.argv.slice(2); 27 | if (args.length !== 1) { 28 | console.log('Use default secrets...') 29 | } else { 30 | jsonFilePath = args[0]; 31 | } 32 | 33 | // Read and parse the JSON file for secrets and latest deployment info 34 | const secrets = readJsonFile(jsonFilePath); 35 | const latestDeployment = readJsonFile('./logs/latestDeployment.json'); 36 | 37 | try { 38 | const gatewayUrl = 'https://wapo-testnet.phala.network'; 39 | const cid = latestDeployment.cid; 40 | const command = `curl ${gatewayUrl}/vaults -H 'Content-Type: application/json' -d '{"cid": "${cid}", "data": ${JSON.stringify(secrets)}}'`; 41 | const childProcess = spawn(command, { shell: true }) 42 | console.log(`Storing secrets...`) 43 | let stdout = '' 44 | childProcess.stdout.on('data', (data) => { 45 | process.stdout.write(data) 46 | stdout += data 47 | }) 48 | 49 | let stderr = '' 50 | childProcess.stderr.on('data', (data) => { 51 | process.stderr.write(data) 52 | stderr += data 53 | }) 54 | 55 | childProcess.on('close', (code) => { 56 | if (code === 0) { 57 | const regex = /"token":\s*"([a-zA-Z0-9]+)","key":\s*"([a-zA-Z0-9]+)"/; 58 | const match = stdout.match(regex); 59 | 60 | if (match) { 61 | const token = match[1]; 62 | const key = match[2]; 63 | const url = `${gatewayUrl}/ipfs/${cid}?key=${key}`; 64 | console.log(`\n\nSecrets set successfully. Go to the URL below to interact with your agent:`); 65 | console.log(`${url}`); 66 | // Log the details to a file 67 | logToFile(cid, token, key, url); 68 | } else { 69 | console.log('Secrets failed to set'); 70 | } 71 | } else { 72 | console.log(`Command exited with code ${code}`) 73 | } 74 | }); 75 | 76 | } catch (error) { 77 | console.error('Error:', error) 78 | } 79 | -------------------------------------------------------------------------------- /secrets/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "openaiApiKey": "OPENAI_API_KEY" 3 | } 4 | -------------------------------------------------------------------------------- /src/httpSupport.ts: -------------------------------------------------------------------------------- 1 | // DO NOT MODIFY THIS INTERFACE 2 | export interface SerializedRequest { 3 | method: 'GET' | 'POST' | 'PATCH' | 'PUT'; 4 | path: string; 5 | queries: Record; 6 | headers: Record; 7 | body?: string; 8 | secret?: Record; 9 | } 10 | 11 | // DO NOT MODIFY THIS CLASS 12 | export class Request implements SerializedRequest { 13 | method: 'GET' | 'POST' | 'PATCH' | 'PUT'; 14 | path: string; 15 | queries: Record; 16 | headers: Record; 17 | body?: string; 18 | secret?: Record; 19 | constructor(raw: SerializedRequest) { 20 | this.body = raw.body; 21 | this.queries = raw.queries; 22 | this.headers = raw.headers; 23 | this.method = raw.method; 24 | this.path = raw.path; 25 | this.secret = raw.secret; 26 | } 27 | async json(): Promise { 28 | return JSON.parse(this.body!) 29 | } 30 | } 31 | 32 | type ResponseOption = { 33 | status?: number, 34 | headers?: Record 35 | } 36 | export class Response { 37 | status: number; 38 | body?: string; 39 | headers: Record; 40 | constructor(body: string, options?: ResponseOption) { 41 | this.status = options?.status ?? 200; 42 | this.body = body; 43 | this.headers = { 44 | // To change the response type, change the Content-Type header 45 | 'Content-Type': 'application/json', 46 | 'Access-Control-Allow-Origin': '*', 47 | ...options?.headers 48 | } 49 | } 50 | } 51 | 52 | export type RouteConfig = { 53 | GET?: (req: Request) => Promise, 54 | POST?: (req: Request) => Promise, 55 | PATCH?: (req: Request) => Promise, 56 | PUT?: (req: Request) => Promise, 57 | } 58 | 59 | export async function route(config: RouteConfig, request: string) { 60 | const reqObj: SerializedRequest = JSON.parse(request) 61 | let response: Response; 62 | const method = reqObj.method 63 | const req = new Request(reqObj) 64 | if (method == 'GET' && config.GET) { 65 | response = await config.GET(req); 66 | } else if (method == 'POST' && config.POST) { 67 | response = await config.POST(req); 68 | } else if (method == 'PATCH' && config.PATCH) { 69 | response = await config.PATCH(req); 70 | } else if (method == 'PUT' && config.PUT) { 71 | response = await config.PUT(req); 72 | } else { 73 | response = new Response('Not Found'); 74 | response.status = 404 75 | } 76 | return JSON.stringify(response) 77 | } 78 | 79 | // Only works for ascii string 80 | export function stringToHex(str: string): string { 81 | let hex = ''; 82 | for (let i = 0; i < str.length; i++) { 83 | hex += str.charCodeAt(i).toString(16).padStart(2, '0'); 84 | } 85 | return '0x' + hex; 86 | } 87 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, route } from './httpSupport' 2 | 3 | import OpenAI from 'openai' 4 | 5 | async function GET(req: Request): Promise { 6 | let result = { message: '' } 7 | const secrets = req.secret || {} 8 | const queries = req.queries 9 | const openaiApiKey = (secrets.openaiApiKey) ? secrets.openaiApiKey as string : '' 10 | const openai = new OpenAI({ apiKey: openaiApiKey }) 11 | // Choose from any model listed here https://platform.openai.com/docs/models 12 | const openAiModel = (queries.openAiModel) ? queries.openAiModel[0] : 'gpt-4o'; 13 | const query = (queries.chatQuery) ? queries.chatQuery[0] as string : 'Who are you?' 14 | 15 | const completion = await openai.chat.completions.create({ 16 | messages: [{ role: "system", content: `${query}` }], 17 | model: `${openAiModel}`, 18 | }) 19 | 20 | result.message = (completion.choices) ? completion.choices[0].message.content as string : 'Failed to get result' 21 | 22 | return new Response(JSON.stringify(result)) 23 | } 24 | 25 | async function POST(req: Request): Promise { 26 | return new Response(JSON.stringify({message: 'Not Implemented'})) 27 | } 28 | 29 | export default async function main(request: string) { 30 | return await route({ GET, POST }, request) 31 | } 32 | -------------------------------------------------------------------------------- /tests/test.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import './testSupport' 3 | import {execute} from "./testSupport"; 4 | 5 | async function test() { 6 | const getResult = await execute({ 7 | method: 'GET', 8 | path: '/ipfs/CID', 9 | queries: { 10 | chatQuery: ["Who are you?"], 11 | // Choose from any model listed here https://platform.openai.com/docs/models 12 | model: ["gpt-4o"] 13 | }, 14 | secret: { openaiApiKey: 'YOUR_OPENAI_API_KEY' }, 15 | headers: {}, 16 | }) 17 | console.log('GET RESULT:', JSON.parse(getResult)) 18 | 19 | console.log(`Now you are ready to publish your agent, add secrets, and interact with your agent in the following steps:\n- Execute: 'npm run publish-agent'\n- Set secrets: 'npm run set-secrets'\n- Go to the url produced by setting the secrets (e.g. https://wapo-testnet.phala.network/ipfs/QmPQJD5zv3cYDRM25uGAVjLvXGNyQf9Vonz7rqkQB52Jae?key=b092532592cbd0cf)`) 20 | } 21 | 22 | test().then(() => { }).catch(err => console.error(err)).finally(() => process.exit()) 23 | -------------------------------------------------------------------------------- /tests/testSupport.ts: -------------------------------------------------------------------------------- 1 | /* DO NOT MODIFY THIS FILE */ 2 | import main from "../src"; 3 | import * as blake from 'blakejs'; 4 | import * as CryptoJS from 'crypto-js'; 5 | import { keccak256 } from 'js-sha3'; 6 | import { wyhash } from 'wyhash'; 7 | 8 | // Mock function to simulate getting JavaScript code 9 | function getJsCode(): string[] { 10 | // Replace this with actual code fetching logic 11 | return [ 12 | "console.log('Hello, world!');", 13 | "function add(a, b) { return a + b; }" 14 | ]; 15 | } 16 | 17 | // Function to hash a message using Blake2x256 18 | function hashBlake2x256(message: Uint8Array): Uint8Array { 19 | return blake.blake2b(message, undefined, 32); 20 | } 21 | 22 | // Function to get the hash of the JavaScript code 23 | function getJsCodeHash(): Uint8Array { 24 | const codes = getJsCode(); 25 | const output = new Uint8Array(32 * codes.length); 26 | let offset = 0; 27 | 28 | for (const code of codes) { 29 | const codeHash = hashBlake2x256(new TextEncoder().encode(code)); 30 | output.set(codeHash, offset); 31 | offset += 32; 32 | } 33 | 34 | return hashBlake2x256(output); 35 | } 36 | 37 | // @ts-ignore 38 | globalThis.Wapo = { 39 | deriveSecret(salt: Uint8Array | string): Uint8Array { 40 | let saltBytes: Uint8Array; 41 | 42 | // Convert string salt to Uint8Array 43 | if (typeof salt === 'string') { 44 | saltBytes = new TextEncoder().encode(salt); 45 | } else { 46 | saltBytes = salt; 47 | } 48 | 49 | const prefix = new TextEncoder().encode('JavaScript:'); 50 | const jsCodeHash = getJsCodeHash(); 51 | const seed = new Uint8Array(prefix.length + jsCodeHash.length + saltBytes.length); 52 | 53 | seed.set(prefix, 0); 54 | seed.set(jsCodeHash, prefix.length); 55 | seed.set(saltBytes, prefix.length + jsCodeHash.length); 56 | 57 | // Derive a secret using Blake2x256 58 | return hashBlake2x256(seed); 59 | }, 60 | hash(algorithm: 'blake2b128' | 'blake2b256' | 'blake2b512' | 'sha256' | 'keccak256', message: Uint8Array | string): Uint8Array { 61 | let messageBytes: Uint8Array; 62 | 63 | // Convert string message to Uint8Array 64 | if (typeof message === 'string') { 65 | messageBytes = new TextEncoder().encode(message); 66 | } else { 67 | messageBytes = message; 68 | } 69 | 70 | switch (algorithm) { 71 | case 'blake2b128': 72 | return blake.blake2b(messageBytes, undefined, 16); 73 | case 'blake2b256': 74 | return blake.blake2b(messageBytes, undefined, 32); 75 | case 'blake2b512': 76 | return blake.blake2b(messageBytes, undefined, 64); 77 | case 'sha256': 78 | return Uint8Array.from(CryptoJS.SHA256(CryptoJS.lib.WordArray.create(messageBytes)).words.map((word: number) => [ 79 | (word >> 24) & 0xff, 80 | (word >> 16) & 0xff, 81 | (word >> 8) & 0xff, 82 | word & 0xff 83 | ]).flat()); 84 | case 'keccak256': 85 | return Uint8Array.from(keccak256.array(messageBytes)); 86 | default: 87 | throw new Error(`Unsupported algorithm: ${algorithm}`); 88 | } 89 | }, 90 | nonCryptographicHash(algorithm: 'wyhash64', message: Uint8Array | string): Uint8Array { 91 | let messageBytes: Uint8Array; 92 | 93 | // Convert string message to Uint8Array 94 | if (typeof message === 'string') { 95 | messageBytes = new TextEncoder().encode(message); 96 | } else { 97 | messageBytes = message; 98 | } 99 | 100 | switch (algorithm) { 101 | case 'wyhash64': 102 | const seed = BigInt(0); // You can use any seed value 103 | const hash = wyhash(messageBytes, seed); 104 | const buffer = new ArrayBuffer(8); 105 | const view = new DataView(buffer); 106 | view.setBigUint64(0, hash, true); 107 | return new Uint8Array(buffer); 108 | default: 109 | throw new Error(`Unsupported algorithm: ${algorithm}`); 110 | } 111 | }, 112 | concatU8a(arrays: Uint8Array[]): Uint8Array { 113 | const totalSize = arrays.reduce((acc, e) => acc + e.length, 0); 114 | const merged = new Uint8Array(totalSize); 115 | arrays.forEach((array, i, arrays) => { 116 | const offset = arrays.slice(0, i).reduce((acc, e) => acc + e.length, 0); 117 | merged.set(array, offset); 118 | }); 119 | return merged; 120 | } 121 | } 122 | 123 | export async function execute(inputObj: any) { 124 | const inputJson = JSON.stringify(inputObj) 125 | console.log('INPUT:', inputJson) 126 | return await main(inputJson) 127 | } 128 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ES6", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | --------------------------------------------------------------------------------