-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1 parent
e3590eb
commit aaabb34
Showing
1 changed file
with
837 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,837 @@ | ||
{ | ||
"cells": [ | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"# Part 1: Motivation\n", | ||
"Let's say we're building a local editor that allows you to load an AIConfig\n", | ||
"from a local file and then run methods on it.\n", | ||
"\n", | ||
"In the (simplified) code below, we do just that." | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 16, | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"name": "stdout", | ||
"output_type": "stream", | ||
"text": [ | ||
"Loaded AIConfig: NYC Trip Planner\n", | ||
"\n", | ||
"\n" | ||
] | ||
} | ||
], | ||
"source": [ | ||
"import json\n", | ||
"from typing import Any\n", | ||
"\n", | ||
"\n", | ||
"def read_json_from_file(path: str) -> dict[str, Any]:\n", | ||
" with open(path, \"r\") as f:\n", | ||
" return json.loads(f.read())\n", | ||
" \n", | ||
"\n", | ||
"def start_app(path: str):\n", | ||
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n", | ||
" aiconfig = read_json_from_file(path)\n", | ||
" print(f\"Loaded AIConfig: {aiconfig['name']}\\n\")\n", | ||
"\n", | ||
"\n", | ||
"start_app(\"cookbooks/Getting-Started/travel.aiconfig.json\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"## Cool, LGTM, ship it!" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"# A few hours later..." | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"# Issue #9000 Editor crashes on new file path\n", | ||
"### opened 2 hours ago by lastmile-biggest-fan\n", | ||
"\n", | ||
"Dear LastMile team,\n", | ||
"I really like the editor, but when I give it a new file path, it crashes!\n", | ||
"I was hoping it would create a new AIConfig for me and write it to the file..." | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"# OK, what happened?" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 17, | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"ename": "FileNotFoundError", | ||
"evalue": "[Errno 2] No such file or directory: 'i-dont-exist-yet-please-create-me.json'", | ||
"output_type": "error", | ||
"traceback": [ | ||
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", | ||
"\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", | ||
"Cell \u001b[0;32mIn[17], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mstart_app\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mi-dont-exist-yet-please-create-me.json\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", | ||
"Cell \u001b[0;32mIn[16], line 11\u001b[0m, in \u001b[0;36mstart_app\u001b[0;34m(path)\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mstart_app\u001b[39m(path: \u001b[38;5;28mstr\u001b[39m):\n\u001b[1;32m 10\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\u001b[39;00m\n\u001b[0;32m---> 11\u001b[0m aiconfig \u001b[38;5;241m=\u001b[39m json\u001b[38;5;241m.\u001b[39mloads(\u001b[43mread_file\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath\u001b[49m\u001b[43m)\u001b[49m)\n\u001b[1;32m 12\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mLoaded AIConfig: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00maiconfig[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mname\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 13\u001b[0m \u001b[38;5;28mprint\u001b[39m()\n", | ||
"Cell \u001b[0;32mIn[16], line 5\u001b[0m, in \u001b[0;36mread_file\u001b[0;34m(path)\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mread_file\u001b[39m(path: \u001b[38;5;28mstr\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mstr\u001b[39m:\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mr\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m f\u001b[38;5;241m.\u001b[39mread()\n", | ||
"File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/aiconfig/lib/python3.10/site-packages/IPython/core/interactiveshell.py:310\u001b[0m, in \u001b[0;36m_modified_open\u001b[0;34m(file, *args, **kwargs)\u001b[0m\n\u001b[1;32m 303\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m}:\n\u001b[1;32m 304\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 305\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIPython won\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt let you open fd=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfile\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m by default \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 306\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mas it is likely to crash IPython. If you know what you are doing, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 307\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myou can use builtins\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m open.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 308\u001b[0m )\n\u001b[0;32m--> 310\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mio_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", | ||
"\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'i-dont-exist-yet-please-create-me.json'" | ||
] | ||
} | ||
], | ||
"source": [ | ||
"start_app(\"i-dont-exist-yet-please-create-me.json\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"# Oops" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"Ok, let's diagnose the problem here. We forgot to handle the case where the path doesn't exist.\n", | ||
"\n", | ||
"That's understandable. As programmers, we don't always write perfect code.\n", | ||
"Sometimes it's helpful to bring new tools into the workflow to prevent this kind of problem in the future.\n", | ||
"\n", | ||
"\n", | ||
"Hmm, ok. Wouldn't it be nice if we had a static analyzer that could have caught this problem immediately? That way we could have fixed it before the initial PR was merged.\n", | ||
"\n", | ||
"Let's analyze some tools." | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"## V2: Optional\n", | ||
"\n", | ||
"First, let's fix the root cause and catch exceptions. Now, what do we do in the `except` block? \n", | ||
"\n", | ||
"Well, we can reraise, but that brings us right back to the previous case and doesn't achieve anything helpful. \n", | ||
"\n", | ||
"Instead, notice what happens if we return None and type hint the function accordingly (Optional[...])." | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 6, | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"name": "stdout", | ||
"output_type": "stream", | ||
"text": [ | ||
"\n", | ||
"[Pyright] Object of type \"None\" is not subscriptable\n", | ||
"PylancereportOptionalSubscript\n", | ||
"(variable) aiconfig: dict[str, Any] | None\n", | ||
"\n" | ||
] | ||
} | ||
], | ||
"source": [ | ||
"from typing import Any, Optional\n", | ||
"\n", | ||
"\n", | ||
"def read_json_from_file(path: str) -> Optional[dict[str, Any]]:\n", | ||
" try:\n", | ||
" with open(path, \"r\") as f:\n", | ||
" return json.loads(f.read())\n", | ||
" except Exception as e:\n", | ||
" return None\n", | ||
" \n", | ||
"\n", | ||
"def start_app(path: str):\n", | ||
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n", | ||
" aiconfig = read_json_from_file(path)\n", | ||
" print(f\"Loaded AIConfig: {aiconfig['name']}\\n\")\n", | ||
"\n", | ||
"print(\"\"\"\n", | ||
"[Pyright] Object of type \"None\" is not subscriptable\n", | ||
"PylancereportOptionalSubscript\n", | ||
"(variable) aiconfig: dict[str, Any] | None\n", | ||
"\"\"\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"# Aha!\n", | ||
"\n", | ||
"Now, Pyright immediately tells us that `None` is a possibility, and we have to handle this case. Let's do that.\n" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 31, | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"name": "stdout", | ||
"output_type": "stream", | ||
"text": [ | ||
"Loaded AIConfig: NYC Trip Planner\n", | ||
"\n", | ||
"Loaded AIConfig: \n", | ||
"\n" | ||
] | ||
} | ||
], | ||
"source": [ | ||
"from typing import Optional\n", | ||
"from aiconfig.Config import AIConfigRuntime\n", | ||
"\n", | ||
"\n", | ||
"\n", | ||
"def read_json_from_file(path: str) -> Optional[dict[str, Any]]:\n", | ||
" try:\n", | ||
" with open(path, \"r\") as f:\n", | ||
" return json.loads(f.read())\n", | ||
" except Exception:\n", | ||
" return None\n", | ||
"\n", | ||
"def start_app(path: str):\n", | ||
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n", | ||
" aiconfig = read_json_from_file(path)\n", | ||
" if aiconfig is None:\n", | ||
" print(f\"Could not load AIConfig from path: {path}. Creating and saving.\")\n", | ||
" aiconfig = json.dumps(AIConfigRuntime.create())\n", | ||
" # [save the aiconfig to the path] \n", | ||
" print(f\"Loaded and saved new AIConfig\\n\")\n", | ||
" else:\n", | ||
" print(f\"Loaded AIConfig: {aiconfig}\\n\")\n", | ||
"\n", | ||
"start_app(\"cookbooks/Getting-Started/travel.aiconfig.json\")\n", | ||
"start_app(\"i-dont-exist-yet-please-create-me.json\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"Ok, cool, much better. But wait, it would be nice to retain some information about what went wrong. My `None` value doesn't tell me anything about why the AIConfig couldn't be loaded. Does the file not exist? Was it a permission problem, networked filesystem problem? etc." | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"# V3: Result" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"The result library (https://github.com/rustedpy/result) provides a neat type\n", | ||
"called `Result`, which is a bit like Optional. It's parametrized by the value type just like optional, but also by a second type for the error case.\n", | ||
"\n", | ||
"We can use it like optional, but store an arbitrary value with information about what went wrong." | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 52, | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"name": "stdout", | ||
"output_type": "stream", | ||
"text": [ | ||
"Loaded AIConfig: NYC Trip Planner\n", | ||
"\n", | ||
"Could not load AIConfig from path: i-dont-exist-yet-please-create-me.json (File not found at path: i-dont-exist-yet-please-create-me.json). Creating and saving.\n", | ||
"Created and saved new AIConfig: \n", | ||
"\n" | ||
] | ||
} | ||
], | ||
"source": [ | ||
"from aiconfig.Config import AIConfigRuntime\n", | ||
"from result import Result, Ok, Err\n", | ||
"from typing import Any\n", | ||
"\n", | ||
"from json import JSONDecodeError\n", | ||
"\n", | ||
"\n", | ||
"def read_json_from_file(path: str) -> Result[dict[str, Any], str]:\n", | ||
" \"\"\"Use `str` in the error case to contain a helpful error message.\"\"\"\n", | ||
" try:\n", | ||
" with open(path, \"r\") as f:\n", | ||
" return Ok(json.loads(f.read()))\n", | ||
" except FileNotFoundError:\n", | ||
" return Err(f\"File not found at path: {path}\")\n", | ||
" except OSError as e:\n", | ||
" return Err(f\"Could not read file at path: {path}: {e}\")\n", | ||
" except JSONDecodeError as e:\n", | ||
" return Err(f\"Could not parse JSON at path: {path}: {e}\")\n", | ||
"\n", | ||
"def start_app(path: str):\n", | ||
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n", | ||
" file_contents = read_json_from_file(path)\n", | ||
" match file_contents:\n", | ||
" case Ok(aiconfig_ok):\n", | ||
" print(f\"Loaded AIConfig: {aiconfig_ok['name']}\\n\")\n", | ||
" case Err(e):\n", | ||
" print(f\"Could not load AIConfig from path: {path} ({e}). Creating and saving.\")\n", | ||
" aiconfig = AIConfigRuntime.create().model_dump(exclude=\"callback_manager\")\n", | ||
" # [Save to file path]\n", | ||
" # aiconfig.save(path)\n", | ||
" print(f\"Created and saved new AIConfig: {aiconfig['name']}\\n\")\n", | ||
"\n", | ||
"start_app(\"cookbooks/Getting-Started/travel.aiconfig.json\")\n", | ||
"start_app(\"i-dont-exist-yet-please-create-me.json\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"There are several nice things about this pattern:\n", | ||
"* If you fail to check for the error case, you get static errors similar to the `None` Optional case\n", | ||
"* You also get specific, useful error information unlike Optional\n", | ||
"* Structural pattern matching: When matching the cases, you can elegantly and safely unbox the data inside the result.\n", | ||
"* Because of pyright's ability to check for exhaustive pattern matching, it will yell at you if you don't handle the Err case. Try it! Comment out the Err case." | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"# Part 2: Composition" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"Cool, so we have a very basic example of better error handling. What about a more realistic level of complexity involving a sequence of chained operations? Consider this variant of the previous app example:" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 5, | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"ename": "KeyError", | ||
"evalue": "'text'", | ||
"output_type": "error", | ||
"traceback": [ | ||
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", | ||
"\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", | ||
"Cell \u001b[0;32mIn[5], line 37\u001b[0m\n\u001b[1;32m 32\u001b[0m prompts \u001b[38;5;241m=\u001b[39m get_prompt_text_list(aiconfig)\n\u001b[1;32m 33\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m HTTPResponse(\u001b[38;5;241m200\u001b[39m, JSON({\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprompts\u001b[39m\u001b[38;5;124m\"\u001b[39m: prompts}))\n\u001b[0;32m---> 37\u001b[0m \u001b[43mendpoint\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mcookbooks/Getting-Started/travel.aiconfig.json\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", | ||
"Cell \u001b[0;32mIn[5], line 32\u001b[0m, in \u001b[0;36mendpoint\u001b[0;34m(path)\u001b[0m\n\u001b[1;32m 30\u001b[0m contents \u001b[38;5;241m=\u001b[39m read_file(path)\n\u001b[1;32m 31\u001b[0m aiconfig \u001b[38;5;241m=\u001b[39m parse_json(contents)\n\u001b[0;32m---> 32\u001b[0m prompts \u001b[38;5;241m=\u001b[39m \u001b[43mget_prompt_text_list\u001b[49m\u001b[43m(\u001b[49m\u001b[43maiconfig\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 33\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m HTTPResponse(\u001b[38;5;241m200\u001b[39m, JSON({\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprompts\u001b[39m\u001b[38;5;124m\"\u001b[39m: prompts}))\n", | ||
"Cell \u001b[0;32mIn[5], line 26\u001b[0m, in \u001b[0;36mget_prompt_text_list\u001b[0;34m(aiconfig)\u001b[0m\n\u001b[1;32m 25\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget_prompt_text_list\u001b[39m(aiconfig: JSON) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mlist\u001b[39m[\u001b[38;5;28mstr\u001b[39m]:\n\u001b[0;32m---> 26\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m [prompt[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtext\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;28;01mfor\u001b[39;00m prompt \u001b[38;5;129;01min\u001b[39;00m aiconfig[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprompts\u001b[39m\u001b[38;5;124m\"\u001b[39m]]\n", | ||
"Cell \u001b[0;32mIn[5], line 26\u001b[0m, in \u001b[0;36m<listcomp>\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 25\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mget_prompt_text_list\u001b[39m(aiconfig: JSON) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mlist\u001b[39m[\u001b[38;5;28mstr\u001b[39m]:\n\u001b[0;32m---> 26\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m [\u001b[43mprompt\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtext\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m prompt \u001b[38;5;129;01min\u001b[39;00m aiconfig[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mprompts\u001b[39m\u001b[38;5;124m\"\u001b[39m]]\n", | ||
"\u001b[0;31mKeyError\u001b[0m: 'text'" | ||
] | ||
} | ||
], | ||
"source": [ | ||
"from dataclasses import dataclass\n", | ||
"import json\n", | ||
"from typing import NewType\n", | ||
"\n", | ||
"JSON = NewType(\"JSON\", dict[str, Any])\n", | ||
"\n", | ||
"@dataclass\n", | ||
"class HTTPResponse:\n", | ||
" status_code: int\n", | ||
" body: JSON\n", | ||
"\n", | ||
"def read_file(path: str) -> str:\n", | ||
" with open(path, \"r\") as f:\n", | ||
" return f.read()\n", | ||
" \n", | ||
"\n", | ||
"def parse_json(json_str: str) -> JSON:\n", | ||
" return json.loads(json_str)\n", | ||
"\n", | ||
"\n", | ||
"def read_json_from_file(path: str) -> JSON:\n", | ||
" with open(path, \"r\") as f:\n", | ||
" return json.loads(f.read())\n", | ||
"\n", | ||
"def get_prompt_text_list(aiconfig: JSON) -> list[str]:\n", | ||
" return [prompt[\"text\"] for prompt in aiconfig[\"prompts\"]]\n", | ||
"\n", | ||
"\n", | ||
"def endpoint(path: str):\n", | ||
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n", | ||
" contents = read_file(path)\n", | ||
" aiconfig = parse_json(contents)\n", | ||
" prompts = get_prompt_text_list(aiconfig)\n", | ||
" return HTTPResponse(200, JSON({\"prompts\": prompts}))\n", | ||
"\n", | ||
"\n", | ||
"\n", | ||
"endpoint(\"cookbooks/Getting-Started/travel.aiconfig.json\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"We should not let this propagate to the server framework. It will get caught and shown to the user as an unhelpful stack trace. Even if the frontend handles generic errors, it cannot do anything useful. Instead, we should return specific HTTP errors." | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 7, | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"data": { | ||
"text/plain": [ | ||
"HTTPResponse(status_code=567, body={'error': \"Could not find key: 'text'\"})" | ||
] | ||
}, | ||
"execution_count": 7, | ||
"metadata": {}, | ||
"output_type": "execute_result" | ||
} | ||
], | ||
"source": [ | ||
"from dataclasses import dataclass\n", | ||
"import json\n", | ||
"from typing import NewType\n", | ||
"\n", | ||
"JSON = NewType(\"JSON\", dict[str, Any])\n", | ||
"\n", | ||
"@dataclass\n", | ||
"class HTTPResponse:\n", | ||
" status_code: int\n", | ||
" body: JSON\n", | ||
"\n", | ||
"def read_file(path: str) -> str:\n", | ||
" with open(path, \"r\") as f:\n", | ||
" return f.read()\n", | ||
" \n", | ||
"\n", | ||
"def parse_json(json_str: str) -> JSON:\n", | ||
" return json.loads(json_str)\n", | ||
"\n", | ||
"\n", | ||
"def get_prompt_text_list(aiconfig: JSON) -> list[str]:\n", | ||
" return [prompt[\"text\"] for prompt in aiconfig[\"prompts\"]]\n", | ||
"\n", | ||
"\n", | ||
"def prompt_list_to_http_response(prompts: list[str]) -> HTTPResponse:\n", | ||
" return HTTPResponse(200, JSON({\"prompts\": prompts}))\n", | ||
"\n", | ||
"def endpoint(path: str):\n", | ||
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n", | ||
"\n", | ||
" try:\n", | ||
" contents = read_file(path)\n", | ||
" aiconfig = parse_json(contents)\n", | ||
" prompts = get_prompt_text_list(aiconfig)\n", | ||
" response = prompt_list_to_http_response(prompts)\n", | ||
" return response\n", | ||
" except FileNotFoundError:\n", | ||
" return HTTPResponse(404, JSON({\"error\": \"File not found\"}))\n", | ||
" except OSError as e:\n", | ||
" return HTTPResponse(500, JSON({\"error\": f\"Could not read file: {e}\"}))\n", | ||
" except json.JSONDecodeError as e:\n", | ||
" return HTTPResponse(555, JSON({\"error\": f\"Could not parse JSON: {e}\"}))\n", | ||
" except KeyError as e:\n", | ||
" return HTTPResponse(567, JSON({\"error\": f\"Could not find key: {e}\"}))\n", | ||
"\n", | ||
"\n", | ||
"endpoint(\"cookbooks/Getting-Started/travel.aiconfig.json\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"This is better, but we can't reuse any of our helper functions! Every endpoint will have to repeat this error handling. OK, new version:\n" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 11, | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"data": { | ||
"text/plain": [ | ||
"HTTPResponse(status_code=567, body={'error': \"Could not find key: InternalFailure(message='Could not find key', category='KeyError', exception=KeyError('text'))\"})" | ||
] | ||
}, | ||
"execution_count": 11, | ||
"metadata": {}, | ||
"output_type": "execute_result" | ||
} | ||
], | ||
"source": [ | ||
"from dataclasses import dataclass\n", | ||
"import json\n", | ||
"from typing import NewType\n", | ||
"from result import Result, Ok, Err\n", | ||
"\n", | ||
"JSON = NewType(\"JSON\", dict[str, Any])\n", | ||
"\n", | ||
"@dataclass\n", | ||
"class InternalFailure:\n", | ||
" message: str\n", | ||
" category: str\n", | ||
" exception: Exception\n", | ||
"\n", | ||
"@dataclass\n", | ||
"class HTTPResponse:\n", | ||
" status_code: int\n", | ||
" body: JSON\n", | ||
"\n", | ||
"def read_file(path: str) -> Result[str, InternalFailure]:\n", | ||
" try:\n", | ||
" with open(path, \"r\") as f:\n", | ||
" return Ok(f.read())\n", | ||
" except Exception as e:\n", | ||
" return Err(InternalFailure(\n", | ||
" message=f\"Could not read file at path: {path}\",\n", | ||
" category=\"OSError\",\n", | ||
" exception=e,\n", | ||
" ))\n", | ||
" \n", | ||
"\n", | ||
"def parse_json(json_str: str) -> Result[JSON, InternalFailure]:\n", | ||
" try: \n", | ||
" return Ok(json.loads(json_str))\n", | ||
" except json.JSONDecodeError as e:\n", | ||
" return Err(InternalFailure(\n", | ||
" message=\"Could not parse JSON\",\n", | ||
" category=\"JSONDecodeError\",\n", | ||
" exception=e,\n", | ||
" ))\n", | ||
"\n", | ||
"\n", | ||
"\n", | ||
"def get_prompt_text_list(aiconfig: JSON) -> Result[list[str], InternalFailure]:\n", | ||
" try:\n", | ||
" return Ok([prompt[\"text\"] for prompt in aiconfig[\"prompts\"]])\n", | ||
" except KeyError as e:\n", | ||
" return Err(InternalFailure(\n", | ||
" message=\"Could not find key\",\n", | ||
" category=\"KeyError\",\n", | ||
" exception=e,\n", | ||
" ))\n", | ||
"\n", | ||
"def prompt_list_to_http_response(prompts: list[str]) -> HTTPResponse:\n", | ||
" return HTTPResponse(200, JSON({\"prompts\": prompts}))\n", | ||
"\n", | ||
"\n", | ||
"def internal_failure_to_http_response(failure: InternalFailure) -> HTTPResponse:\n", | ||
" if failure.category == \"OSError\":\n", | ||
" return HTTPResponse(500, JSON({\"error\": f\"Could not read file: {failure.exception}\"}))\n", | ||
" elif failure.category == \"JSONDecodeError\":\n", | ||
" return HTTPResponse(555, JSON({\"error\": f\"Could not parse JSON: {failure.exception}\"}))\n", | ||
" elif failure.category == \"KeyError\":\n", | ||
" return HTTPResponse(567, JSON({\"error\": f\"Could not find key: {failure.exception}\"}))\n", | ||
" else:\n", | ||
" return HTTPResponse(500, JSON({\"error\": f\"Internal failure: {failure}\"}))\n", | ||
"\n", | ||
"\n", | ||
"def endpoint(path: str):\n", | ||
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n", | ||
"\n", | ||
" file_contents = read_file(path)\n", | ||
" match file_contents:\n", | ||
" case Ok(file_contents_ok):\n", | ||
" parsed_json = parse_json(file_contents_ok)\n", | ||
" match parsed_json:\n", | ||
" case Ok(parsed_json_ok):\n", | ||
" prompts = get_prompt_text_list(parsed_json_ok)\n", | ||
" match prompts:\n", | ||
" case Ok(prompts_ok):\n", | ||
" return prompt_list_to_http_response(prompts_ok)\n", | ||
" case Err(e):\n", | ||
" return internal_failure_to_http_response(e)\n", | ||
" case Err(e):\n", | ||
" return internal_failure_to_http_response(e)\n", | ||
" case Err(e):\n", | ||
" return internal_failure_to_http_response(e)\n", | ||
"\n", | ||
"\n", | ||
"endpoint(\"cookbooks/Getting-Started/travel.aiconfig.json\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"Ok, but Now we still have to do this annoying nested error checking! Luckily, Result has a really nice way to do this." | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 12, | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"data": { | ||
"text/plain": [ | ||
"HTTPResponse(status_code=567, body={'error': \"Could not find key: 'text'\"})" | ||
] | ||
}, | ||
"execution_count": 12, | ||
"metadata": {}, | ||
"output_type": "execute_result" | ||
} | ||
], | ||
"source": [ | ||
"from dataclasses import dataclass\n", | ||
"import json\n", | ||
"from typing import NewType\n", | ||
"from result import Result, Ok, Err\n", | ||
"\n", | ||
"JSON = NewType(\"JSON\", dict[str, Any])\n", | ||
"\n", | ||
"@dataclass\n", | ||
"class InternalFailure:\n", | ||
" message: str\n", | ||
" category: str\n", | ||
" exception: Exception\n", | ||
"\n", | ||
"@dataclass\n", | ||
"class HTTPResponse:\n", | ||
" status_code: int\n", | ||
" body: JSON\n", | ||
" \n", | ||
"\n", | ||
"def read_file(path: str) -> Result[str, InternalFailure]:\n", | ||
" try:\n", | ||
" with open(path, \"r\") as f:\n", | ||
" return Ok(f.read())\n", | ||
" except Exception as e:\n", | ||
" return Err(InternalFailure(\n", | ||
" message=f\"Could not read file at path: {path}\",\n", | ||
" category=\"OSError\",\n", | ||
" exception=e,\n", | ||
" ))\n", | ||
" \n", | ||
"\n", | ||
"def parse_json(json_str: str) -> Result[JSON, InternalFailure]:\n", | ||
" try: \n", | ||
" return Ok(json.loads(json_str))\n", | ||
" except json.JSONDecodeError as e:\n", | ||
" return Err(InternalFailure(\n", | ||
" message=\"Could not parse JSON\",\n", | ||
" category=\"JSONDecodeError\",\n", | ||
" exception=e,\n", | ||
" ))\n", | ||
"\n", | ||
"\n", | ||
"\n", | ||
"def get_prompt_text_list(aiconfig: JSON) -> Result[list[str], InternalFailure]:\n", | ||
" try:\n", | ||
" return Ok([prompt[\"text\"] for prompt in aiconfig[\"prompts\"]])\n", | ||
" except KeyError as e:\n", | ||
" return Err(InternalFailure(\n", | ||
" message=\"Could not find key\",\n", | ||
" category=\"KeyError\",\n", | ||
" exception=e,\n", | ||
" ))\n", | ||
"\n", | ||
"def prompt_list_to_http_response(prompts: list[str]) -> HTTPResponse:\n", | ||
" return HTTPResponse(200, JSON({\"prompts\": prompts}))\n", | ||
"\n", | ||
"\n", | ||
"def internal_failure_to_http_response(failure: InternalFailure) -> HTTPResponse:\n", | ||
" if failure.category == \"OSError\":\n", | ||
" return HTTPResponse(500, JSON({\"error\": f\"Could not read file: {failure.exception}\"}))\n", | ||
" elif failure.category == \"JSONDecodeError\":\n", | ||
" return HTTPResponse(555, JSON({\"error\": f\"Could not parse JSON: {failure.exception}\"}))\n", | ||
" elif failure.category == \"KeyError\":\n", | ||
" return HTTPResponse(567, JSON({\"error\": f\"Could not find key: {failure.exception}\"}))\n", | ||
" else:\n", | ||
" return HTTPResponse(500, JSON({\"error\": f\"Internal failure: {failure}\"}))\n", | ||
"\n", | ||
"\n", | ||
"def endpoint(path: str):\n", | ||
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n", | ||
"\n", | ||
" response = (\n", | ||
" read_file(path)\n", | ||
" .and_then(parse_json)\n", | ||
" .and_then(get_prompt_text_list)\n", | ||
" .map(prompt_list_to_http_response)\n", | ||
" .unwrap_or_else(internal_failure_to_http_response)\n", | ||
" )\n", | ||
" return response\n", | ||
"\n", | ||
"\n", | ||
"endpoint(\"cookbooks/Getting-Started/travel.aiconfig.json\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"Neat. To recap, this code now has the following very nice properties:\n", | ||
"* Errors are statically checked, eliminating a class of bugs\n", | ||
"* Modular and highly reusable\n", | ||
"* Concise (especially the endpoint)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"metadata": {}, | ||
"source": [ | ||
"# Part 3: Advanced topics\n", | ||
"* decorators and other higher-order functions\n", | ||
"* partial application\n", | ||
"* do-notation for complex composition" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"from functools import partial\n", | ||
"from typing import Callable, TypeVar, ParamSpec\n", | ||
"\n", | ||
"R = TypeVar('R')\n", | ||
"P = ParamSpec('P')\n", | ||
"E = TypeVar('E')\n", | ||
"\n", | ||
"def parametrized(\n", | ||
" decorator: Callable[[Callable[P, R], Callable[..., Result[R, E]]], Callable[P, Result[R, E]]]\n", | ||
") -> Callable[[Callable[..., Result[R, E]]], Callable[[Callable[P, R]], Callable[P, Result[R, E]]]]:\n", | ||
" def layer(exception_handler: Callable[..., Result[R, E]]) -> Callable[[Callable[P, R]], Callable[P, Result[R, E]]]:\n", | ||
" def wrapper(func: Callable[P, R]) -> Callable[P, Result[R, E]]:\n", | ||
" return decorator(func, exception_handler)\n", | ||
" return wrapper\n", | ||
" return layer\n", | ||
"\n", | ||
"@parametrized\n", | ||
"def exception_handled(func: Callable[P, R], exception_handler: Callable[..., Result[R, E]]) -> Callable[P, Result[R, E]]:\n", | ||
" def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[R, E]:\n", | ||
" try:\n", | ||
" return Ok(func(*args, **kwargs))\n", | ||
" except Exception as e: # catch all exceptions and delegate to handler\n", | ||
" return exception_handler(e)\n", | ||
" return wrapper\n", | ||
"\n", | ||
"from dataclasses import dataclass\n", | ||
"import json\n", | ||
"from typing import NewType\n", | ||
"from result import Result, Ok, Err\n", | ||
"\n", | ||
"JSON = NewType(\"JSON\", dict[str, Any])\n", | ||
"\n", | ||
"@dataclass\n", | ||
"class InternalFailure:\n", | ||
" message: str\n", | ||
" category: str\n", | ||
" exception: Exception\n", | ||
"\n", | ||
"@dataclass\n", | ||
"class HTTPResponse:\n", | ||
" status_code: int\n", | ||
" body: JSON\n", | ||
" \n", | ||
"\n", | ||
"def _handle_exception_with_helper(exception: Exception, message: str, category: str) -> Err[InternalFailure]:\n", | ||
" return Err(InternalFailure(message, category, exception))\n", | ||
"\n", | ||
"def handle_exception_with(message: str, category: str) -> Callable[[Exception], Err[InternalFailure]]:\n", | ||
" return partial(_handle_exception_with_helper, message=message, category=category)\n", | ||
"\n", | ||
"@exception_handled(handle_exception_with(\"Could not read file\", \"OSError\"))\n", | ||
"def read_file(path: str) -> str:\n", | ||
" with open(path, \"r\") as f:\n", | ||
" return f.read()\n", | ||
" \n", | ||
"\n", | ||
"@exception_handled(handle_exception_with(\"Could not parse JSON\", \"JSONDecodeError\"))\n", | ||
"def parse_json(json_str: str) -> JSON:\n", | ||
" return JSON(json.loads(json_str))\n", | ||
"\n", | ||
"\n", | ||
"@exception_handled(handle_exception_with(\"Could not find key\", \"KeyError\"))\n", | ||
"def get_prompt_text_list(aiconfig: JSON) -> list[str]:\n", | ||
" return [prompt[\"text\"] for prompt in aiconfig[\"prompts\"]]\n", | ||
"\n", | ||
"\n", | ||
"def prompt_list_to_http_response(prompts: list[str]) -> HTTPResponse:\n", | ||
" return HTTPResponse(200, JSON({\"prompts\": prompts}))\n", | ||
"\n", | ||
"\n", | ||
"def internal_failure_to_http_response(failure: InternalFailure) -> HTTPResponse:\n", | ||
" if failure.category == \"OSError\":\n", | ||
" return HTTPResponse(500, JSON({\"error\": f\"Could not read file: {failure.exception}\"}))\n", | ||
" elif failure.category == \"JSONDecodeError\":\n", | ||
" return HTTPResponse(555, JSON({\"error\": f\"Could not parse JSON: {failure.exception}\"}))\n", | ||
" elif failure.category == \"KeyError\":\n", | ||
" return HTTPResponse(567, JSON({\"error\": f\"Could not find key: {failure.exception}\"}))\n", | ||
" else:\n", | ||
" return HTTPResponse(500, JSON({\"error\": f\"Internal failure: {failure}\"}))\n", | ||
"\n", | ||
"\n", | ||
"def endpoint(path: str):\n", | ||
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n", | ||
"\n", | ||
" response = (\n", | ||
" read_file(path)\n", | ||
" .and_then(parse_json)\n", | ||
" .and_then(get_prompt_text_list)\n", | ||
" .map(prompt_list_to_http_response)\n", | ||
" .unwrap_or_else(internal_failure_to_http_response)\n", | ||
" )\n", | ||
" return response\n", | ||
"\n" | ||
] | ||
} | ||
], | ||
"metadata": { | ||
"kernelspec": { | ||
"display_name": "aiconfig", | ||
"language": "python", | ||
"name": "python3" | ||
}, | ||
"language_info": { | ||
"codemirror_mode": { | ||
"name": "ipython", | ||
"version": 3 | ||
}, | ||
"file_extension": ".py", | ||
"mimetype": "text/x-python", | ||
"name": "python", | ||
"nbconvert_exporter": "python", | ||
"pygments_lexer": "ipython3", | ||
"version": "3.10.13" | ||
} | ||
}, | ||
"nbformat": 4, | ||
"nbformat_minor": 2 | ||
} |