diff --git a/docs/modules/agents/agents/wikibase_agent.ipynb b/docs/modules/agents/agents/wikibase_agent.ipynb new file mode 100644 index 00000000..acc0ccbb --- /dev/null +++ b/docs/modules/agents/agents/wikibase_agent.ipynb @@ -0,0 +1,758 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5e3cb542-933d-4bf3-a82b-d9d6395a7832", + "metadata": { + "tags": [] + }, + "source": [ + "# Wikibase Agent\n", + "\n", + "This notebook demonstrates a very simple wikibase agent that uses sparql generation. Although this code is intended to work against any\n", + "wikibase instance, we use http://wikidata.org for testing.\n", + "\n", + "If you are interested in wikibases and sparql, please consider helping to improve this agent. Look [here](https://github.com/donaldziff/langchain-wikibase) for more details and open questions.\n" + ] + }, + { + "cell_type": "markdown", + "id": "07d42966-7e99-4157-90dc-6704977dcf1b", + "metadata": { + "tags": [] + }, + "source": [ + "## Preliminaries" + ] + }, + { + "cell_type": "markdown", + "id": "9132f093-c61e-4b8d-abef-91ebef3fc85f", + "metadata": { + "tags": [] + }, + "source": [ + "### API keys and other secrats\n", + "\n", + "We use an `.ini` file, like this: \n", + "```\n", + "[OPENAI]\n", + "OPENAI_API_KEY=xyzzy\n", + "[WIKIDATA]\n", + "WIKIDATA_USER_AGENT_HEADER=argle-bargle\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "99567dfd-05a7-412f-abf0-9b9f4424acbd", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['./secrets.ini']" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import configparser\n", + "config = configparser.ConfigParser()\n", + "config.read('./secrets.ini')" + ] + }, + { + "cell_type": "markdown", + "id": "332b6658-c978-41ca-a2be-4f8677fecaef", + "metadata": { + "tags": [] + }, + "source": [ + "### OpenAI API Key\n", + "\n", + "An OpenAI API key is required unless you modify the code below to use another LLM provider." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "dd328ee2-33cc-4e1e-aff7-cc0a2e05e2e6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "openai_api_key = config['OPENAI']['OPENAI_API_KEY']\n", + "import os\n", + "os.environ.update({'OPENAI_API_KEY': openai_api_key})" + ] + }, + { + "cell_type": "markdown", + "id": "42a9311b-600d-42bc-b000-2692ef87a213", + "metadata": { + "tags": [] + }, + "source": [ + "### Wikidata user-agent header\n", + "\n", + "Wikidata policy requires a user-agent header. See https://meta.wikimedia.org/wiki/User-Agent_policy. However, at present this policy is not strictly enforced." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "17ba657e-789d-40e1-b4b7-4f29ba06fe79", + "metadata": {}, + "outputs": [], + "source": [ + "wikidata_user_agent_header = None if not config.has_section('WIKIDATA') else config['WIKIDATA']['WIKIDAtA_USER_AGENT_HEADER']" + ] + }, + { + "cell_type": "markdown", + "id": "db08d308-050a-4fc8-93c9-8de4ae977ac3", + "metadata": {}, + "source": [ + "### Enable tracing if desired" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "77d2da08-fccd-4676-b77e-c0e89bf343cb", + "metadata": {}, + "outputs": [], + "source": [ + "#import os\n", + "#os.environ[\"LANGCHAIN_HANDLER\"] = \"langchain\"\n", + "#os.environ[\"LANGCHAIN_SESSION\"] = \"default\" # Make sure this session actually exists. " + ] + }, + { + "cell_type": "markdown", + "id": "3dbc5bfc-48ce-4f90-873c-7336b21300c6", + "metadata": {}, + "source": [ + "# Tools\n", + "\n", + "Three tools are provided for this simple agent:\n", + "* `ItemLookup`: for finding the q-number of an item\n", + "* `PropertyLookup`: for finding the p-number of a property\n", + "* `SparqlQueryRunner`: for running a sparql query" + ] + }, + { + "cell_type": "markdown", + "id": "1f801b4e-6576-4914-aa4f-6f4c4e3c7924", + "metadata": { + "tags": [] + }, + "source": [ + "## Item and Property lookup\n", + "\n", + "Item and Property lookup are implemented in a single method, using an elastic search endpoint. Not all wikibase instances have it, but wikidata does, and that's where we'll start." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "42d23f0a-1c74-4c9c-85f2-d0e24204e96a", + "metadata": {}, + "outputs": [], + "source": [ + "def get_nested_value(o: dict, path: list) -> any:\n", + " current = o\n", + " for key in path:\n", + " try:\n", + " current = current[key]\n", + " except:\n", + " return None\n", + " return current\n", + "\n", + "import requests\n", + "\n", + "from typing import Optional\n", + "\n", + "def vocab_lookup(search: str, entity_type: str = \"item\",\n", + " url: str = \"https://www.wikidata.org/w/api.php\",\n", + " user_agent_header: str = wikidata_user_agent_header,\n", + " srqiprofile: str = None,\n", + " ) -> Optional[str]: \n", + " headers = {\n", + " 'Accept': 'application/json'\n", + " }\n", + " if wikidata_user_agent_header is not None:\n", + " headers['User-Agent'] = wikidata_user_agent_header\n", + " \n", + " if entity_type == \"item\":\n", + " srnamespace = 0\n", + " srqiprofile = \"classic_noboostlinks\" if srqiprofile is None else srqiprofile\n", + " elif entity_type == \"property\":\n", + " srnamespace = 120\n", + " srqiprofile = \"classic\" if srqiprofile is None else srqiprofile\n", + " else:\n", + " raise ValueError(\"entity_type must be either 'property' or 'item'\") \n", + " \n", + " params = {\n", + " \"action\": \"query\",\n", + " \"list\": \"search\",\n", + " \"srsearch\": search,\n", + " \"srnamespace\": srnamespace,\n", + " \"srlimit\": 1,\n", + " \"srqiprofile\": srqiprofile,\n", + " \"srwhat\": 'text',\n", + " \"format\": \"json\"\n", + " }\n", + " \n", + " response = requests.get(url, headers=headers, params=params)\n", + " \n", + " if response.status_code == 200:\n", + " title = get_nested_value(response.json(), ['query', 'search', 0, 'title'])\n", + " if title is None:\n", + " return f\"I couldn't find any {entity_type} for '{search}'. Please rephrase your request and try again\"\n", + " # if there is a prefix, strip it off\n", + " return title.split(':')[-1]\n", + " else:\n", + " return \"Sorry, I got an error. Please try again.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e52060fa-3614-43fb-894e-54e9b75d1e9f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Q4180017\n" + ] + } + ], + "source": [ + "print(vocab_lookup(\"Malin 1\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b23ab322-b2cf-404e-b36f-2bfc1d79b0d3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "P31\n" + ] + } + ], + "source": [ + "print(vocab_lookup(\"instance of\", entity_type=\"property\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "89020cc8-104e-42d0-ac32-885e590de515", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "I couldn't find any item for 'Ceci n'est pas un q-item'. Please rephrase your request and try again\n" + ] + } + ], + "source": [ + "print(vocab_lookup(\"Ceci n'est pas un q-item\"))" + ] + }, + { + "cell_type": "markdown", + "id": "78d66d8b-0e34-4d3f-a18d-c7284840ac76", + "metadata": {}, + "source": [ + "## Sparql runner " + ] + }, + { + "cell_type": "markdown", + "id": "c6f60069-fbe0-4015-87fb-0e487cd914e7", + "metadata": {}, + "source": [ + "This tool runs sparql - by default, wikidata is used." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b5b97a4d-2a39-4993-88d9-e7818c0a2853", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from typing import List, Dict, Any\n", + "import json\n", + "\n", + "def run_sparql(query: str, url='https://query.wikidata.org/sparql',\n", + " user_agent_header: str = wikidata_user_agent_header) -> List[Dict[str, Any]]:\n", + " headers = {\n", + " 'Accept': 'application/json'\n", + " }\n", + " if wikidata_user_agent_header is not None:\n", + " headers['User-Agent'] = wikidata_user_agent_header\n", + "\n", + " response = requests.get(url, headers=headers, params={'query': query, 'format': 'json'})\n", + "\n", + " if response.status_code != 200:\n", + " return \"That query failed. Perhaps you could try a different one?\"\n", + " results = get_nested_value(response.json(),['results', 'bindings'])\n", + " return json.dumps(results)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "149722ec-8bc1-4d4f-892b-e4ddbe8444c1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'[{\"count\": {\"datatype\": \"http://www.w3.org/2001/XMLSchema#integer\", \"type\": \"literal\", \"value\": \"20\"}}]'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run_sparql(\"SELECT (COUNT(?children) as ?count) WHERE { wd:Q1339 wdt:P40 ?children . }\")" + ] + }, + { + "cell_type": "markdown", + "id": "9f0302fd-ba35-4acc-ba32-1d7c9295c898", + "metadata": {}, + "source": [ + "# Agent" + ] + }, + { + "cell_type": "markdown", + "id": "3122a961-9673-4a52-b1cd-7d62fbdf8d96", + "metadata": {}, + "source": [ + "## Wrap the tools" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "cc41ae88-2e53-4363-9878-28b26430cb1e", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.agents import Tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser\n", + "from langchain.prompts import StringPromptTemplate\n", + "from langchain import OpenAI, LLMChain\n", + "from typing import List, Union\n", + "from langchain.schema import AgentAction, AgentFinish\n", + "import re" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "2810a3ce-b9c6-47ee-8068-12ca967cd0ea", + "metadata": {}, + "outputs": [], + "source": [ + "# Define which tools the agent can use to answer user queries\n", + "tools = [\n", + " Tool(\n", + " name = \"ItemLookup\",\n", + " func=(lambda x: vocab_lookup(x, entity_type=\"item\")),\n", + " description=\"useful for when you need to know the q-number for an item\"\n", + " ),\n", + " Tool(\n", + " name = \"PropertyLookup\",\n", + " func=(lambda x: vocab_lookup(x, entity_type=\"property\")),\n", + " description=\"useful for when you need to know the p-number for a property\"\n", + " ),\n", + " Tool(\n", + " name = \"SparqlQueryRunner\",\n", + " func=run_sparql,\n", + " description=\"useful for getting results from a wikibase\"\n", + " ) \n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "ab0f2778-a195-4a4a-a5b4-c1e809e1fb7b", + "metadata": {}, + "source": [ + "## Prompts" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "7bd4ba4f-57d6-4ceb-b932-3cb0d0509a24", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the base template\n", + "template = \"\"\"\n", + "Answer the following questions by running a sparql query against a wikibase where the p and q items are \n", + "completely unknown to you. You will need to discover the p and q items before you can generate the sparql.\n", + "Do not assume you know the p and q items for any concepts. Always use tools to find all p and q items.\n", + "After you generate the sparql, you should run it. The results will be returned in json. \n", + "Summarize the json results in natural language.\n", + "\n", + "You may assume the following prefixes:\n", + "PREFIX wd: \n", + "PREFIX wdt: \n", + "PREFIX p: \n", + "PREFIX ps: \n", + "\n", + "When generating sparql:\n", + "* Try to avoid \"count\" and \"filter\" queries if possible\n", + "* Never enclose the sparql in back-quotes\n", + "\n", + "You have access to the following tools:\n", + "\n", + "{tools}\n", + "\n", + "Use the following format:\n", + "\n", + "Question: the input question for which you must provide a natural language answer\n", + "Thought: you should always think about what to do\n", + "Action: the action to take, should be one of [{tool_names}]\n", + "Action Input: the input to the action\n", + "Observation: the result of the action\n", + "... (this Thought/Action/Action Input/Observation can repeat N times)\n", + "Thought: I now know the final answer\n", + "Final Answer: the final answer to the original input question\n", + "\n", + "Question: {input}\n", + "{agent_scratchpad}\"\"\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "7e8d771a-64bb-4ec8-b472-6a9a40c6dd38", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up a prompt template\n", + "class CustomPromptTemplate(StringPromptTemplate):\n", + " # The template to use\n", + " template: str\n", + " # The list of tools available\n", + " tools: List[Tool]\n", + " \n", + " def format(self, **kwargs) -> str:\n", + " # Get the intermediate steps (AgentAction, Observation tuples)\n", + " # Format them in a particular way\n", + " intermediate_steps = kwargs.pop(\"intermediate_steps\")\n", + " thoughts = \"\"\n", + " for action, observation in intermediate_steps:\n", + " thoughts += action.log\n", + " thoughts += f\"\\nObservation: {observation}\\nThought: \"\n", + " # Set the agent_scratchpad variable to that value\n", + " kwargs[\"agent_scratchpad\"] = thoughts\n", + " # Create a tools variable from the list of tools provided\n", + " kwargs[\"tools\"] = \"\\n\".join([f\"{tool.name}: {tool.description}\" for tool in self.tools])\n", + " # Create a list of tool names for the tools provided\n", + " kwargs[\"tool_names\"] = \", \".join([tool.name for tool in self.tools])\n", + " return self.template.format(**kwargs)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "f97dca78-fdde-4a70-9137-e34a21d14e64", + "metadata": {}, + "outputs": [], + "source": [ + "prompt = CustomPromptTemplate(\n", + " template=template,\n", + " tools=tools,\n", + " # This omits the `agent_scratchpad`, `tools`, and `tool_names` variables because those are generated dynamically\n", + " # This includes the `intermediate_steps` variable because that is needed\n", + " input_variables=[\"input\", \"intermediate_steps\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "12c57d77-3c1e-4cde-9a83-7d2134392479", + "metadata": {}, + "source": [ + "## Output parser \n", + "This is unchanged from langchain docs" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "42da05eb-c103-4649-9d20-7143a8880721", + "metadata": {}, + "outputs": [], + "source": [ + "class CustomOutputParser(AgentOutputParser):\n", + " \n", + " def parse(self, llm_output: str) -> Union[AgentAction, AgentFinish]:\n", + " # Check if agent should finish\n", + " if \"Final Answer:\" in llm_output:\n", + " return AgentFinish(\n", + " # Return values is generally always a dictionary with a single `output` key\n", + " # It is not recommended to try anything else at the moment :)\n", + " return_values={\"output\": llm_output.split(\"Final Answer:\")[-1].strip()},\n", + " log=llm_output,\n", + " )\n", + " # Parse out the action and action input\n", + " regex = r\"Action: (.*?)[\\n]*Action Input:[\\s]*(.*)\"\n", + " match = re.search(regex, llm_output, re.DOTALL)\n", + " if not match:\n", + " raise ValueError(f\"Could not parse LLM output: `{llm_output}`\")\n", + " action = match.group(1).strip()\n", + " action_input = match.group(2)\n", + " # Return the action and action input\n", + " return AgentAction(tool=action, tool_input=action_input.strip(\" \").strip('\"'), log=llm_output)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "d2b4d710-8cc9-4040-9269-59cf6c5c22be", + "metadata": {}, + "outputs": [], + "source": [ + "output_parser = CustomOutputParser()" + ] + }, + { + "cell_type": "markdown", + "id": "48a758cb-93a7-4555-b69a-896d2d43c6f0", + "metadata": {}, + "source": [ + "## Specify the LLM model" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "72988c79-8f60-4b0f-85ee-6af32e8de9c2", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.chat_models import ChatOpenAI\n", + "llm = ChatOpenAI(model=\"gpt-4\", temperature=0)" + ] + }, + { + "cell_type": "markdown", + "id": "95685d14-647a-4e24-ae2c-a8dd1e364921", + "metadata": {}, + "source": [ + "## Agent and agent executor" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "13d55765-bfa1-43b3-b7cb-00f52ebe7747", + "metadata": {}, + "outputs": [], + "source": [ + "# LLM chain consisting of the LLM and a prompt\n", + "llm_chain = LLMChain(llm=llm, prompt=prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "b3f7ac3c-398e-49f9-baed-554f49a191c3", + "metadata": {}, + "outputs": [], + "source": [ + "tool_names = [tool.name for tool in tools]\n", + "agent = LLMSingleActionAgent(\n", + " llm_chain=llm_chain, \n", + " output_parser=output_parser,\n", + " stop=[\"\\nObservation:\"], \n", + " allowed_tools=tool_names\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "65740577-272e-4853-8d47-b87784cfaba0", + "metadata": {}, + "outputs": [], + "source": [ + "agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True)" + ] + }, + { + "cell_type": "markdown", + "id": "66e3d13b-77cf-41d3-b541-b54535c14459", + "metadata": {}, + "source": [ + "## Run it!" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "6e97a07c-d7bf-4a35-9ab2-b59ae865c62c", + "metadata": {}, + "outputs": [], + "source": [ + "# If you prefer in-line tracing, uncomment this line\n", + "# agent_executor.agent.llm_chain.verbose = True" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "a11ca60d-f57b-4fe8-943e-a258e37463c7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mThought: I need to find the Q number for J.S. Bach.\n", + "Action: ItemLookup\n", + "Action Input: J.S. Bach\u001b[0m\n", + "\n", + "Observation:\u001b[36;1m\u001b[1;3mQ1339\u001b[0m\u001b[32;1m\u001b[1;3mI need to find the P number for children.\n", + "Action: PropertyLookup\n", + "Action Input: children\u001b[0m\n", + "\n", + "Observation:\u001b[33;1m\u001b[1;3mP1971\u001b[0m\u001b[32;1m\u001b[1;3mNow I can query the number of children J.S. Bach had.\n", + "Action: SparqlQueryRunner\n", + "Action Input: SELECT ?children WHERE { wd:Q1339 wdt:P1971 ?children }\u001b[0m\n", + "\n", + "Observation:\u001b[38;5;200m\u001b[1;3m[{\"children\": {\"datatype\": \"http://www.w3.org/2001/XMLSchema#decimal\", \"type\": \"literal\", \"value\": \"20\"}}]\u001b[0m\u001b[32;1m\u001b[1;3mI now know the final answer.\n", + "Final Answer: J.S. Bach had 20 children.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "'J.S. Bach had 20 children.'" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent_executor.run(\"How many children did J.S. Bach have?\")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "d0b42a41-996b-4156-82e4-f0651a87ee34", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mThought: To find Hakeem Olajuwon's Basketball-Reference.com NBA player ID, I need to first find his Wikidata item (Q-number) and then query for the relevant property (P-number).\n", + "Action: ItemLookup\n", + "Action Input: Hakeem Olajuwon\u001b[0m\n", + "\n", + "Observation:\u001b[36;1m\u001b[1;3mQ273256\u001b[0m\u001b[32;1m\u001b[1;3mNow that I have Hakeem Olajuwon's Wikidata item (Q273256), I need to find the P-number for the Basketball-Reference.com NBA player ID property.\n", + "Action: PropertyLookup\n", + "Action Input: Basketball-Reference.com NBA player ID\u001b[0m\n", + "\n", + "Observation:\u001b[33;1m\u001b[1;3mP2685\u001b[0m\u001b[32;1m\u001b[1;3mNow that I have both the Q-number for Hakeem Olajuwon (Q273256) and the P-number for the Basketball-Reference.com NBA player ID property (P2685), I can run a SPARQL query to get the ID value.\n", + "Action: SparqlQueryRunner\n", + "Action Input: \n", + "SELECT ?playerID WHERE {\n", + " wd:Q273256 wdt:P2685 ?playerID .\n", + "}\u001b[0m\n", + "\n", + "Observation:\u001b[38;5;200m\u001b[1;3m[{\"playerID\": {\"type\": \"literal\", \"value\": \"o/olajuha01\"}}]\u001b[0m\u001b[32;1m\u001b[1;3mI now know the final answer\n", + "Final Answer: Hakeem Olajuwon's Basketball-Reference.com NBA player ID is \"o/olajuha01\".\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "'Hakeem Olajuwon\\'s Basketball-Reference.com NBA player ID is \"o/olajuha01\".'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent_executor.run(\"What is the Basketball-Reference.com NBA player ID of Hakeem Olajuwon?\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05fb3a3e-8a9f-482d-bd54-4c6e60ef60dd", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "conda210", + "language": "python", + "name": "conda210" + }, + "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.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}