|
1 | | -# (Python) Crypto Tools |
| 1 | +## Writing your first tool in Python |
2 | 2 |
|
3 | | -This repo is an example of a Python-based [Tool](https://docs.otto8.ai/docs/tools/tools) set packaged for use by [Otto8 Assistants](https://docs.otto8.ai/docs/assistants/assistants). |
| 3 | +[python-hash-tool](https://github.com/otto8-ai/python-hash-tool) contains a reference `Python` implementation of the `Hash` Tool. |
| 4 | + |
| 5 | +This guide walks through the structure and design of the Tool and outlines the packaging requirements for [Otto8](https://docs.otto8.ai/concepts/agents) |
| 6 | + |
| 7 | +To clone this repo and follow along, run the following command: |
| 8 | + |
| 9 | +```bash |
| 10 | +git clone [email protected]:otto8-ai/python-hash-tool |
| 11 | +``` |
| 12 | + |
| 13 | +<br/> |
| 14 | + |
| 15 | +## Tool Repo Structure |
| 16 | + |
| 17 | +The directory tree below highlights the files required to implement `Hash` in Python and package it for `Otto8`. |
| 18 | + |
| 19 | +``` |
| 20 | +python-hash-tool |
| 21 | +├── hash.py |
| 22 | +├── requirements.txt |
| 23 | +└── tool.gpt |
| 24 | +``` |
| 25 | +<br/> |
| 26 | + |
| 27 | +## Defining the `Hash` Tool |
| 28 | + |
| 29 | +The `tool.gpt` file contains [GPTScript Tool Definitions](https://docs.gptscript.ai/tools/gpt-file-reference) which describe a set of Tools that can be used by Agents in `Otto8`. |
| 30 | +Every Tool repository must have a `tool.gpt` file in its root directory. |
| 31 | + |
| 32 | +The Tools defined in this file must have a descriptive `Name` and `Description` that will help Agents understand what the Tool does, what it returns (if anything), and all the `Parameters` it takes. |
| 33 | +Agents use these details to figure out when and how to use the Tool. We call the section of a Tool definition that contains this info a `Preamble`. |
| 34 | + |
| 35 | +We want the `Hash` Tool to return the hash of some given `data`. It would also be nice to support a few different algorithms for the Agent to choose from. |
| 36 | + |
| 37 | +Let's take a look at the `Preamble` for `Hash` to see how that's achieved: |
| 38 | + |
| 39 | +```yaml |
| 40 | +Name: Hash |
| 41 | +Description: Generate a hash of data using the given algorithm and return the result as a hexadecimal string |
| 42 | +Param: data: The data to hash |
| 43 | +Param: algo: The algorithm to generate a hash with. Supports "sha256" and "md5". Default is "sha256" |
| 44 | +``` |
| 45 | +
|
| 46 | +Breaking this down a bit: |
| 47 | +
|
| 48 | +- The `Preamble` above declares a Tool named `Hash`. |
| 49 | +- The `Param` fields enumerate the arguments that an Agent must provide when calling `Hash`, `data` and `algo`. |
| 50 | +- In this case, the description of the `algo` parameter outlines the valid options (`sha256` or `md5`) and defines a default value (`sha256`) |
| 51 | +- The `Description` explains what `Hash` returns with respect to the given arguments; the hash of `data` using the algorithm selected with `algo`. |
| 52 | + |
| 53 | +<br/> |
| 54 | + |
| 55 | +Immediately below the `Preamble` is the `Tool Body`, which tells `Otto8` how to execute the Tool: |
| 56 | + |
| 57 | +```bash |
| 58 | + #!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/hash.py |
| 59 | +``` |
| 60 | + |
| 61 | + |
| 62 | +This is where the magic happens. |
| 63 | + |
| 64 | +To oversimplify, when an Agent calls the `Hash` Tool, `Otto8` reads this line and then: |
| 65 | + |
| 66 | +1. Downloads the appropriate `Python` tool chain |
| 67 | +2. Sets up a working directory for the Tool and creates a virtual environment |
| 68 | +3. Installs the dependencies from the `requirements.txt`, if present |
| 69 | +4. Projects the call arguments onto environment variables (`DATA` and `ALGO`) |
| 70 | +5. Runs `python3 ${GPTSCRIPT_TOOL_DIR}/hash.py`. |
| 71 | + |
| 72 | +<br/> |
| 73 | + |
| 74 | +Putting it all together, here's the complete definition of the `Hash` Tool. |
| 75 | + |
| 76 | +```yaml |
| 77 | +Name: Hash |
| 78 | +Description: Generate a hash of data using the given algorithm and return the result as a hexadecimal string |
| 79 | +Param: data: The data to hash |
| 80 | +Param: algo: The algorithm to generate a hash with. Default is "sha256". Supports "sha256" and "md5". |
| 81 | +
|
| 82 | +#!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/hash.py |
| 83 | +``` |
| 84 | + |
| 85 | +<br/> |
| 86 | + |
| 87 | +## Tool Metadata |
| 88 | + |
| 89 | +The `tool.gpt` file also provides the following metadata for use in `Otto8`: |
| 90 | + |
| 91 | +- `!metadata:*:category` which tags Tools with the `Crypto` category to promote organization and discovery |
| 92 | +- `!metadata:*:icon` which assigns `https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/duotone/fingerprint-duotone.svg` as the Tool icon |
| 93 | + |
| 94 | +<br/> |
| 95 | + |
| 96 | +> **Note:** `*` is a wild card pattern that applies the metadata to all Tools in a `tool.gpt`. |
| 97 | + |
| 98 | +```yaml |
| 99 | +--- |
| 100 | +!metadata:*:category |
| 101 | +Crypto |
| 102 | +
|
| 103 | +--- |
| 104 | +!metadata:*:icon |
| 105 | +https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/duotone/fingerprint-duotone.svg |
| 106 | +``` |
| 107 | + |
| 108 | +<details> |
| 109 | + <summary> |
| 110 | + <strong>Note:</strong> Metadata can be applied to a specific Tool by either specifying the exact name (e.g. <code>!metadata:Hash:category</code>) or by adding the metadata directly to a Tool's <code>Preamble</code> |
| 111 | + </summary> |
| 112 | + |
| 113 | +```yaml |
| 114 | +Name: Hash |
| 115 | +Metadata: category: Crypto |
| 116 | +Metadata: icon: https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/duotone/fingerprint-duotone.svg |
| 117 | +``` |
| 118 | +</details> |
| 119 | + |
| 120 | +<br/> |
| 121 | + |
| 122 | +<details> |
| 123 | + <summary>Complete <code>tool.gpt</code></summary> |
| 124 | + |
| 125 | +```yaml |
| 126 | +--- |
| 127 | +Name: Hash |
| 128 | +Description: Generate a hash of data using the given algorithm and return the result as a hexadecimal string |
| 129 | +Param: data: The data to hash |
| 130 | +Param: algo: The algorithm to generate a hash with. Supports "sha256" and "md5". Default is "sha256" |
| 131 | +
|
| 132 | +#!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/hash.py |
| 133 | +
|
| 134 | +--- |
| 135 | +!metadata:*:category |
| 136 | +Crypto |
| 137 | +
|
| 138 | +--- |
| 139 | +!metadata:*:icon |
| 140 | +https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/duotone/fingerprint-duotone.svg |
| 141 | +``` |
| 142 | + |
| 143 | +</details> |
| 144 | + |
| 145 | +<br/> |
| 146 | + |
| 147 | +## Implementing Business Logic |
| 148 | + |
| 149 | +The `hash.py` file executed by the `Tool Body` is the concrete implementation of the Tool's business logic. |
| 150 | + |
| 151 | +Let's walk through the code to understand how it works. |
| 152 | + |
| 153 | +```python |
| 154 | +if __name__ == '__main__': |
| 155 | + try: |
| 156 | + main() |
| 157 | + except Exception as err: |
| 158 | + # Print err to stdout to return the error to the agent |
| 159 | + print(f'Error: {err}') |
| 160 | + sys.exit(1) |
| 161 | +``` |
| 162 | + |
| 163 | +Starting at the bottom, the `main` function is called in a `try` block so that any runtime exceptions caught are written to stdout. |
| 164 | +This is important because everything written to stdout is returned to the Agent when the Tool call is completed, while everything written to stderr is discarded. |
| 165 | +Using this pattern ensures that when a Tool call fails, the calling Agent is informed of the failure. |
| 166 | + |
| 167 | +Moving on, the `main` function implements the meat and potatoes of the `Hash` Tool. |
| 168 | + |
| 169 | +```python |
| 170 | +SUPPORTED_HASH_ALGORITHMS = ['sha256', 'md5'] |
| 171 | +
|
| 172 | +def main(): |
| 173 | + # Extract the tool's `data` argument from the env |
| 174 | + data = os.getenv('DATA') |
| 175 | + if not data: |
| 176 | + raise ValueError('A data argument must be provided') |
| 177 | + |
| 178 | + # Extract the tool's `algo` argument from the env and default to `sha256` |
| 179 | + algo = os.getenv('ALGO', 'sha256') |
| 180 | + if algo not in SUPPORTED_HASH_ALGORITHMS: |
| 181 | + # Return the supported algorithms in the error message to help agents choose a valid |
| 182 | + # algorithm the next time they call this tool |
| 183 | + raise ValueError(f'Unsupported hash algorithm: {algo} not in {SUPPORTED_HASH_ALGORITHMS}') |
| 184 | + #... |
| 185 | +``` |
| 186 | + |
| 187 | +It starts off by extracting the Tool's arguments from the respective environment variables and validates them. |
| 188 | +When an argument is invalid, the function raises an exception that describes the validation issue in detail. |
| 189 | +The goal is to provide useful information that an Agent can use to construct valid arguments for future calls. |
| 190 | +For example, when an invalid `algo` argument is provided, the code returns an error that contains the complete list of valid algorithms. |
| 191 | + |
| 192 | +After validating the Tool arguments, it calculates the hash and writes a JSON object to stdout. |
| 193 | +This object contains the hash and the algorithm used to generate it. |
| 194 | + |
| 195 | +```python |
| 196 | + # ... |
| 197 | + # Generate the hash |
| 198 | + hash_obj = hashlib.new(algo) |
| 199 | + hash_obj.update(data.encode('utf-8')) |
| 200 | + |
| 201 | + # Return the hash along with the algorithm used to generate it. |
| 202 | + # Providing more information in the tool's response makes it easier for agents to keep |
| 203 | + # track of the context. |
| 204 | + print(json.dumps({ |
| 205 | + 'algo': algo, |
| 206 | + 'hash': hash_obj.hexdigest() |
| 207 | + })) |
| 208 | +``` |
| 209 | + |
| 210 | +> **Note:** Producing structured data with extra contextual info (e.g. the algorithm) is considered good form. |
| 211 | +> It's a pattern that improves the Agent's ability to correctly use the Tool's result over time. |
| 212 | +
|
| 213 | +<details> |
| 214 | + <summary>Complete <code>hash.py</code></summary> |
| 215 | + |
| 216 | +```python |
| 217 | +import hashlib |
| 218 | +import json |
| 219 | +import os |
| 220 | +import sys |
| 221 | + |
| 222 | +SUPPORTED_HASH_ALGORITHMS = ['sha256', 'md5'] |
| 223 | + |
| 224 | + |
| 225 | +def main(): |
| 226 | + # Extract the tool's `data` argument from the env |
| 227 | + data = os.getenv('DATA') |
| 228 | + if not data: |
| 229 | + raise ValueError('A data argument must be provided') |
| 230 | + |
| 231 | + # Extract the tool's `algo` argument from the env and default to `sha256` |
| 232 | + algo = os.getenv('ALGO', 'sha256') |
| 233 | + if algo not in SUPPORTED_HASH_ALGORITHMS: |
| 234 | + # Return the supported algorithms in the error message to help assistants choose a valid |
| 235 | + # algorithm the next time they call this tool |
| 236 | + raise ValueError(f'Unsupported hash algorithm: {algo} not in {SUPPORTED_HASH_ALGORITHMS}') |
| 237 | + |
| 238 | + # Generate the hash |
| 239 | + hash_obj = hashlib.new(algo) |
| 240 | + hash_obj.update(data.encode('utf-8')) |
| 241 | + |
| 242 | + # Return the hash along with the algorithm used to generate it. |
| 243 | + # Providing more information in the tool's response makes it easier for assistants to keep |
| 244 | + # track of the context. |
| 245 | + print(json.dumps({ |
| 246 | + 'algo': algo, |
| 247 | + 'hash': hash_obj.hexdigest() |
| 248 | + })) |
| 249 | + |
| 250 | + |
| 251 | +if __name__ == '__main__': |
| 252 | + try: |
| 253 | + main() |
| 254 | + except Exception as err: |
| 255 | + # Print err to stdout to return the error to the assistant |
| 256 | + print(f'Error: {err}') |
| 257 | + sys.exit(1) |
| 258 | +``` |
| 259 | + |
| 260 | +</details> |
| 261 | + |
| 262 | +<br/> |
| 263 | + |
| 264 | +## Testing `hash.py` Locally |
| 265 | + |
| 266 | +Before adding a Tool to `Otto8`, verify that the Python business logic works on your machine. |
| 267 | + |
| 268 | +To do this, run through the following steps in the root of your local fork: |
| 269 | + |
| 270 | +1. Set up a virtual environment: |
| 271 | + |
| 272 | + ```bash |
| 273 | + python3 -m venv venv |
| 274 | + source venv/bin/activate |
| 275 | + ``` |
| 276 | + |
| 277 | +2. Activate the virtual environment: |
| 278 | + |
| 279 | + ```bash |
| 280 | + source venv/bin/activate |
| 281 | + ``` |
| 282 | + |
| 283 | +3. Install and freeze dependencies: |
| 284 | + |
| 285 | + ```bash |
| 286 | + pip install -r requirements.txt |
| 287 | + pip freeze > requirements.txt |
| 288 | + ``` |
| 289 | + |
| 290 | +4. Run the Tool with some test arguments: |
| 291 | + |
| 292 | + | **Command** | **Output** | |
| 293 | + | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | |
| 294 | + | `DATA='foo' python3 hash.py` | `{ "algo": "sha256", "hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" }` | |
| 295 | + | `python3 hash.py` | `Error: A data argument must be provided` | |
| 296 | + | `DATA='foo' ALGO='md5' python3 hash.py` | `{ "algo": "md5", "hash": "acbd18db4cc2f85cedef654fccc4a4d8" }` | |
| 297 | + | `DATA='foo' ALGO='whirlpool' python3 hash.py` | `Error: Unsupported hash algorithm: whirlpool not in ['sha256', 'md5']` | |
| 298 | + |
| 299 | +<br/> |
| 300 | + |
| 301 | +## Adding The `Hash` Tool to `Otto8` |
| 302 | + |
| 303 | +Before a Tool can be used by an Agent, an admin must first add the Tool to `Otto8` by performing the steps below: |
| 304 | + |
| 305 | +1. <details> |
| 306 | + <summary>Navigate to the <code>Otto8</code> admin UI in a browser and open the Tools page by clicking the <em>Tools</em> button in the left drawer</summary> |
| 307 | + <div align="left"> |
| 308 | + <img src="https://raw.githubusercontent.com/otto8-ai/python-hash-tool/refs/heads/main/docs/add-tools-step-0.png" |
| 309 | + alt="Open The Tools Page" width="200"/> |
| 310 | + </div> |
| 311 | + </details> |
| 312 | + |
| 313 | +2. <details> |
| 314 | + <summary>Click the <em>Register New Tool</em> button on the right</summary> |
| 315 | + <div align="left"> |
| 316 | + <img src="https://raw.githubusercontent.com/otto8-ai/python-hash-tool/refs/heads/main/docs/add-tools-step-1.png" |
| 317 | + alt="Click The Register New Tool Button" width="200"/> |
| 318 | + </div> |
| 319 | + </details> |
| 320 | + |
| 321 | +3. <details> |
| 322 | + <summary>Type the Tool repo reference into the modal's input box and click <em>Register Tool</em></summary> |
| 323 | + <div align="left"> |
| 324 | + <img src="https://raw.githubusercontent.com/otto8-ai/python-hash-tool/refs/heads/main/docs/add-tools-step-2.png" |
| 325 | + alt="Enter Tool Repo Reference" width="500" height="auto"/> |
| 326 | + </div> |
| 327 | + </details> |
| 328 | + |
| 329 | +<br/> |
| 330 | + |
| 331 | +<details> |
| 332 | + <summary>Once the tool has been added, you can search for it by category or name on the Tools page to verify</summary> |
| 333 | + <div align="left"> |
| 334 | + <img src="https://raw.githubusercontent.com/otto8-ai/python-hash-tool/refs/heads/main/docs/add-tools-step-3.png" |
| 335 | + alt="Search For Newly Added Tools" height="300"/> |
| 336 | + </div> |
| 337 | +</details> |
| 338 | + |
| 339 | +## Using The `Hash` Tool in an Agent |
| 340 | + |
| 341 | +To use the `Hash` Tool in an Agent, open the Agent's Edit page, then: |
| 342 | + |
| 343 | +1. <details> |
| 344 | + <summary>Click the <em>Add Tool</em> button under either the <em>Agent Tools</em> or <em>User Tools</em> sections</summary> |
| 345 | + <div align="left"> |
| 346 | + <img src="https://raw.githubusercontent.com/otto8-ai/python-hash-tool/refs/heads/main/docs/use-tools-step-0.png" |
| 347 | + alt="Click The Add Tool Button" width="500"/> |
| 348 | + </div> |
| 349 | + </details> |
| 350 | + |
| 351 | +2. <details> |
| 352 | + <summary>Search for "Hash" or "Crypto" in the Tool search pop-out and select the <code>Hash</code> Tool</summary> |
| 353 | + <div align="left"> |
| 354 | + <img src="https://raw.githubusercontent.com/otto8-ai/python-hash-tool/refs/heads/main/docs/use-tools-step-1.png" |
| 355 | + alt="Add Hash Tool To Agent" width="500"/> |
| 356 | + </div> |
| 357 | + </details> |
| 358 | + |
| 359 | +3. <details> |
| 360 | + <summary>Ask the Agent to generate a hash</summary> |
| 361 | + <div align="left"> |
| 362 | + <img src="https://raw.githubusercontent.com/otto8-ai/python-hash-tool/refs/heads/main/docs/use-tools-step-2.png" |
| 363 | + alt="Ask The Agent To Generate a Hash" width="500"/> |
| 364 | + </div> |
| 365 | + </details> |
0 commit comments