From 9129318466245ae4c219ab52c8b0f696b65b10fb Mon Sep 17 00:00:00 2001 From: Boris Date: Tue, 11 Jul 2023 07:11:21 -0700 Subject: [PATCH] CPAL (#6255) # Causal program-aided language (CPAL) chain ## Motivation This builds on the recent [PAL](https://arxiv.org/abs/2211.10435) to stop LLM hallucination. The problem with the [PAL](https://arxiv.org/abs/2211.10435) approach is that it hallucinates on a math problem with a nested chain of dependence. The innovation here is that this new CPAL approach includes causal structure to fix hallucination. For example, using the below word problem, PAL answers with 5, and CPAL answers with 13. "Tim buys the same number of pets as Cindy and Boris." "Cindy buys the same number of pets as Bill plus Bob." "Boris buys the same number of pets as Ben plus Beth." "Bill buys the same number of pets as Obama." "Bob buys the same number of pets as Obama." "Ben buys the same number of pets as Obama." "Beth buys the same number of pets as Obama." "If Obama buys one pet, how many pets total does everyone buy?" The CPAL chain represents the causal structure of the above narrative as a causal graph or DAG, which it can also plot, as shown below. ![complex-graph](https://github.com/hwchase17/langchain/assets/367522/d938db15-f941-493d-8605-536ad530f576) . The two major sections below are: 1. Technical overview 2. Future application Also see [this jupyter notebook](https://github.com/borisdev/langchain/blob/master/docs/extras/modules/chains/additional/cpal.ipynb) doc. ## 1. Technical overview ### CPAL versus PAL Like [PAL](https://arxiv.org/abs/2211.10435), CPAL intends to reduce large language model (LLM) hallucination. The CPAL chain is different from the PAL chain for a couple of reasons. * CPAL adds a causal structure (or DAG) to link entity actions (or math expressions). * The CPAL math expressions are modeling a chain of cause and effect relations, which can be intervened upon, whereas for the PAL chain math expressions are projected math identities. PAL's generated python code is wrong. It hallucinates when complexity increases. ```python def solution(): """Tim buys the same number of pets as Cindy and Boris.Cindy buys the same number of pets as Bill plus Bob.Boris buys the same number of pets as Ben plus Beth.Bill buys the same number of pets as Obama.Bob buys the same number of pets as Obama.Ben buys the same number of pets as Obama.Beth buys the same number of pets as Obama.If Obama buys one pet, how many pets total does everyone buy?""" obama_pets = 1 tim_pets = obama_pets cindy_pets = obama_pets + obama_pets boris_pets = obama_pets + obama_pets total_pets = tim_pets + cindy_pets + boris_pets result = total_pets return result # math result is 5 ``` CPAL's generated python code is correct. ```python story outcome data name code value depends_on 0 obama pass 1.0 [] 1 bill bill.value = obama.value 1.0 [obama] 2 bob bob.value = obama.value 1.0 [obama] 3 ben ben.value = obama.value 1.0 [obama] 4 beth beth.value = obama.value 1.0 [obama] 5 cindy cindy.value = bill.value + bob.value 2.0 [bill, bob] 6 boris boris.value = ben.value + beth.value 2.0 [ben, beth] 7 tim tim.value = cindy.value + boris.value 4.0 [cindy, boris] query data { "question": "how many pets total does everyone buy?", "expression": "SELECT SUM(value) FROM df", "llm_error_msg": "" } # query result is 13 ``` Based on the comments below, CPAL's intended location in the library is `experimental/chains/cpal` and PAL's location is`chains/pal`. ### CPAL vs Graph QA Both the CPAL chain and the Graph QA chain extract entity-action-entity relations into a DAG. The CPAL chain is different from the Graph QA chain for a few reasons. * Graph QA does not connect entities to math expressions * Graph QA does not associate actions in a sequence of dependence. * Graph QA does not decompose the narrative into these three parts: 1. Story plot or causal model 4. Hypothetical question 5. Hypothetical condition ### Evaluation Preliminary evaluation on simple math word problems shows that this CPAL chain generates less hallucination than the PAL chain on answering questions about a causal narrative. Two examples are in [this jupyter notebook](https://github.com/borisdev/langchain/blob/master/docs/extras/modules/chains/additional/cpal.ipynb) doc. ## 2. Future application ### "Describe as Narrative, Test as Code" The thesis here is that the Describe as Narrative, Test as Code approach allows you to represent a causal mental model both as code and as a narrative, giving you the best of both worlds. #### Why describe a causal mental mode as a narrative? The narrative form is quick. At a consensus building meeting, people use narratives to persuade others of their causal mental model, aka. plan. You can share, version control and index a narrative. #### Why test a causal mental model as a code? Code is testable, complex narratives are not. Though fast, narratives are problematic as their complexity increases. The problem is LLMs and humans are prone to hallucination when predicting the outcomes of a narrative. The cost of building a consensus around the validity of a narrative outcome grows as its narrative complexity increases. Code does not require tribal knowledge or social power to validate. Code is composable, complex narratives are not. The answer of one CPAL chain can be the hypothetical conditions of another CPAL Chain. For stochastic simulations, a composable plan can be integrated with the [DoWhy library](https://github.com/py-why/dowhy). Lastly, for the futuristic folk, a composable plan as code allows ordinary community folk to design a plan that can be integrated with a blockchain for funding. An explanation of a dependency planning application is [here.](https://github.com/borisdev/cpal-llm-chain-demo) --- Twitter handle: @boris_dev --------- Co-authored-by: Boris Dev --- .../modules/chains/additional/cpal.ipynb | 916 ++++++++++++++++++ langchain/experimental/cpal/README.md | 4 + langchain/experimental/cpal/__init__.py | 0 langchain/experimental/cpal/base.py | 271 ++++++ langchain/experimental/cpal/constants.py | 7 + langchain/experimental/cpal/models.py | 245 +++++ .../experimental/cpal/templates/__init__.py | 0 .../cpal/templates/univariate/__init__.py | 0 .../cpal/templates/univariate/causal.py | 113 +++ .../cpal/templates/univariate/intervention.py | 59 ++ .../cpal/templates/univariate/narrative.py | 79 ++ .../cpal/templates/univariate/query.py | 270 ++++++ langchain/graphs/networkx_graph.py | 45 + tests/integration_tests/chains/test_cpal.py | 554 +++++++++++ 14 files changed, 2563 insertions(+) create mode 100644 docs/extras/modules/chains/additional/cpal.ipynb create mode 100644 langchain/experimental/cpal/README.md create mode 100644 langchain/experimental/cpal/__init__.py create mode 100644 langchain/experimental/cpal/base.py create mode 100644 langchain/experimental/cpal/constants.py create mode 100644 langchain/experimental/cpal/models.py create mode 100644 langchain/experimental/cpal/templates/__init__.py create mode 100644 langchain/experimental/cpal/templates/univariate/__init__.py create mode 100644 langchain/experimental/cpal/templates/univariate/causal.py create mode 100644 langchain/experimental/cpal/templates/univariate/intervention.py create mode 100644 langchain/experimental/cpal/templates/univariate/narrative.py create mode 100644 langchain/experimental/cpal/templates/univariate/query.py create mode 100644 tests/integration_tests/chains/test_cpal.py diff --git a/docs/extras/modules/chains/additional/cpal.ipynb b/docs/extras/modules/chains/additional/cpal.ipynb new file mode 100644 index 0000000000..f7daca6ed4 --- /dev/null +++ b/docs/extras/modules/chains/additional/cpal.ipynb @@ -0,0 +1,916 @@ +{ + "cells": [ + { + "attachments": { + "4caf0580-4f46-4293-8b7a-5d6e7c9df990.png": { + "image/png": "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q2cW7m6U310JsDvJ559/Ll999ZV8/fXXFhz/hx9+kB9//NEEKdJ94YUXZP/995f55ptPLrroIplooomkd+/eto52ggkm6C553ssJAqNGjRL8oR2cdtppcsEFF8gCCywg//znP81qAksFdiXCnDrqHG1gkkkmkcknn7ymd3izxa7Xpscdd1xBO0Qgf7T5JDTmZnnmc+VBgEK1JHVdqyOqN8/5wQcfyKuvvipvvPGG7Y/5zjvvyHvvvScffvihfPzxxyZQO4ENHexUU00l00wzjUw33XQy44wzykwzzSSzzjqrzDbbbDLnnHPaXFknefDdzhDAwOjll1+utIO33npLvB189NFH1g4wuGqXIFi9/meYYQbBmtJZZplFZp99dplrrrlsAFYvbQjNWlvOQYjCFE1hWg85Xk8CAQrVJFDOUB5h4Yr9Tc844wx5+umn5dlnn7XNxKFhfvHFFxWOJ554YhN6EH7TTjutCcM+ffqYtgGtA/chJKGNQCvxtYsw/e6www5y6qmnynfffSfffPONaTRI+7PPPpNPPvlE0DlDgL/77rv22zOFhjPvvPOa1rPQQgvJIossIosvvrjl5c/wGB0CEKBDhgyRJ598Up555hl57rnn5Pnnn7f9Uj2X6aefvtIOIAwxKJpiiilksskms3qBFnrppZfKEkssIUsuuaS9hjbw008/mfb47bffmlXjyy+/tEHZp59+2qX+sck5tEynmWeeWRZccEFZeOGFZbHFFrN00e7C7defpTB1JHjMAgIUqlmohQR5wPIbLIiHSRaaqBO0xfnnn9+E2dxzzy1zzDGHaY3oPNslOEQ1qzVA8IIfaMjQkIYPH24d+7BhwyrZQ7hiDSQ0Faw9hLZDag8BtIP777/f2gJ+w3QPQt3DhIu2gN+wGkB71E0S2suohbegCb/yyitW/xjcoe4h4LEUDAS+wMdTTz1l5xSmBgP/ZQwBCtWMVUjU7IwcOVJuu+02uf322+Wuu+4SaAromCCc4FQCLfPPf/6zmeKizjuK9NDZoxN94oknzOHkscceM1M00kYZ1lxzTVl77bVNq4kiv6KmAS3wpptukltuuUXuuOMOsxZgDhIDFG8L0DJhfcgawZLy+OOPy9lnn21z9c7fKqusIuuss46st956As2WRASygACFahZqIWIeYGq9+uqr5dprr7UOFMlDy1tttdUEHRG0PJhY80ovvvii3HfffXLPPffInXfeaR7GcIjaeOONZdNNNzVtO69li5rvW2+9Va688kq55pprTBudZ555bCCy+uqrW1vwZSdR5xtnekOHDq3UPdoBCN7mf/zjH2WLLbYwZ7g482faRKA7BChUu0MnZ/dg1h00aJBccsklJmighWywwQay/vrrmxNIzorTFLvQwKB93XjjjXL99dfb/C062G222Ua22267XA8emgKgxkPvv/++eeLCxD9C499CkG6yySay0UYbFU6jx9zsDTfcINddd53cfffd5uCGeof1BRo4iQgkjoA6KZByjsDAgQMDXeBue6jq/FegywkCnZvMealaZ19NxYEOKgLVyA0LdaQJDjrooEAdoVpPLIdv6PxjoM5hVnZ1GAtUsAQ6r53DkrTH8ptvvmltX72IDQO1yAQqbNtLjG8RgTYRwHoyUk4RUM/dQJegWAeic0uBams5LUn0bKujS7DnnnsGOmds+Oy2226BLguJPqMMpIiyqmZu5VQv3eDYY48N1MM6A5ylx4KavAO1WBgmGHBSuKZXF2XLmUI1hzWu5t1A1/JZh7H11lsHuhQih6VIhmUNTBDonp+BLscwvDQgRaABK5LJPOZcULa9997byqXe24EGYIg5x/wlr05ZwaqrrmoYrbHGGoFGV8pfIchxrhCgUM1RdUF4umlT58gC9YrMEffpsqrLMgJd4xio53Mw9dRTB+edd166DHWYO0z+ul40UA9eGzR0mFzhX4emqmueTbjus88+gQauKHyZWcB0EKBQTQf3lnOFQNAJ90C9eANdItPy+3zhNwQwvzpgwADDct111w1ee+21XEGjS6SCLbfc0vjfaqutAl3bmSv+02b2uOOOM+zUeSu4995702aH+RcQAQrVjFeqBkII4HABgQrBSooGgZtvvjmAUxfmXC+++OJoEo05FV1CFGg4xwAOWBq9KObcips85qBXXnll+6Y0TnFxC8qSpYIAhWoqsDeXqS4VCDQIfaAh+wJdLtPcS3yqaQQ0ilNFa91vv/2afi+NB8866ywTArq+NNCQfmmwULg8DzzwQMMUlgsSEYgKAQrVqJCMOB149kI73WyzzYLRo0dHnDqTCyNw8sknG9YaPCB8OTO/jzrqKONv1113zQxPRWEES7Dwna211lqBrnkuSrFYjhQRoFBNEfx6WR9//PH2oe+11171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jqAJ0FoXWWSRbovfrAm220TavPnjjz/WFKhIjgK1TVD5GhFICAEK1YSAZjbZQQCC9dFHH+12rWeaQvWxxx6rCdbyyy9vQpVzqDXh4UUikAkEKFQzUQ1kIg0E6s2zwkPYHZuS5queMIe5d/DgwUmzw/yIABFoEQEK1RYB4+PFQgBaH4QraPzxx7cjPITrCTd7IMZ/4Xw9TCHnT2MEnEkTgYgRoKNSxIAyuXwiAGF28MEHS9j0CmELU3EtGjlypLz33nvy0UcfyWeffSajRo2Sb7/91pyfIJQhEMcZZxyZYIIJpHfv3jL55JPL1FNPLX379pUZZ5yxVpJ2DfmFtWQK1LpQ8QYRyCQCFKqZrBYylRYCHmwf+UOgHXjggTJkyBB55plnZOjQoTJ8+HB59dVXTYjW4nHccceVnj172npYhD385ZdfxngM92effXaZe+65Zf7555eFF15YllhiCRO4rp1i/pTm3jGg4wUikHkEKFQzX0VkMGkEtt9+e7nwwgtNo3znnXcq2UMQzjPPPDLHHHPILLPMIjPMMIMJwj59+shkk00mE000UeVZ/4FlO1999ZVAs/34449Nu33rrbfk9ddfl5deekmef/55QRAHENJ79913ZdNNN7X8a6Xn6fJIBIhANhGgUM1mvZCrhBGA+ffaa6+VG264QT744APTNqFlHnnkkbL00kubJgkzbhz07LPPyuOPPy7XX3+9acVff/21ZbP++uvLxhtvLH/84x/NlBxH3kyTCBCBaBGgUI0WT6aWIwS++OILueCCC2TgwIHy4osvyhRTTCEbbrihrLvuurLGGmvY/Gq9OdU4ignBjvyefPJJue222+TGG280kzPmZbHdGzToRRddNI6smSYRIAIRIUChGhGQTCY/CLz99tty6qmnyumnn27bvUEj/NOf/mRaYdZK8dRTT8mll14qF198sWAQsPbaa8see+whq622WtZYJT9EgAgoAhSqbAalQQBC6dhjj5UTTjhBsCH5rrvuKrvssovNkWYdBJiizznnHDnjjDPMWWqttdaSgw46SJZddtmss07+iECpEKBQLVV1l7ewZ555phxyyCGm7e27776y3377yVRTTZVLQOBE9a9//cu8kHfeeWc55phjbMlOLgtDpolAwRCgUC1YhbI4XRF4+eWXZc8995S77rpLtthiC9tTFV68RSBo3YcddpjA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GlnuisBOO+0k5557rjz44IO2vVrXu+U+W3755eWdd94RbJdHB6VytwWWPhkEKFSTwZm5xIgAIhottdRS8sknn5hgRcQlkphT0kUXXSR33XWXrLbaaoSECBCBBBCg+TcBkJlFvAiMNdZYtuk3dnvBnqce6SjeXLOd+j777CMQqPCOpkDNdl2Ru2IhQKFarPosbWmw+fZ1110nCAMIZ5wk9yjNGuh77bWXzTEfd9xxtlF61vgjP0SgyAjQ/Fvk2i1h2bC8RkMG2v6ll112mSyxxBKlQmGbbbYxrf1f//pXJUZxqQBgYYlAyghQU025Aph9tAgss8wycv/999sm5ssuu6wMHDgw2gwymtqLL74oKPsll1wi5513HgVqRuuJbBUfAQrV4tdx6Uo477zzyqOPPmoh+QYMGCDwDkbs3qLSOeecI4suuqh89NFHct9998kOO+xQ1KKyXEQg8whQqGa+ishgOwhMNNFEctVVVwm2a8NyG90RRnR3lnaSyuw70E7XX399mzeFgxa2wltppZUyyy8ZIwJlQIBCtQy1XOIyIpIQ9hCda665ZMstt5S1117bwhrmGRLsloOADvPNN588+eSTMmjQILn00ktlsskmy3OxyDsRKAQCFKqFqEYWojsEsCk3NhfH/Oqrr74qmGvdbLPN5JFHHunutczdw2bohxxyiMwwwwyCuMf77befeTvDOYlEBIhANhCg92826oFcJIjAKaecIoiHi0hDMJdi3nXrrbdOkIPWssL8MAYE559/vr248847C9ahzjbbbK0lxKeJABGIHQEK1dghZgZZReCCCy4wT9khQ4ZYCD9or9hvdNVVV02d5ZdeekluuOEGueaaa8x8PeWUU5oDEpyuZpppptT5IwNEgAjURoBCtTYuvFoiBKAJYgPvq6++WkaOHCl9+vSxKEQrr7yy9O/fPxGN8MsvvxRsy4blQNhx54UXXrAaWGeddWSLLbaw+eASVQmLSgRyiwCFam6rjozHgcCdd94pt99+u8XLxfwraLrpphPsR4q5WSzXmWOOOWTWWWeViSeeuGUWEKf4rbfesrldaKPDhg2TZ555Rp5//nlLa5JJJjFNec0115R1111XoKGSiAARyA8CFKr5qStymjACb7zxhjkzwTz81FNPydChQwXxhZ2w68s000xjmu2kk04qE044oYw33niCWMRBENizo0ePlq+++ko+++wzC/j//vvv++t2xH6wiyyyiEV+QvAGbAxAIgJEIL8IUKjmt+7IeQoIvPLKK+ZxC23zvffekw8//NBMxqNGjZJvv/1Wvv/+e/nll19MsI4zzjgW2al3794yxRRTyNRTT21aL+ZE4WSEeMXtaLspFJtZEgEi0CQCFKpNAsXHiAARIAJEgAg0QoDrVBshxPtEgAgQASJABJpEgEK1SaD4GBEgAkSACBCBRghQqDZCiPeJABEgAkSACDSJAIVqk0DxMSJABIgAESACjRCgUG2EEO8TASJABIgAEWgSAQrVJoHiY0SACBABIkAEGiFAodoIId4nAkSACBABItAkAhSqTQLFx4gAESACRIAINEKAQrURQrxPBIgAESACRKBJBChUmwSKjxEBIkAEiAARaIQAhWojhHifCBABIkAEiECTCPwPC/Af643/JP4AAAAASUVORK5CYII=" + } + }, + "cell_type": "markdown", + "id": "82f3f65d-fbcb-4e8e-b04b-959856283643", + "metadata": {}, + "source": [ + "# Causal program-aided language (CPAL) chain\n", + "\n", + "The [PR's description](https://github.com/hwchase17/langchain/pull/6255) contains the overview.\n", + "\n", + "Using the CPAL chain, the LLM translated this\n", + "\n", + " \"Tim buys the same number of pets as Cindy and Boris.\"\n", + " \"Cindy buys the same number of pets as Bill plus Bob.\"\n", + " \"Boris buys the same number of pets as Ben plus Beth.\"\n", + " \"Bill buys the same number of pets as Obama.\"\n", + " \"Bob buys the same number of pets as Obama.\"\n", + " \"Ben buys the same number of pets as Obama.\"\n", + " \"Beth buys the same number of pets as Obama.\"\n", + " \"If Obama buys one pet, how many pets total does everyone buy?\"\n", + "\n", + "\n", + "into this\n", + "\n", + "![complex-graph.png](attachment:4caf0580-4f46-4293-8b7a-5d6e7c9df990.png).\n", + "\n", + "Outline of code examples demoed in this notebook.\n", + "\n", + "1. CPAL's value against hallucination: CPAL vs PAL \n", + " 1.1 Complex narrative \n", + " 1.2 Unanswerable math word problem \n", + "2. CPAL's three types of causal diagrams ([The Book of Why](https://en.wikipedia.org/wiki/The_Book_of_Why)). \n", + " 2.1 Mediator \n", + " 2.2 Collider \n", + " 2.3 Confounder " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1370e40f", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import SVG\n", + "\n", + "from langchain.experimental.cpal.base import CPALChain\n", + "from langchain.chains import PALChain\n", + "from langchain import OpenAI\n", + "\n", + "llm = OpenAI(temperature=0, max_tokens=512)\n", + "cpal_chain = CPALChain.from_univariate_prompt(llm=llm, verbose=True)\n", + "pal_chain = PALChain.from_math_prompt(llm=llm, verbose=True)" + ] + }, + { + "cell_type": "markdown", + "id": "858a87d9-a9bd-4850-9687-9af4b0856b62", + "metadata": {}, + "source": [ + "## 1. CPAL's value against hallucination: CPAL vs PAL" + ] + }, + { + "cell_type": "markdown", + "id": "496403c5-d268-43ae-8852-2bd9903ce444", + "metadata": {}, + "source": [ + "### 1.1 Complex narrative\n", + "\n", + "Takeaway: PAL hallucinates, CPAL does not hallucinate." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d5dad768-2892-4825-8093-9b840f643a8a", + "metadata": {}, + "outputs": [], + "source": [ + "question = (\n", + " \"Tim buys the same number of pets as Cindy and Boris.\"\n", + " \"Cindy buys the same number of pets as Bill plus Bob.\"\n", + " \"Boris buys the same number of pets as Ben plus Beth.\"\n", + " \"Bill buys the same number of pets as Obama.\"\n", + " \"Bob buys the same number of pets as Obama.\"\n", + " \"Ben buys the same number of pets as Obama.\"\n", + " \"Beth buys the same number of pets as Obama.\"\n", + " \"If Obama buys one pet, how many pets total does everyone buy?\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bbffa7a0-3c22-4a1d-ab2d-f230973073b0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mdef solution():\n", + " \"\"\"Tim buys the same number of pets as Cindy and Boris.Cindy buys the same number of pets as Bill plus Bob.Boris buys the same number of pets as Ben plus Beth.Bill buys the same number of pets as Obama.Bob buys the same number of pets as Obama.Ben buys the same number of pets as Obama.Beth buys the same number of pets as Obama.If Obama buys one pet, how many pets total does everyone buy?\"\"\"\n", + " obama_pets = 1\n", + " tim_pets = obama_pets\n", + " cindy_pets = obama_pets + obama_pets\n", + " boris_pets = obama_pets + obama_pets\n", + " total_pets = tim_pets + cindy_pets + boris_pets\n", + " result = total_pets\n", + " return result\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "'5'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pal_chain.run(question)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "35a70d1d-86f8-4abc-b818-fbd083f072e9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mstory outcome data\n", + " name code value depends_on\n", + "0 obama pass 1.0 []\n", + "1 bill bill.value = obama.value 1.0 [obama]\n", + "2 bob bob.value = obama.value 1.0 [obama]\n", + "3 ben ben.value = obama.value 1.0 [obama]\n", + "4 beth beth.value = obama.value 1.0 [obama]\n", + "5 cindy cindy.value = bill.value + bob.value 2.0 [bill, bob]\n", + "6 boris boris.value = ben.value + beth.value 2.0 [ben, beth]\n", + "7 tim tim.value = cindy.value + boris.value 4.0 [cindy, boris]\u001b[0m\n", + "\n", + "\u001b[36;1m\u001b[1;3mquery data\n", + "{\n", + " \"question\": \"how many pets total does everyone buy?\",\n", + " \"expression\": \"SELECT SUM(value) FROM df\",\n", + " \"llm_error_msg\": \"\"\n", + "}\u001b[0m\n", + "\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "13.0" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cpal_chain.run(question)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ccb6b2b0-9de6-4f66-a8fb-fc59229ee316", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "obama\n", + "\n", + "obama\n", + "\n", + "\n", + "\n", + "bill\n", + "\n", + "bill\n", + "\n", + "\n", + "\n", + "obama->bill\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "bob\n", + "\n", + "bob\n", + "\n", + "\n", + "\n", + "obama->bob\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "ben\n", + "\n", + "ben\n", + "\n", + "\n", + "\n", + "obama->ben\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "beth\n", + "\n", + "beth\n", + "\n", + "\n", + "\n", + "obama->beth\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cindy\n", + "\n", + "cindy\n", + "\n", + "\n", + "\n", + "bill->cindy\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "bob->cindy\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "boris\n", + "\n", + "boris\n", + "\n", + "\n", + "\n", + "ben->boris\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "beth->boris\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "tim\n", + "\n", + "tim\n", + "\n", + "\n", + "\n", + "cindy->tim\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "boris->tim\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# wait 20 secs to see display\n", + "cpal_chain.draw(path='web.svg')\n", + "SVG('web.svg')" + ] + }, + { + "cell_type": "markdown", + "id": "1f6f345a-bb16-4e64-83c4-cbbc789a8325", + "metadata": {}, + "source": [ + "### Unanswerable math\n", + "\n", + "Takeaway: PAL hallucinates, where CPAL, rather than hallucinate, answers with _\"unanswerable, narrative question and plot are incoherent\"_" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "068afd79-fd41-4ec2-b4d0-c64140dc413f", + "metadata": {}, + "outputs": [], + "source": [ + "question = (\n", + " \"Jan has three times the number of pets as Marcia.\"\n", + " \"Marcia has two more pets than Cindy.\"\n", + " \"If Cindy has ten pets, how many pets does Barak have?\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "02f77db2-72e8-46c2-90b3-5e37ca42f80d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mdef solution():\n", + " \"\"\"Jan has three times the number of pets as Marcia.Marcia has two more pets than Cindy.If Cindy has ten pets, how many pets does Barak have?\"\"\"\n", + " cindy_pets = 10\n", + " marcia_pets = cindy_pets + 2\n", + " jan_pets = marcia_pets * 3\n", + " result = jan_pets\n", + " return result\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "'36'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pal_chain.run(question)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "925958de-e998-4ffa-8b2e-5a00ddae5026", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mstory outcome data\n", + " name code value depends_on\n", + "0 cindy pass 10.0 []\n", + "1 marcia marcia.value = cindy.value + 2 12.0 [cindy]\n", + "2 jan jan.value = marcia.value * 3 36.0 [marcia]\u001b[0m\n", + "\n", + "\u001b[36;1m\u001b[1;3mquery data\n", + "{\n", + " \"question\": \"how many pets does barak have?\",\n", + " \"expression\": \"SELECT name, value FROM df WHERE name = 'barak'\",\n", + " \"llm_error_msg\": \"\"\n", + "}\u001b[0m\n", + "\n", + "unanswerable, query and outcome are incoherent\n", + "\n", + "outcome:\n", + " name code value depends_on\n", + "0 cindy pass 10.0 []\n", + "1 marcia marcia.value = cindy.value + 2 12.0 [cindy]\n", + "2 jan jan.value = marcia.value * 3 36.0 [marcia]\n", + "query:\n", + "{'question': 'how many pets does barak have?', 'expression': \"SELECT name, value FROM df WHERE name = 'barak'\", 'llm_error_msg': ''}\n" + ] + } + ], + "source": [ + "try:\n", + " cpal_chain.run(question)\n", + "except Exception as e_msg:\n", + " print(e_msg)" + ] + }, + { + "cell_type": "markdown", + "id": "095adc76", + "metadata": {}, + "source": [ + "### Basic math\n", + "\n", + "#### Causal mediator" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3ecf03fa-8350-4c4e-8080-84a307ba6ad4", + "metadata": {}, + "outputs": [], + "source": [ + "question = (\n", + " \"Jan has three times the number of pets as Marcia. \" \n", + " \"Marcia has two more pets than Cindy. \"\n", + " \"If Cindy has four pets, how many total pets do the three have?\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "74e49c47-3eed-4abe-98b7-8e97bcd15944", + "metadata": {}, + "source": [ + "---\n", + "PAL" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2e88395f-d014-4362-abb0-88f6800860bb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mdef solution():\n", + " \"\"\"Jan has three times the number of pets as Marcia. Marcia has two more pets than Cindy. If Cindy has four pets, how many total pets do the three have?\"\"\"\n", + " cindy_pets = 4\n", + " marcia_pets = cindy_pets + 2\n", + " jan_pets = marcia_pets * 3\n", + " total_pets = cindy_pets + marcia_pets + jan_pets\n", + " result = total_pets\n", + " return result\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "'28'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pal_chain.run(question)" + ] + }, + { + "cell_type": "markdown", + "id": "20ba6640-3d17-4b59-8101-aaba89d68cf4", + "metadata": {}, + "source": [ + "---\n", + "CPAL" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "312a0943-a482-4ed0-a064-1e7a72e9479b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mstory outcome data\n", + " name code value depends_on\n", + "0 cindy pass 4.0 []\n", + "1 marcia marcia.value = cindy.value + 2 6.0 [cindy]\n", + "2 jan jan.value = marcia.value * 3 18.0 [marcia]\u001b[0m\n", + "\n", + "\u001b[36;1m\u001b[1;3mquery data\n", + "{\n", + " \"question\": \"how many total pets do the three have?\",\n", + " \"expression\": \"SELECT SUM(value) FROM df\",\n", + " \"llm_error_msg\": \"\"\n", + "}\u001b[0m\n", + "\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "28.0" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cpal_chain.run(question)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "4466b975-ae2b-4252-972b-b3182a089ade", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "cindy\n", + "\n", + "cindy\n", + "\n", + "\n", + "\n", + "marcia\n", + "\n", + "marcia\n", + "\n", + "\n", + "\n", + "cindy->marcia\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "jan\n", + "\n", + "jan\n", + "\n", + "\n", + "\n", + "marcia->jan\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# wait 20 secs to see display\n", + "cpal_chain.draw(path='web.svg')\n", + "SVG('web.svg')" + ] + }, + { + "cell_type": "markdown", + "id": "29fa7b8a-75a3-4270-82a2-2c31939cd7e0", + "metadata": {}, + "source": [ + "### Causal collider" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "618eddac-f0ef-4ab5-90ed-72e880fdeba3", + "metadata": {}, + "outputs": [], + "source": [ + "question = (\n", + " \"Jan has the number of pets as Marcia plus the number of pets as Cindy. \" \n", + " \"Marcia has no pets. \"\n", + " \"If Cindy has four pets, how many total pets do the three have?\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a01563f3-7974-4de4-8bd9-0b7d710aa0d3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mstory outcome data\n", + " name code value depends_on\n", + "0 marcia pass 0.0 []\n", + "1 cindy pass 4.0 []\n", + "2 jan jan.value = marcia.value + cindy.value 4.0 [marcia, cindy]\u001b[0m\n", + "\n", + "\u001b[36;1m\u001b[1;3mquery data\n", + "{\n", + " \"question\": \"how many total pets do the three have?\",\n", + " \"expression\": \"SELECT SUM(value) FROM df\",\n", + " \"llm_error_msg\": \"\"\n", + "}\u001b[0m\n", + "\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "8.0" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cpal_chain.run(question)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "0fbe7243-0522-4946-b9a2-6e21e7c49a42", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "marcia\n", + "\n", + "marcia\n", + "\n", + "\n", + "\n", + "jan\n", + "\n", + "jan\n", + "\n", + "\n", + "\n", + "marcia->jan\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "cindy\n", + "\n", + "cindy\n", + "\n", + "\n", + "\n", + "cindy->jan\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# wait 20 secs to see display\n", + "cpal_chain.draw(path='web.svg')\n", + "SVG('web.svg')" + ] + }, + { + "cell_type": "markdown", + "id": "d4082538-ec03-44f0-aac3-07e03aad7555", + "metadata": {}, + "source": [ + "### Causal confounder" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "83932c30-950b-435a-b328-7993ce8cc6bd", + "metadata": {}, + "outputs": [], + "source": [ + "question = (\n", + " \"Jan has the number of pets as Marcia plus the number of pets as Cindy. \" \n", + " \"Marcia has two more pets than Cindy. \"\n", + " \"If Cindy has four pets, how many total pets do the three have?\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "570de307-7c6b-4fdc-80c3-4361daa8a629", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mstory outcome data\n", + " name code value depends_on\n", + "0 cindy pass 4.0 []\n", + "1 marcia marcia.value = cindy.value + 2 6.0 [cindy]\n", + "2 jan jan.value = cindy.value + marcia.value 10.0 [cindy, marcia]\u001b[0m\n", + "\n", + "\u001b[36;1m\u001b[1;3mquery data\n", + "{\n", + " \"question\": \"how many total pets do the three have?\",\n", + " \"expression\": \"SELECT SUM(value) FROM df\",\n", + " \"llm_error_msg\": \"\"\n", + "}\u001b[0m\n", + "\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "20.0" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cpal_chain.run(question)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "00375615-6b6d-4357-bdb8-f64f682f7605", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "cindy\n", + "\n", + "cindy\n", + "\n", + "\n", + "\n", + "marcia\n", + "\n", + "marcia\n", + "\n", + "\n", + "\n", + "cindy->marcia\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "jan\n", + "\n", + "jan\n", + "\n", + "\n", + "\n", + "cindy->jan\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "marcia->jan\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# wait 20 secs to see display\n", + "cpal_chain.draw(path='web.svg')\n", + "SVG('web.svg')" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "255683de-0c1c-4131-b277-99d09f5ac1fc", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/langchain/experimental/cpal/README.md b/langchain/experimental/cpal/README.md new file mode 100644 index 0000000000..cf84b800fc --- /dev/null +++ b/langchain/experimental/cpal/README.md @@ -0,0 +1,4 @@ +# Causal program-aided language (CPAL) chain + + +see https://github.com/hwchase17/langchain/pull/6255 diff --git a/langchain/experimental/cpal/__init__.py b/langchain/experimental/cpal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/langchain/experimental/cpal/base.py b/langchain/experimental/cpal/base.py new file mode 100644 index 0000000000..02dcdcba97 --- /dev/null +++ b/langchain/experimental/cpal/base.py @@ -0,0 +1,271 @@ +""" +CPAL Chain and its subchains +""" +from __future__ import annotations + +import json +from typing import Any, ClassVar, Dict, List, Optional, Type + +import pydantic + +from langchain.base_language import BaseLanguageModel +from langchain.callbacks.manager import CallbackManagerForChainRun +from langchain.chains.base import Chain +from langchain.chains.llm import LLMChain +from langchain.experimental.cpal.constants import Constant +from langchain.experimental.cpal.models import ( + CausalModel, + InterventionModel, + NarrativeModel, + QueryModel, + StoryModel, +) +from langchain.experimental.cpal.templates.univariate.causal import ( + template as causal_template, +) +from langchain.experimental.cpal.templates.univariate.intervention import ( + template as intervention_template, +) +from langchain.experimental.cpal.templates.univariate.narrative import ( + template as narrative_template, +) +from langchain.experimental.cpal.templates.univariate.query import ( + template as query_template, +) +from langchain.output_parsers import PydanticOutputParser +from langchain.prompts.prompt import PromptTemplate + + +class _BaseStoryElementChain(Chain): + chain: LLMChain + input_key: str = Constant.narrative_input.value #: :meta private: + output_key: str = Constant.chain_answer.value #: :meta private: + pydantic_model: ClassVar[ + Optional[Type[pydantic.BaseModel]] + ] = None #: :meta private: + template: ClassVar[Optional[str]] = None #: :meta private: + + @classmethod + def parser(cls) -> PydanticOutputParser: + """Parse LLM output into a pydantic object.""" + if cls.pydantic_model is None: + raise NotImplementedError( + f"pydantic_model not implemented for {cls.__name__}" + ) + return PydanticOutputParser(pydantic_object=cls.pydantic_model) + + @property + def input_keys(self) -> List[str]: + """Return the input keys. + + :meta private: + """ + return [self.input_key] + + @property + def output_keys(self) -> List[str]: + """Return the output keys. + + :meta private: + """ + _output_keys = [self.output_key] + return _output_keys + + @classmethod + def from_univariate_prompt( + cls, + llm: BaseLanguageModel, + **kwargs: Any, + ) -> Any: + return cls( + chain=LLMChain( + llm=llm, + prompt=PromptTemplate( + input_variables=[Constant.narrative_input.value], + template=kwargs.get("template", cls.template), + partial_variables={ + "format_instructions": cls.parser().get_format_instructions() + }, + ), + ), + **kwargs, + ) + + def _call( + self, + inputs: Dict[str, Any], + run_manager: Optional[CallbackManagerForChainRun] = None, + ) -> Dict[str, Any]: + completion = self.chain.run(inputs[self.input_key]) + pydantic_data = self.__class__.parser().parse(completion) + return { + Constant.chain_data.value: pydantic_data, + Constant.chain_answer.value: None, + } + + +class NarrativeChain(_BaseStoryElementChain): + """Decompose the narrative into its story elements + + - causal model + - query + - intervention + """ + + pydantic_model: ClassVar[Type[pydantic.BaseModel]] = NarrativeModel + template: ClassVar[str] = narrative_template + + +class CausalChain(_BaseStoryElementChain): + """Translate the causal narrative into a stack of operations.""" + + pydantic_model: ClassVar[Type[pydantic.BaseModel]] = CausalModel + template: ClassVar[str] = causal_template + + +class InterventionChain(_BaseStoryElementChain): + """Set the hypothetical conditions for the causal model.""" + + pydantic_model: ClassVar[Type[pydantic.BaseModel]] = InterventionModel + template: ClassVar[str] = intervention_template + + +class QueryChain(_BaseStoryElementChain): + """Query the outcome table using SQL.""" + + pydantic_model: ClassVar[Type[pydantic.BaseModel]] = QueryModel + template: ClassVar[str] = query_template # TODO: incl. table schema + + +class CPALChain(_BaseStoryElementChain): + llm: BaseLanguageModel + narrative_chain: Optional[NarrativeChain] = None + causal_chain: Optional[CausalChain] = None + intervention_chain: Optional[InterventionChain] = None + query_chain: Optional[QueryChain] = None + _story: StoryModel = pydantic.PrivateAttr(default=None) # TODO: change name ? + + @classmethod + def from_univariate_prompt( + cls, + llm: BaseLanguageModel, + **kwargs: Any, + ) -> CPALChain: + """instantiation depends on component chains""" + return cls( + llm=llm, + chain=LLMChain( + llm=llm, + prompt=PromptTemplate( + input_variables=["question", "query_result"], + template=( + "Summarize this answer '{query_result}' to this " + "question '{question}'? " + ), + ), + ), + narrative_chain=NarrativeChain.from_univariate_prompt(llm=llm), + causal_chain=CausalChain.from_univariate_prompt(llm=llm), + intervention_chain=InterventionChain.from_univariate_prompt(llm=llm), + query_chain=QueryChain.from_univariate_prompt(llm=llm), + **kwargs, + ) + + def _call( + self, + inputs: Dict[str, Any], + run_manager: Optional[CallbackManagerForChainRun] = None, + **kwargs: Any, + ) -> Dict[str, Any]: + # instantiate component chains + if self.narrative_chain is None: + self.narrative_chain = NarrativeChain.from_univariate_prompt(llm=self.llm) + if self.causal_chain is None: + self.causal_chain = CausalChain.from_univariate_prompt(llm=self.llm) + if self.intervention_chain is None: + self.intervention_chain = InterventionChain.from_univariate_prompt( + llm=self.llm + ) + if self.query_chain is None: + self.query_chain = QueryChain.from_univariate_prompt(llm=self.llm) + + # decompose narrative into three causal story elements + narrative = self.narrative_chain(inputs[Constant.narrative_input.value])[ + Constant.chain_data.value + ] + + story = StoryModel( + causal_operations=self.causal_chain(narrative.story_plot)[ + Constant.chain_data.value + ], + intervention=self.intervention_chain(narrative.story_hypothetical)[ + Constant.chain_data.value + ], + query=self.query_chain(narrative.story_outcome_question)[ + Constant.chain_data.value + ], + ) + self._story = story + + def pretty_print_str(title: str, d: str) -> str: + return title + "\n" + d + + _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager() + _run_manager.on_text( + pretty_print_str("story outcome data", story._outcome_table.to_string()), + color="green", + end="\n\n", + verbose=self.verbose, + ) + + def pretty_print_dict(title: str, d: dict) -> str: + return title + "\n" + json.dumps(d, indent=4) + + _run_manager.on_text( + pretty_print_dict("query data", story.query.dict()), + color="blue", + end="\n\n", + verbose=self.verbose, + ) + if story.query._result_table.empty: + # prevent piping bad data into subsequent chains + raise ValueError( + ( + "unanswerable, query and outcome are incoherent\n" + "\n" + "outcome:\n" + f"{story._outcome_table}\n" + "query:\n" + f"{story.query.dict()}" + ) + ) + else: + query_result = float(story.query._result_table.values[0][-1]) + if False: + """TODO: add this back in when demanded by composable chains""" + reporting_chain = self.chain + human_report = reporting_chain.run( + question=story.query.question, query_result=query_result + ) + query_result = { + "query_result": query_result, + "human_report": human_report, + } + output = { + Constant.chain_data.value: story, + self.output_key: query_result, + **kwargs, + } + return output + + def draw(self, **kwargs: Any) -> None: + """ + CPAL chain can draw its resulting DAG. + + Usage in a jupyter notebook: + + >>> from IPython.display import SVG + >>> cpal_chain.draw(path="graph.svg") + >>> SVG('graph.svg') + """ + self._story._networkx_wrapper.draw_graphviz(**kwargs) diff --git a/langchain/experimental/cpal/constants.py b/langchain/experimental/cpal/constants.py new file mode 100644 index 0000000000..1ab620130d --- /dev/null +++ b/langchain/experimental/cpal/constants.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class Constant(Enum): + narrative_input = "narrative_input" + chain_answer = "chain_answer" # natural language answer + chain_data = "chain_data" # pydantic instance diff --git a/langchain/experimental/cpal/models.py b/langchain/experimental/cpal/models.py new file mode 100644 index 0000000000..1acd873e02 --- /dev/null +++ b/langchain/experimental/cpal/models.py @@ -0,0 +1,245 @@ +from __future__ import annotations # allows pydantic model to reference itself + +import re +from typing import Any, Optional, Union + +import duckdb +import pandas as pd +from pydantic import BaseModel, Field, PrivateAttr, root_validator, validator + +from langchain.experimental.cpal.constants import Constant +from langchain.graphs.networkx_graph import NetworkxEntityGraph + + +class NarrativeModel(BaseModel): + """ + Represent the narrative input as three story elements. + """ + + story_outcome_question: str + story_hypothetical: str + story_plot: str # causal stack of operations + + @validator("*", pre=True) + def empty_str_to_none(cls, v: str) -> Union[str, None]: + """Empty strings are not allowed""" + if v == "": + return None + return v + + +class EntityModel(BaseModel): + name: str = Field(description="entity name") + code: str = Field(description="entity actions") + value: float = Field(description="entity initial value") + depends_on: list[str] = Field(default=[], description="ancestor entities") + + # TODO: generalize to multivariate math + # TODO: acyclic graph + + class Config: + validate_assignment = True + + @validator("name") + def lower_case_name(cls, v: str) -> str: + v = v.lower() + return v + + +class CausalModel(BaseModel): + attribute: str = Field(description="name of the attribute to be calculated") + entities: list[EntityModel] = Field(description="entities in the story") + + # TODO: root validate each `entity.depends_on` using system's entity names + + +class EntitySettingModel(BaseModel): + """ + Initial conditions for an entity + + {"name": "bud", "attribute": "pet_count", "value": 12} + """ + + name: str = Field(description="name of the entity") + attribute: str = Field(description="name of the attribute to be calculated") + value: float = Field(description="entity's attribute value (calculated)") + + @validator("name") + def lower_case_transform(cls, v: str) -> str: + v = v.lower() + return v + + +class SystemSettingModel(BaseModel): + """ + Initial global conditions for the system. + + {"parameter": "interest_rate", "value": .05} + """ + + parameter: str + value: float + + +class InterventionModel(BaseModel): + """ + aka initial conditions + + >>> intervention.dict() + { + entity_settings: [ + {"name": "bud", "attribute": "pet_count", "value": 12}, + {"name": "pat", "attribute": "pet_count", "value": 0}, + ], + system_settings: None, + } + """ + + entity_settings: list[EntitySettingModel] + system_settings: Optional[list[SystemSettingModel]] = None + + @validator("system_settings") + def lower_case_name(cls, v: str) -> Union[str, None]: + if v is not None: + raise NotImplementedError("system_setting is not implemented yet") + return v + + +class QueryModel(BaseModel): + """translate a question about the story outcome into a programatic expression""" + + question: str = Field(alias=Constant.narrative_input.value) # input + expression: str # output, part of llm completion + llm_error_msg: str # output, part of llm completion + _result_table: str = PrivateAttr() # result of the executed query + + +class ResultModel(BaseModel): + question: str = Field(alias=Constant.narrative_input.value) # input + _result_table: str = PrivateAttr() # result of the executed query + + +class StoryModel(BaseModel): + causal_operations: Any = Field(required=True) + intervention: Any = Field(required=True) + query: Any = Field(required=True) + _outcome_table: pd.DataFrame = PrivateAttr(default=None) + _networkx_wrapper: Any = PrivateAttr(default=None) + + def __init__(self, **kwargs: Any): + super().__init__(**kwargs) + self._compute() + + # TODO: when langchain adopts pydantic.v2 replace w/ `__post_init__` + # misses hints github.com/pydantic/pydantic/issues/1729#issuecomment-1300576214 + + @root_validator + def check_intervention_is_valid(cls, values: dict) -> dict: + valid_names = [e.name for e in values["causal_operations"].entities] + for setting in values["intervention"].entity_settings: + if setting.name not in valid_names: + error_msg = f""" + Hypothetical question has an invalid entity name. + `{setting.name}` not in `{valid_names}` + """ + raise ValueError(error_msg) + return values + + def _block_back_door_paths(self) -> None: + # stop intervention entities from depending on others + intervention_entities = [ + entity_setting.name for entity_setting in self.intervention.entity_settings + ] + for entity in self.causal_operations.entities: + if entity.name in intervention_entities: + entity.depends_on = [] + entity.code = "pass" + + def _set_initial_conditions(self) -> None: + for entity_setting in self.intervention.entity_settings: + for entity in self.causal_operations.entities: + if entity.name == entity_setting.name: + entity.value = entity_setting.value + + def _make_graph(self) -> None: + self._networkx_wrapper = NetworkxEntityGraph() + for entity in self.causal_operations.entities: + for parent_name in entity.depends_on: + self._networkx_wrapper._graph.add_edge( + parent_name, entity.name, relation=entity.code + ) + + # TODO: is it correct to drop entities with no impact on the outcome (?) + self.causal_operations.entities = [ + entity + for entity in self.causal_operations.entities + if entity.name in self._networkx_wrapper.get_topological_sort() + ] + + def _sort_entities(self) -> None: + # order the sequence of causal actions + sorted_nodes = self._networkx_wrapper.get_topological_sort() + self.causal_operations.entities.sort(key=lambda x: sorted_nodes.index(x.name)) + + def _forward_propagate(self) -> None: + entity_scope = { + entity.name: entity for entity in self.causal_operations.entities + } + for entity in self.causal_operations.entities: + if entity.code == "pass": + continue + else: + # gist.github.com/dean0x7d/df5ce97e4a1a05be4d56d1378726ff92 + exec(entity.code, globals(), entity_scope) + row_values = [entity.dict() for entity in entity_scope.values()] + self._outcome_table = pd.DataFrame(row_values) + + def _run_query(self) -> None: + def humanize_sql_error_msg(error: str) -> str: + pattern = r"column\s+(.*?)\s+not found" + col_match = re.search(pattern, error) + if col_match: + return ( + "SQL error: " + + col_match.group(1) + + " is not an attribute in your story!" + ) + else: + return str(error) + + if self.query.llm_error_msg == "": + try: + df = self._outcome_table # noqa + query_result = duckdb.sql(self.query.expression).df() + self.query._result_table = query_result + except duckdb.BinderException as e: + self.query._result_table = humanize_sql_error_msg(str(e)) + except Exception as e: + self.query._result_table = str(e) + else: + msg = "LLM maybe failed to translate question to SQL query." + raise ValueError( + { + "question": self.query.question, + "llm_error_msg": self.query.llm_error_msg, + "msg": msg, + } + ) + + def _compute(self) -> Any: + self._block_back_door_paths() + self._set_initial_conditions() + self._make_graph() + self._sort_entities() + self._forward_propagate() + self._run_query() + + def print_debug_report(self) -> None: + report = { + "outcome": self._outcome_table, + "query": self.query.dict(), + "result": self.query._result_table, + } + from pprint import pprint + + pprint(report) diff --git a/langchain/experimental/cpal/templates/__init__.py b/langchain/experimental/cpal/templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/langchain/experimental/cpal/templates/univariate/__init__.py b/langchain/experimental/cpal/templates/univariate/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/langchain/experimental/cpal/templates/univariate/causal.py b/langchain/experimental/cpal/templates/univariate/causal.py new file mode 100644 index 0000000000..bb0e772606 --- /dev/null +++ b/langchain/experimental/cpal/templates/univariate/causal.py @@ -0,0 +1,113 @@ +# flake8: noqa E501 + +# fmt: off +template = ( + """ +Transform the math story plot into a JSON object. Don't guess at any of the parts. + +{format_instructions} + + + +Story: Boris has seven times the number of pets as Marcia. Jan has three times the number of pets as Marcia. Marcia has two more pets than Cindy. + + + +# JSON: + + + +{{ + "attribute": "pet_count", + "entities": [ + {{ + "name": "cindy", + "value": 0, + "depends_on": [], + "code": "pass" + }}, + {{ + "name": "marcia", + "value": 0, + "depends_on": ["cindy"], + "code": "marcia.value = cindy.value + 2" + }}, + {{ + "name": "boris", + "value": 0, + "depends_on": ["marcia"], + "code": "boris.value = marcia.value * 7" + }}, + {{ + "name": "jan", + "value": 0, + "depends_on": ["marcia"], + "code": "jan.value = marcia.value * 3" + }} + ] +}} + + + + +Story: Boris gives 20 percent of his money to Marcia. Marcia gives 10 +percent of her money to Cindy. Cindy gives 5 percent of her money to Jan. + + + + +# JSON: + + + +{{ + "attribute": "money", + "entities": [ + {{ + "name": "boris", + "value": 0, + "depends_on": [], + "code": "pass" + }}, + {{ + "name": "marcia", + "value": 0, + "depends_on": ["boris"], + "code": " + marcia.value = boris.value * 0.2 + boris.value = boris.value * 0.8 + " + }}, + {{ + "name": "cindy", + "value": 0, + "depends_on": ["marcia"], + "code": " + cindy.value = marcia.value * 0.1 + marcia.value = marcia.value * 0.9 + " + }}, + {{ + "name": "jan", + "value": 0, + "depends_on": ["cindy"], + "code": " + jan.value = cindy.value * 0.05 + cindy.value = cindy.value * 0.9 + " + }} + ] +}} + + + + +Story: {narrative_input} + + + +# JSON: +""".strip() + + "\n" +) +# fmt: on diff --git a/langchain/experimental/cpal/templates/univariate/intervention.py b/langchain/experimental/cpal/templates/univariate/intervention.py new file mode 100644 index 0000000000..fea4f33552 --- /dev/null +++ b/langchain/experimental/cpal/templates/univariate/intervention.py @@ -0,0 +1,59 @@ +# flake8: noqa E501 + +# fmt: off +template = ( + """ +Transform the hypothetical whatif statement into JSON. Don't guess at any of the parts. Write NONE if you are unsure. + +{format_instructions} + + + +statement: if cindy's pet count was 4 + + + + +# JSON: + + + +{{ + "entity_settings" : [ + {{ "name": "cindy", "attribute": "pet_count", "value": "4" }} + ] +}} + + + + + +statement: Let's say boris has ten dollars and Bill has 20 dollars. + + + + +# JSON: + + +{{ + "entity_settings" : [ + {{ "name": "boris", "attribute": "dollars", "value": "10" }}, + {{ "name": "bill", "attribute": "dollars", "value": "20" }} + ] +}} + + + + + +Statement: {narrative_input} + + + + +# JSON: +""".strip() + + "\n\n\n" +) +# fmt: on diff --git a/langchain/experimental/cpal/templates/univariate/narrative.py b/langchain/experimental/cpal/templates/univariate/narrative.py new file mode 100644 index 0000000000..effd4a124e --- /dev/null +++ b/langchain/experimental/cpal/templates/univariate/narrative.py @@ -0,0 +1,79 @@ +# flake8: noqa E501 + + +# fmt: off +template = ( + """ +Split the given text into three parts: the question, the story_hypothetical, and the logic. Don't guess at any of the parts. Write NONE if you are unsure. + +{format_instructions} + + + +Q: Boris has seven times the number of pets as Marcia. Jan has three times the number of pets as Marcia. Marcia has two more pets than Cindy. If Cindy has four pets, how many total pets do the three have? + + + +# JSON + + + +{{ + "story_outcome_question": "how many total pets do the three have?", + "story_hypothetical": "If Cindy has four pets", + "story_plot": "Boris has seven times the number of pets as Marcia. Jan has three times the number of pets as Marcia. Marcia has two more pets than Cindy." +}} + + + +Q: boris gives ten percent of his money to marcia. marcia gives ten +percent of her money to andy. If boris has 100 dollars, how much money +will andy have? + + + +# JSON + + + +{{ + "story_outcome_question": "how much money will andy have?", + "story_hypothetical": "If boris has 100 dollars" + "story_plot": "boris gives ten percent of his money to marcia. marcia gives ten percent of her money to andy." +}} + + + + +Q: boris gives ten percent of his candy to marcia. marcia gives ten +percent of her candy to andy. If boris has 100 pounds of candy and marcia has +200 pounds of candy, then how many pounds of candy will andy have? + + + + + +# JSON + + + + +{{ + "story_outcome_question": "how many pounds of candy will andy have?", + "story_hypothetical": "If boris has 100 pounds of candy and marcia has 200 pounds of candy" + "story_plot": "boris gives ten percent of his candy to marcia. marcia gives ten percent of her candy to andy." +}} + + + + + +Q: {narrative_input} + + + +# JSON +""".strip() + + "\n\n\n" +) +# fmt: on diff --git a/langchain/experimental/cpal/templates/univariate/query.py b/langchain/experimental/cpal/templates/univariate/query.py new file mode 100644 index 0000000000..dd22e032af --- /dev/null +++ b/langchain/experimental/cpal/templates/univariate/query.py @@ -0,0 +1,270 @@ +# flake8: noqa E501 + + +# fmt: off +template = ( + """ +Transform the narrative_input into an SQL expression. If you are +unsure, then do not guess, instead add a llm_error_msg that explains why you are unsure. + + +{format_instructions} + + +narrative_input: how much money will boris have? + + +# JSON: + + {{ + "narrative_input": "how much money will boris have?", + "llm_error_msg": "", + "expression": "SELECT name, value FROM df WHERE name = 'boris'" + }} + + + +narrative_input: How much money does ted have? + + + +# JSON: + + {{ + "narrative_input": "How much money does ted have?", + "llm_error_msg": "", + "expression": "SELECT name, value FROM df WHERE name = 'ted'" + }} + + + +narrative_input: what is the sum of pet count for all the people? + + + +# JSON: + + {{ + "narrative_input": "what is the sum of pet count for all the people?", + "llm_error_msg": "", + "expression": "SELECT SUM(value) FROM df" + }} + + + + +narrative_input: what's the average of the pet counts for all the people? + + + +# JSON: + + {{ + "narrative_input": "what's the average of the pet counts for all the people?", + "llm_error_msg": "", + "expression": "SELECT AVG(value) FROM df" + }} + + + + +narrative_input: what's the maximum of the pet counts for all the people? + + + +# JSON: + + {{ + "narrative_input": "what's the maximum of the pet counts for all the people?", + "llm_error_msg": "", + "expression": "SELECT MAX(value) FROM df" + }} + + + + +narrative_input: what's the minimum of the pet counts for all the people? + + + +# JSON: + + {{ + "narrative_input": "what's the minimum of the pet counts for all the people?", + "llm_error_msg": "", + "expression": "SELECT MIN(value) FROM df" + }} + + + + +narrative_input: what's the number of people with pet counts greater than 10? + + + +# JSON: + + {{ + "narrative_input": "what's the number of people with pet counts greater than 10?", + "llm_error_msg": "", + "expression": "SELECT COUNT(*) FROM df WHERE value > 10" + }} + + + + +narrative_input: what's the pet count for boris? + + + +# JSON: + + {{ + "narrative_input": "what's the pet count for boris?", + "llm_error_msg": "", + "expression": "SELECT name, value FROM df WHERE name = 'boris'" + }} + + + + +narrative_input: what's the pet count for cindy and marcia? + + + +# JSON: + + {{ + "narrative_input": "what's the pet count for cindy and marcia?", + "llm_error_msg": "", + "expression": "SELECT name, value FROM df WHERE name IN ('cindy', 'marcia')" + }} + + + + +narrative_input: what's the total pet count for cindy and marcia? + + + +# JSON: + + {{ + "narrative_input": "what's the total pet count for cindy and marcia?", + "llm_error_msg": "", + "expression": "SELECT SUM(value) FROM df WHERE name IN ('cindy', 'marcia')" + }} + + + + +narrative_input: what's the total pet count for TED? + + + +# JSON: + + {{ + "narrative_input": "what's the total pet count for TED?", + "llm_error_msg": "", + "expression": "SELECT SUM(value) FROM df WHERE name = 'TED'" + }} + + + + + +narrative_input: what's the total dollar count for TED and cindy? + + + +# JSON: + + {{ + "narrative_input": "what's the total dollar count for TED and cindy?", + "llm_error_msg": "", + "expression": "SELECT SUM(value) FROM df WHERE name IN ('TED', 'cindy')" + }} + + + + +narrative_input: what's the total pet count for TED and cindy? + + + + +# JSON: + + {{ + "narrative_input": "what's the total pet count for TED and cindy?", + "llm_error_msg": "", + "expression": "SELECT SUM(value) FROM df WHERE name IN ('TED', 'cindy')" + }} + + + + +narrative_input: what's the best for TED and cindy? + + + + +# JSON: + + {{ + "narrative_input": "what's the best for TED and cindy?", + "llm_error_msg": "ambiguous narrative_input, not sure what 'best' means", + "expression": "" + }} + + + + +narrative_input: what's the value? + + + + +# JSON: + + {{ + "narrative_input": "what's the value?", + "llm_error_msg": "ambiguous narrative_input, not sure what entity is being asked about", + "expression": "" + }} + + + + + + +narrative_input: how many total pets do the three have? + + + + + +# JSON: + + {{ + "narrative_input": "how many total pets do the three have?", + "llm_error_msg": "", + "expression": "SELECT SUM(value) FROM df" + }} + + + + + + +narrative_input: {narrative_input} + + + + +# JSON: +""".strip() + + "\n\n\n" +) +# fmt: on diff --git a/langchain/graphs/networkx_graph.py b/langchain/graphs/networkx_graph.py index 753db5fc5d..0d178a6a4c 100644 --- a/langchain/graphs/networkx_graph.py +++ b/langchain/graphs/networkx_graph.py @@ -122,3 +122,48 @@ class NetworkxEntityGraph: def clear(self) -> None: """Clear the graph.""" self._graph.clear() + + def get_topological_sort(self) -> List[str]: + """Get a list of entity names in the graph sorted by causal dependence.""" + import networkx as nx + + return list(nx.topological_sort(self._graph)) + + def draw_graphviz(self, **kwargs: Any) -> None: + """ + Provides better drawing + + Usage in a jupyter notebook: + + >>> from IPython.display import SVG + >>> self.draw_graphviz_svg(layout="dot", filename="web.svg") + >>> SVG('web.svg') + """ + from networkx.drawing.nx_agraph import to_agraph + + try: + import pygraphviz # noqa: F401 + + except ImportError as e: + if e.name == "_graphviz": + """ + >>> e.msg # pygraphviz throws this error + ImportError: libcgraph.so.6: cannot open shared object file + """ + raise ImportError( + "Could not import graphviz debian package. " + "Please install it with:" + "`sudo apt-get update`" + "`sudo apt-get install graphviz graphviz-dev`" + ) + else: + raise ImportError( + "Could not import pygraphviz python package. " + "Please install it with:" + "`pip install pygraphviz`." + ) + + graph = to_agraph(self._graph) # --> pygraphviz.agraph.AGraph + # pygraphviz.github.io/documentation/stable/tutorial.html#layout-and-drawing + graph.layout(prog=kwargs.get("prog", "dot")) + graph.draw(kwargs.get("path", "graph.svg")) diff --git a/tests/integration_tests/chains/test_cpal.py b/tests/integration_tests/chains/test_cpal.py new file mode 100644 index 0000000000..adb3a6ae5f --- /dev/null +++ b/tests/integration_tests/chains/test_cpal.py @@ -0,0 +1,554 @@ +"""Test CPAL chain.""" + +import json +import unittest +from typing import Type +from unittest import mock + +import pydantic +import pytest + +from langchain import OpenAI +from langchain.experimental.cpal.base import ( + CausalChain, + CPALChain, + InterventionChain, + NarrativeChain, + QueryChain, +) +from langchain.experimental.cpal.constants import Constant +from langchain.experimental.cpal.models import ( + CausalModel, + EntityModel, + EntitySettingModel, + InterventionModel, + NarrativeModel, + QueryModel, +) +from langchain.experimental.cpal.templates.univariate.causal import ( + template as causal_template, +) +from langchain.experimental.cpal.templates.univariate.intervention import ( + template as intervention_template, +) +from langchain.experimental.cpal.templates.univariate.narrative import ( + template as narrative_template, +) +from langchain.experimental.cpal.templates.univariate.query import ( + template as query_template, +) +from langchain.output_parsers import PydanticOutputParser +from langchain.prompts.prompt import PromptTemplate +from tests.unit_tests.llms.fake_llm import FakeLLM + + +class TestUnitCPALChain_MathWordProblems(unittest.TestCase): + """Unit Test the CPAL chain and its component chains on math word problems. + + These tests can't run in the standard unit test directory because of + this issue, https://github.com/hwchase17/langchain/issues/7451 + + """ + + def setUp(self) -> None: + self.fake_llm = self.make_fake_llm() + + def make_fake_llm(self) -> FakeLLM: + """ + Fake LLM service for testing CPAL chain and its components chains + on univariate math examples. + """ + + class LLMMockData(pydantic.BaseModel): + question: str + completion: str + template: str + data_model: Type[pydantic.BaseModel] + + @property + def prompt(self) -> str: + """Create LLM prompt with the question.""" + prompt_template = PromptTemplate( + input_variables=[Constant.narrative_input.value], + template=self.template, + partial_variables={ + "format_instructions": PydanticOutputParser( + pydantic_object=self.data_model + ).get_format_instructions() + }, + ) + prompt = prompt_template.format(narrative_input=self.question) + return prompt + + narrative = LLMMockData( + **{ + "question": ( + "jan has three times the number of pets as marcia. " + "marcia has two more pets than cindy." + "if cindy has ten pets, how many pets does jan have? " + ), + "completion": json.dumps( + { + "story_outcome_question": "how many pets does jan have? ", + "story_hypothetical": "if cindy has ten pets", + "story_plot": "jan has three times the number of pets as marcia. marcia has two more pets than cindy.", # noqa: E501 + } + ), + "template": narrative_template, + "data_model": NarrativeModel, + } + ) + + causal_model = LLMMockData( + **{ + "question": ( + "jan has three times the number of pets as marcia. " + "marcia has two more pets than cindy." + ), + "completion": ( + "\n" + "{\n" + ' "attribute": "pet_count",\n' + ' "entities": [\n' + " {\n" + ' "name": "cindy",\n' + ' "value": 0,\n' + ' "depends_on": [],\n' + ' "code": "pass"\n' + " },\n" + " {\n" + ' "name": "marcia",\n' + ' "value": 0,\n' + ' "depends_on": ["cindy"],\n' + ' "code": "marcia.value = cindy.value + 2"\n' + " },\n" + " {\n" + ' "name": "jan",\n' + ' "value": 0,\n' + ' "depends_on": ["marcia"],\n' + ' "code": "jan.value = marcia.value * 3"\n' + " }\n" + " ]\n" + "}" + ), + "template": causal_template, + "data_model": CausalModel, + } + ) + + intervention = LLMMockData( + **{ + "question": ("if cindy has ten pets"), + "completion": ( + "{\n" + ' "entity_settings" : [\n' + ' { "name": "cindy", "attribute": "pet_count", "value": "10" }\n' # noqa: E501 + " ]\n" + "}" + ), + "template": intervention_template, + "data_model": InterventionModel, + } + ) + + query = LLMMockData( + **{ + "question": ("how many pets does jan have? "), + "completion": ( + "{\n" + ' "narrative_input": "how many pets does jan have? ",\n' + ' "llm_error_msg": "",\n' + ' "expression": "SELECT name, value FROM df WHERE name = \'jan\'"\n' # noqa: E501 + "}" + ), + "template": query_template, + "data_model": QueryModel, + } + ) + + fake_llm = FakeLLM() + fake_llm.queries = {} + for mock_data in [narrative, causal_model, intervention, query]: + fake_llm.queries.update({mock_data.prompt: mock_data.completion}) + return fake_llm + + def test_narrative_chain(self) -> None: + """Test narrative chain returns the three main elements of the causal + narrative as a pydantic object. + """ + narrative_chain = NarrativeChain.from_univariate_prompt(llm=self.fake_llm) + output = narrative_chain( + ( + "jan has three times the number of pets as marcia. " + "marcia has two more pets than cindy." + "if cindy has ten pets, how many pets does jan have? " + ) + ) + expected_output = { + "chain_answer": None, + "chain_data": NarrativeModel( + story_outcome_question="how many pets does jan have? ", + story_hypothetical="if cindy has ten pets", + story_plot="jan has three times the number of pets as marcia. marcia has two more pets than cindy.", # noqa: E501 + ), + "narrative_input": "jan has three times the number of pets as marcia. marcia " # noqa: E501 + "has two more pets than cindy.if cindy has ten pets, how " + "many pets does jan have? ", + } + assert output == expected_output + + def test_causal_chain(self) -> None: + """ + Test causal chain returns a DAG as a pydantic object. + """ + causal_chain = CausalChain.from_univariate_prompt(llm=self.fake_llm) + output = causal_chain( + ( + "jan has three times the number of pets as " + "marcia. marcia has two more pets than cindy." + ) + ) + expected_output = { + "chain_answer": None, + "chain_data": CausalModel( + attribute="pet_count", + entities=[ + EntityModel(name="cindy", code="pass", value=0.0, depends_on=[]), + EntityModel( + name="marcia", + code="marcia.value = cindy.value + 2", + value=0.0, + depends_on=["cindy"], + ), + EntityModel( + name="jan", + code="jan.value = marcia.value * 3", + value=0.0, + depends_on=["marcia"], + ), + ], + ), + "narrative_input": "jan has three times the number of pets as marcia. marcia " # noqa: E501 + "has two more pets than cindy.", + } + assert output == expected_output + + def test_intervention_chain(self) -> None: + """ + Test intervention chain correctly transforms + the LLM's text completion into a setting-like object. + """ + intervention_chain = InterventionChain.from_univariate_prompt(llm=self.fake_llm) + output = intervention_chain("if cindy has ten pets") + expected_output = { + "chain_answer": None, + "chain_data": InterventionModel( + entity_settings=[ + EntitySettingModel(name="cindy", attribute="pet_count", value=10), + ] + ), + "narrative_input": "if cindy has ten pets", + } + assert output == expected_output + + def test_query_chain(self) -> None: + """ + Test query chain correctly transforms + the LLM's text completion into a query-like object. + """ + query_chain = QueryChain.from_univariate_prompt(llm=self.fake_llm) + output = query_chain("how many pets does jan have? ") + expected_output = { + "chain_answer": None, + "chain_data": QueryModel( + narrative_input="how many pets does jan have? ", + llm_error_msg="", + expression="SELECT name, value FROM df WHERE name = 'jan'", + ), + "narrative_input": "how many pets does jan have? ", + } + assert output == expected_output + + def test_cpal_chain(self) -> None: + """ + patch required since `networkx` package is not part of unit test environment + """ + with mock.patch( + "langchain.experimental.cpal.models.NetworkxEntityGraph" + ) as mock_networkx: + graph_instance = mock_networkx.return_value + graph_instance.get_topological_sort.return_value = [ + "cindy", + "marcia", + "jan", + ] + cpal_chain = CPALChain.from_univariate_prompt( + llm=self.fake_llm, verbose=True + ) + cpal_chain.run( + ( + "jan has three times the number of pets as " + "marcia. marcia has two more pets than cindy." + "if cindy has ten pets, how many pets does jan have? " + ) + ) + + +class TestCPALChain_MathWordProblems(unittest.TestCase): + """Test the CPAL chain and its component chains on math word problems.""" + + def test_causal_chain(self) -> None: + """Test CausalChain can translate a narrative's plot into a causal model + containing operations linked by a DAG.""" + + llm = OpenAI(temperature=0, max_tokens=512) + casual_chain = CausalChain.from_univariate_prompt(llm) + narrative_plot = ( + "Jan has three times the number of pets as Marcia. " + "Marcia has two more pets than Cindy. " + ) + output = casual_chain(narrative_plot) + expected_output = { + "chain_answer": None, + "chain_data": CausalModel( + attribute="pet_count", + entities=[ + EntityModel(name="cindy", code="pass", value=0.0, depends_on=[]), + EntityModel( + name="marcia", + code="marcia.value = cindy.value + 2", + value=0.0, + depends_on=["cindy"], + ), + EntityModel( + name="jan", + code="jan.value = marcia.value * 3", + value=0.0, + depends_on=["marcia"], + ), + ], + ), + "narrative_input": "Jan has three times the number of pets as Marcia. Marcia " # noqa: E501 + "has two more pets than Cindy. ", + } + self.assertDictEqual(output, expected_output) + self.assertEqual( + isinstance(output[Constant.chain_data.value], CausalModel), True + ) + + def test_intervention_chain(self) -> None: + """Test InterventionChain translates a hypothetical into a new value setting.""" + + llm = OpenAI(temperature=0, max_tokens=512) + story_conditions_chain = InterventionChain.from_univariate_prompt(llm) + question = "if cindy has ten pets" + data = story_conditions_chain(question)[Constant.chain_data.value] + self.assertEqual(type(data), InterventionModel) + + def test_intervention_chain_2(self) -> None: + """Test InterventionChain translates a hypothetical into a new value setting.""" + + llm = OpenAI(temperature=0, max_tokens=512) + story_conditions_chain = InterventionChain.from_univariate_prompt(llm) + narrative_condition = "What if Cindy has ten pets and Boris has 5 pets? " + data = story_conditions_chain(narrative_condition)[Constant.chain_data.value] + self.assertEqual(type(data), InterventionModel) + + def test_query_chain(self) -> None: + """Test QueryChain translates a question into a query expression.""" + llm = OpenAI(temperature=0, max_tokens=512) + query_chain = QueryChain.from_univariate_prompt(llm) + narrative_question = "How many pets will Marcia end up with? " + data = query_chain(narrative_question)[Constant.chain_data.value] + self.assertEqual(type(data), QueryModel) + + def test_narrative_chain(self) -> None: + """Test NarrativeChain decomposes a human's narrative into three story elements: + + - causal model + - intervention model + - query model + """ + + narrative = ( + "Jan has three times the number of pets as Marcia. " + "Marcia has two more pets than Cindy. " + "If Cindy has ten pets, how many pets does Jan have? " + ) + llm = OpenAI(temperature=0, max_tokens=512) + narrative_chain = NarrativeChain.from_univariate_prompt(llm) + data = narrative_chain(narrative)[Constant.chain_data.value] + self.assertEqual(type(data), NarrativeModel) + + out = narrative_chain(narrative) + expected_narrative_out = { + "chain_answer": None, + "chain_data": NarrativeModel( + story_outcome_question="how many pets does Jan have?", + story_hypothetical="If Cindy has ten pets", + story_plot="Jan has three times the number of pets as Marcia. Marcia has two more pets than Cindy.", # noqa: E501 + ), + "narrative_input": "Jan has three times the number of pets as Marcia. Marcia " # noqa: E501 + "has two more pets than Cindy. If Cindy has ten pets, how " + "many pets does Jan have? ", + } + self.assertDictEqual(out, expected_narrative_out) + + def test_against_pal_chain_doc(self) -> None: + """ + Test CPAL chain against the first example in the PAL chain notebook doc: + + https://github.com/hwchase17/langchain/blob/master/docs/extras/modules/chains/additional/pal.ipynb + """ + + narrative_input = ( + "Jan has three times the number of pets as Marcia." + " Marcia has two more pets than Cindy." + " If Cindy has four pets, how many total pets do the three have?" + ) + + llm = OpenAI(temperature=0, max_tokens=512) + cpal_chain = CPALChain.from_univariate_prompt(llm=llm, verbose=True) + answer = cpal_chain.run(narrative_input) + + """ + >>> story._outcome_table + name code value depends_on + 0 cindy pass 4.0 [] + 1 marcia marcia.value = cindy.value + 2 6.0 [cindy] + 2 jan jan.value = marcia.value * 3 18.0 [marcia] + + """ + self.assertEqual(answer, 28.0) + + def test_simple(self) -> None: + """ + Given a simple math word problem here we are test and illustrate the + the data structures that are produced by the CPAL chain. + """ + + narrative_input = ( + "jan has three times the number of pets as marcia." + "marcia has two more pets than cindy." + "If cindy has ten pets, how many pets does jan have?" + ) + llm = OpenAI(temperature=0, max_tokens=512) + cpal_chain = CPALChain.from_univariate_prompt(llm=llm, verbose=True) + output = cpal_chain(narrative_input) + data = output[Constant.chain_data.value] + + expected_output = { + "causal_operations": { + "attribute": "pet_count", + "entities": [ + {"code": "pass", "depends_on": [], "name": "cindy", "value": 10.0}, + { + "code": "marcia.value = cindy.value + 2", + "depends_on": ["cindy"], + "name": "marcia", + "value": 12.0, + }, + { + "code": "jan.value = marcia.value * 3", + "depends_on": ["marcia"], + "name": "jan", + "value": 36.0, + }, + ], + }, + "intervention": { + "entity_settings": [ + {"attribute": "pet_count", "name": "cindy", "value": 10.0} + ], + "system_settings": None, + }, + "query": { + "expression": "SELECT name, value FROM df WHERE name = 'jan'", + "llm_error_msg": "", + "question": "how many pets does jan have?", + }, + } + self.assertDictEqual(data.dict(), expected_output) + + """ + Illustrate the query model's result table as a printed pandas dataframe + >>> data._outcome_table + name code value depends_on + 0 cindy pass 10.0 [] + 1 marcia marcia.value = cindy.value + 2 12.0 [cindy] + 2 jan jan.value = marcia.value * 3 36.0 [marcia] + """ + + expected_output = { + "code": { + 0: "pass", + 1: "marcia.value = cindy.value + 2", + 2: "jan.value = marcia.value * 3", + }, + "depends_on": {0: [], 1: ["cindy"], 2: ["marcia"]}, + "name": {0: "cindy", 1: "marcia", 2: "jan"}, + "value": {0: 10.0, 1: 12.0, 2: 36.0}, + } + self.assertDictEqual(data._outcome_table.to_dict(), expected_output) + + expected_output = {"name": {0: "jan"}, "value": {0: 36.0}} + self.assertDictEqual(data.query._result_table.to_dict(), expected_output) + + # TODO: use an LLM chain to translate numbers to words + df = data.query._result_table + expr = "name == 'jan'" + answer = df.query(expr).iloc[0]["value"] + self.assertEqual(float(answer), 36.0) + + def test_hallucinating(self) -> None: + """ + Test CPAL approach does not hallucinate when given + an invalid entity in the question. + + The PAL chain would hallucinates here! + """ + + narrative_input = ( + "Jan has three times the number of pets as Marcia." + "Marcia has two more pets than Cindy." + "If Cindy has ten pets, how many pets does Barak have?" + ) + llm = OpenAI(temperature=0, max_tokens=512) + cpal_chain = CPALChain.from_univariate_prompt(llm=llm, verbose=True) + with pytest.raises(Exception) as e_info: + print(e_info) + cpal_chain.run(narrative_input) + + def test_causal_mediator(self) -> None: + """ + Test CPAL approach on causal mediator. + """ + + narrative_input = ( + "jan has three times the number of pets as marcia." + "marcia has two more pets than cindy." + "If marcia has ten pets, how many pets does jan have?" + ) + llm = OpenAI(temperature=0, max_tokens=512) + cpal_chain = CPALChain.from_univariate_prompt(llm=llm, verbose=True) + answer = cpal_chain.run(narrative_input) + self.assertEqual(answer, 30.0) + + @pytest.mark.skip(reason="requires manual install of debian and py packages") + def test_draw(self) -> None: + """ + Test CPAL chain can draw its resulting DAG. + """ + import os + + narrative_input = ( + "Jan has three times the number of pets as Marcia." + "Marcia has two more pets than Cindy." + "If Marcia has ten pets, how many pets does Jan have?" + ) + llm = OpenAI(temperature=0, max_tokens=512) + cpal_chain = CPALChain.from_univariate_prompt(llm=llm, verbose=True) + cpal_chain.run(narrative_input) + path = "graph.svg" + cpal_chain.draw(path=path) + self.assertTrue(os.path.exists(path))