From 17cd65953335289b264cbf58009acb1269d323a6 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 5 Jan 2024 18:22:46 -0800 Subject: [PATCH] ... --- examples/.langchain.db | Bin 0 -> 53248 bytes examples/langgraph.ipynb | 94 ++++++------ permchain/__init__.py | 2 + permchain/langgraph/__init__.py | 258 ++++++++++---------------------- permchain/pregel/__init__.py | 2 +- poetry.lock | 38 ++--- pyproject.toml | 2 +- tests/test_graph.py | 0 tests/test_pregel.py | 66 +++++++- tests/test_pregel_async.py | 4 - 10 files changed, 208 insertions(+), 258 deletions(-) create mode 100644 examples/.langchain.db create mode 100644 tests/test_graph.py diff --git a/examples/.langchain.db b/examples/.langchain.db new file mode 100644 index 0000000000000000000000000000000000000000..791e9b4ba8f8727604c9f43d1454fe65ade828f6 GIT binary patch literal 53248 zcmeHQ&2t+^b|<}-DJzz}dr0Zpg$uta?qMsJTyoDL=N$H$WB!ZWQk6@7ucrqXd`O~YRcyEzgk%KI{AU$u^Q?zWED z;r$bK`0&dw>+H(=Q^3p1be2f;chgrwYya?M_s;Io^qJ}nnJ`1GjHsS_aI}BWI(p2$ z*nP~d%Xs6;#?HI%U3;{(y5bmWzD^Lhc6e^J`@T%xwicIq>DxQ2Yu7e5R)4=gg$Lvo z66)@se0ygRmdQ1CykI?e8Hw1Hjo-ZU-nIK1FNI@5 zZl{1^^8FW3GnYx#P*A`6(f`un)#3*ohR;t5fj|9v_3|G-`@Zn3R8 ztJ(2T;II~9YuPN1tj=mUEn}Q~QcJk%M*}Vuoik9+DW{`U7{mL*(DB_ob{C;l|HlZqeggO?F3~36L5K%Vh$BFm#35w%Ts8BhgDlWI<@mqD)$e z5oty&(XF(}j)x+WJsI&N8P^>#W}x#V#v}}%20Ix@b;@{G=XSE7R2oCn_FyJwhI`pV zCI{%fB-gniQma(F@UenF1Zh?vRS?H=XkaDIE#W0os2m_MKcIuBa*`$$=RC5>m~FEm z&n#FEC2{iXYpajF9MH)Hv?87gmJiuT+QEyVYOC9~Z<9>z6}i4 za#Dj$*(cd^H%bMu;)$o|X2b(qGPvt z3*jgydub9YXRod$_?nsNXRG2#}?B`UFsuXAmM z0d1hJ^_UFj-TlKmOeB!kVIsH@O;T47GJvKPPdZaY_5Atsr?00Ip&a3>>KN0|=z1s= zmvpp)3-|G@wLh;Etw^yOxy7QO7&n`nxI5+N9ji};BE7l2xmljegv})MEOd2oj%Fft z+8^4+CylMG%}*P{Oq@wEnoi&0L)keMW8Al7QP?*amG1UB{{DmA!`6PM^&m& zU^E(yC)d}F)x-5VI|L&(UK5_F$$$5L`793Hgl@ ziNXnN4(WoCfHIe`ix~_HL^ppnZWvMpFv;xZCT`Hq2&Xb`e2TAoe2@t> z5E6QIkmW|w!;&NN|ll@ z&#<_D`bfhfR4(o08OX4Bpa<)Np$S}~0=vlw713NU`u(~aSVnomGk}EY?&ZLU?!K2N zV{gTDA7(Lk7NeVI%FwVR6Eq+P&6(0#!7mp)_MS$B;|z;1r`Xq#F=+*z+GTu zAq2#RB?Pyhj+9)Uukk3i%Ix@w;5Uc-3QCS=?tC)Kk67hABD43Yubys8iYdTt^Ejth_uK#mw5vSUbISu8X7 z>6obCCp|*bOag$y|E5!(rA|0zJRnF*ZJzI#b_RqtOm4gOG6pVxVv(1$qPd~ToWIDk z!KDHn5~2=Nfp*5V2k+4gI&gHc&B&!7b5G+Wx(~0uOHPN%5#oNZdIkT!b0u5(@K^XJ zd;$Uifq+0jARrJB2nYlO0s?Owfq(hywZBNzDpo_O0%6DZDpedIJBSphOBlOUAr%39 z=$gAy0);k8j1X2J`a+@#VFE&&q6C3Fd6_1s#6R6S8Dx@DM!1dOgQ@{KLl8;XsL4na zi&wJReqb8B5506i9f0X1gsK}A|aM3MN7mko=y@Ilw#-)2*oe0AsTS_?wWRfVQ zg+LPd3gojyT$!Z7TA++0@g%B~kdd#n*$ zQ=o{@8Edl|37XhfmQb8RM2|>)1A)7u5S2#p1sqx=eH!e^)>AgHc4%(hxPdwd!dlTp z8oL0wxXInN{gT@n4z5ZO=7qc{svbxxR*ODUhRj!vAPjMlXFq;;v`cXMz<8R9pBCcU z3VZ#^KmBISl@i|%+w=Qw&mVgw5$t~7z2N`LH?HKvfB}JkKtLcM5D*9m1Ox&C0fB%( zKp-Fx5C{nTh!CLqKYr;yd;$Uifq+0jARrJB2nYlO0s;YnfIvVXAP^9EixCL%|69!5 zu(W_cKp-Fx5C{ka1Ofs9fq+0jARrJB2ncutLi`^@As`SC2nYlO0s;YnfIvVXAP^7; z2m}NI0&hP8A^v~+c^p<45C{ka1Ofs9fq+0jARrJB2nYlO0s;X6LSU{9+3X*etp3@| z$)65g=w7zaBdhYqEJH`J48VnsVy@j>=qUDc=qMKSYCs?$5D*9m1Ox&C0fB%(Kp-IS zV?`izG@og6PCXbyNAo4k-`nZ|b!wp|GsX&ZJYlXQHxAhr`l7UzZ?o(=sFpoa=Nnpv zF3V`O6S^#iF3WFym*q*1Wma|<4lS0c8}zhy)6ccV@=Swe^zZsX`zt#ehK}uMppAC* zLv*Z1KP?dTLz7i=CiTg)^*l?~Z?W~V$=GN#YI@_keUn;^-B4)FHfZpozuJx5@Ed3% zF0l$F`3=KWqnB|NNfYVyI$Mvlq8{lqDhZ6E#=Vwe0y{IlO~((Z_TzoL2Gj~P4_dNOnY&o z9UEO)7;&aQ_T<4#P=`A9Dq^S^9kl@;9yoaXon9=o3)70f& zxe;gQoR?SjsGz>!2fFC#GayOL>l63%OwT;(4v$uJXv>F=cW6>~s`SWL;Hd3e3B>%e z>+GSD7LOdGdmg&j6Hp}`6@Ow8E~uP#zPZ`mWP455XO24Rp+h(t!_fj`VYy5&Zk5P| zzUWJab$$KbIFv))7e(9m7Xh*U{3&hO+qZAi4+tXK8di&xj2rCWB+(<|c2LnI5sd;B zH5V-FPp%o@T;IVirzV7+pqOgsVQ3fK+r22e$KbDAc_} zISKT_Ou%j1G0{Mz+>z!YVLIA@D|AOF z0j~`H)jT?>Fa{p`a@sH8XiaDIBN>{rwSVBponwZNT?x`Tx4x}TAi!ln^rG0I>?Y15 z^a>|u-wraJ_Xq5ZXD+W@K3GJP2R|% zu%b$hfzhLYq(6;Q5EikUn`m*a5i`V%PjR@%2MF^ALfRtmLVtEd-Npr`uJ1YehEs$Q zW!g^&FyMN-I2z*^*;nYzj%>5pl@=psTZi*0E`_<2*&? zVwP+Q2y_o=3WNnZ!Bs>D2n>i}nrW!2jO|QP927nv=8JQzz3BNK=_Ib+u#;z4p>>+R zmkIc=_|nt;x(lOZlqWocr;BjoUXJLESavT@#!dDJA&A?d^GSykZD7uS7_;7yvV@!>$hj|n-C5fLI z@ej_Bz@>;OiDG0K@(kgVFhdbZx{+u$Lz=S4$qXivG+~7}M-~UW%aKqD2_-%7AW{(E zDx7E^&@mw5? zI0Tp{JUT^EwWNDq1JF9^$hl(YLPp5Q@yG(>s=QSi5?;t$3|(-TB5y~S zKy;KKRZHnVN~;>33K%GsXnjgV)46$;LCF`vJe0!;CQ2duFg>)uIVDsCu5OP~RI0F{ zC+wJuUrW}_2d{IJI{}{wV0?tk(Q(OBj!cT4%9YcK4zjK$#)^*{+d6 zobJ%s^ad^!Y#@L-BzevfBfBTfphIdACL6|*d;=OM(S4|sE_o;_PZIbJR}ufOuKvdg z{tKU!rU&X0S&#gcQaI z+J`diG-zMb!|)3#gO)=%`#PH}-fFTtq*{43FU~5|wpDmrCcG^(n;l=4{U%wwaEYR| zSek$axA5@1*nY=?ftPh`iZ@s&@W}z^{Jor*Gw#Tevcl~&8gKPrA^$bx0!tQTikh&;=xZtlo>ku~*d5VlfH3T0CM8IXp z2sb%~*K1sO>s?t2t>KsFN@5$qDh+i%hiXeW;ANpqzDc#87y+M>0VR=SWdxN^3Qf4~dW)$>98$Q{)2O zr(F$zj4y(CMXLA6SOi|%#EVoE8sA3Quh!Tt2+ zEFy@!c+D7je~A}Sk#=^T*s|9tF)RudC|JRhD+{l1UQk|gg(RAQJ`L6aWgPkQX?PZLn$`%fHx6?^Z9|wmmI(E!=i-UZ5a{!eI4R0cIHD0._completion_with_retry in 4.0 seconds as it raised ServiceUnavailableError: The server is overloaded or not ready yet..\n", - "Retrying langchain_community.chat_models.openai.ChatOpenAI.completion_with_retry.._completion_with_retry in 4.0 seconds as it raised ServiceUnavailableError: The server is overloaded or not ready yet..\n" - ] - }, { "data": { "text/plain": [ "{'input': 'what is the weather in sf',\n", - " 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\":\"weather in San Francisco\"}'}})]),\n", - " [{'url': 'https://www.cbsnews.com/sanfrancisco/news/california-begins-2024-with-below-normal-snowpack-a-year-after-one-of-the-best-starts-in-decades/',\n", - " 'content': 'January 2, 2024 / 3:27 PM PST / AP More from CBS News First published on January 2, 2024 / 2:28 PM PST Watch CBS News California begins 2024 with below-normal snowpack a year after one of the best starts in decades between January and April.New storm packing significant rain, strong winds approaches Bay Area 02:16. California is beginning 2024 with a below-normal mountain snowpack a year after it had one of its best starts in decades ...'}]),\n", - " (AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'current weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'current weather in San Francisco'}`\\nresponded: It seems that the search results did not return the current weather in San Francisco. Let me try another method to fetch the weather information for you.\\n\\n\", message_log=[AIMessage(content='It seems that the search results did not return the current weather in San Francisco. Let me try another method to fetch the weather information for you.', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\":\"current weather in San Francisco\"}'}})]),\n", - " [])],\n", - " 'agent_outcome': AgentFinish(return_values={'output': \"I'm sorry, but it seems that I'm unable to fetch the current weather information for San Francisco at the moment. I recommend using a weather website or app to get the most up-to-date weather forecast for San Francisco.\"}, log=\"I'm sorry, but it seems that I'm unable to fetch the current weather information for San Francisco at the moment. I recommend using a weather website or app to get the most up-to-date weather forecast for San Francisco.\")}" + " 'intermediate_steps': [(AgentActionMessageLog(tool='tavily_search_results_json', tool_input={'query': 'current weather in San Francisco'}, log=\"\\nInvoking: `tavily_search_results_json` with `{'query': 'current weather in San Francisco'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'name': 'tavily_search_results_json', 'arguments': '{\"query\":\"current weather in San Francisco\"}'}})]),\n", + " [{'url': 'https://www.weather25.com/north-america/usa/california/san-francisco',\n", + " 'content': 'will give you an idea of weather trends in San Francisco. For example, the weather in San Francisco in January 2024. San Francisco 14 day weather The weather today in San Francisco San Francisco weather report the weather in San Francisco including humidity, wind, chance of rain and more on the San Francisco current weather Weather25.com provides all the information that you need to know about the weather in San Francisco, United States.The current temperature in San Francisco is ° F. You can find more detailed information about the weather in San Francisco including humidity, wind, chance of rain and more on the San Francisco current weather page. The weather in San Francisco'}])],\n", + " 'agent_outcome': AgentFinish(return_values={'output': 'The current temperature in San Francisco is not available, but you can find detailed information about the weather in San Francisco, including humidity, wind, and chance of rain on the San Francisco current weather page. You can visit [Weather25.com](https://www.weather25.com/north-america/usa/california/san-francisco) for more information.'}, log='The current temperature in San Francisco is not available, but you can find detailed information about the weather in San Francisco, including humidity, wind, and chance of rain on the San Francisco current weather page. You can visit [Weather25.com](https://www.weather25.com/north-america/usa/california/san-francisco) for more information.')}" ] }, "execution_count": 2, @@ -129,7 +128,16 @@ "execution_count": 3, "id": "f6f96e81-4a20-4599-a625-8d18df6fa76d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/nuno/dev/permchain/.venv/lib/python3.11/site-packages/langchain_core/_api/deprecation.py:189: LangChainDeprecationWarning: The class `ChatOpenAI` was deprecated in LangChain 0.1.0 and will be removed in 0.2.0. Use langchain_openai.ChatOpenAI instead.\n", + " warn_deprecated(\n" + ] + } + ], "source": [ "from langchain.agents import AgentExecutor, BaseMultiActionAgent, Tool\n", "from langchain.schema import AgentAction, AgentFinish\n", @@ -333,7 +341,7 @@ { "data": { "text/plain": [ - "AgentFinish(return_values={'output': 'The current weather in San Francisco (SF) is characterized by a mild climate with January daytime maximum temperatures averaging around 13°C (55°F). The city experiences microclimates due to its topography and coastal location, leading to significant weather variations across different neighborhoods[1]. Historically, SF has wet winters and dry summers, with average temperatures ranging from the mid-40s to the low 70s Fahrenheit (7-22 degrees Celsius). The warmest months are typically September and October. Fog is frequent, especially in summer, which can lead to cooler temperatures. The rainy season spans from November to March, with an annual average rainfall of about 23 inches (584 mm). Wind is also a notable factor, particularly in coastal areas.\\n\\nReferences:\\n[1] https://www.weather2travel.com/california/san-francisco/january/', 'initial_answer': \"The weather in San Francisco (SF) is characterized by a mild, Mediterranean-like climate with wet winters and dry summers. The city's unique topography and coastal location result in microclimates, where weather conditions can vary significantly from one neighborhood to another. Average temperatures typically range from the mid-40s to the low 70s Fahrenheit (7-22 degrees Celsius), with the warmest months being September and October. Fog is a common occurrence, particularly in the summer, leading to cooler temperatures compared to the surrounding areas. Rainfall is concentrated from November to March, with the city receiving an average of about 23 inches (584 mm) annually. Wind is another factor to consider, as it can be quite strong, especially near the Golden Gate Bridge. It's always advisable to dress in layers when visiting SF due to the potential for rapid weather changes.\"}, log='Reached max steps.')" + "AgentFinish(return_values={'output': 'The current weather in San Francisco can be accessed through various weather reporting services, which provide up-to-date temperature, humidity, wind, and chance of rain information [1]. Historically, San Francisco experiences a Mediterranean climate with average temperatures ranging from the low 50s to mid-60s Fahrenheit. The city is known for its microclimates, leading to significant weather variations across different neighborhoods. Summer temperatures are often cooler compared to other California areas due to the cold California Current and frequent fog, particularly in June and July. Winters are mild and moist, with most rainfall occurring between November and March, averaging around 23 inches annually. Wind is a prominent feature, especially in spring. For historical weather extremes and average wind speeds, additional specific data would be required.\\n\\nReferences:\\n[1] https://www.weather25.com/north-america/usa/california/san-francisco', 'initial_answer': \"The weather in San Francisco (SF) is characterized by a mild, Mediterranean-like climate with wet winters and dry summers. The city's unique topography and coastal location result in microclimates, where weather conditions can vary significantly from one neighborhood to another. Average temperatures typically range from the low 50s to the mid-60s Fahrenheit throughout the year. Summers in San Francisco are often cooler than in other parts of California due to the cold California Current offshore and the presence of fog, particularly in June and July. The fog usually burns off by the afternoon, leading to clearer skies and slightly warmer temperatures. Winters are mild and moist, with the majority of the city's rainfall occurring between November and March. Rainfall averages around 23 inches annually. Wind is also a notable feature of San Francisco's weather, with spring being the windiest season. Despite the general patterns, it's always advisable to dress in layers due to the potential for rapid weather changes.\"}, log='Reached max steps.')" ] }, "execution_count": 5, @@ -343,31 +351,27 @@ ], "source": [ "workflow = Graph()\n", - "initial_answer_actor = Actor(\"initial\", initial_chain)\n", - "next_step_actor = Actor(\"next\", next_chain)\n", - "finish_actor = Actor(\"finish\", RunnableLambda(finish))\n", - "tool_actor = Actor(\"tools\", RunnableLambda(execute_tools))\n", "\n", "# add actors\n", - "workflow.add_node(initial_answer_actor)\n", - "workflow.add_node(next_step_actor)\n", - "workflow.add_node(finish_actor)\n", - "workflow.add_node(tool_actor)\n", + "workflow.add_node(\"initial\", initial_chain)\n", + "workflow.add_node(\"next\", next_chain)\n", + "workflow.add_node(\"finish\", finish)\n", + "workflow.add_node(\"tools\", execute_tools)\n", "\n", "# Enter with initial actor, then loop through tools -> next steps until finished\n", - "workflow.set_entry_point(initial_answer_actor.key)\n", + "workflow.set_entry_point('initial')\n", "\n", - "workflow.add_edge(initial_answer_actor.key, tool_actor.key)\n", + "workflow.add_edge('initial', 'tools')\n", "workflow.add_conditional_edges(\n", - " tool_actor.key,\n", + " 'tools',\n", " lambda x: \"exit\" if len(x['intermediate_steps']) >= 2 else \"continue\",\n", " {\n", - " \"continue\": next_step_actor.key,\n", - " \"exit\": finish_actor.key\n", + " \"continue\": 'next',\n", + " \"exit\": 'finish'\n", " }\n", ")\n", - "workflow.add_edge(next_step_actor.key, tool_actor.key)\n", - "workflow.set_finish_point(finish_actor.key)\n", + "workflow.add_edge('next', 'tools')\n", + "workflow.set_finish_point('finish')\n", "\n", "chain = workflow.compile()\n", "\n", diff --git a/permchain/__init__.py b/permchain/__init__.py index f72ff7219..24c2199d6 100644 --- a/permchain/__init__.py +++ b/permchain/__init__.py @@ -1,4 +1,5 @@ from permchain.checkpoint.base import BaseCheckpointAdapter, CheckpointAt +from permchain.langgraph import Graph from permchain.pregel import Channel, Pregel, ReservedChannels __all__ = [ @@ -7,4 +8,5 @@ __all__ = [ "ReservedChannels", "BaseCheckpointAdapter", "CheckpointAt", + "Graph", ] diff --git a/permchain/langgraph/__init__.py b/permchain/langgraph/__init__.py index 4876e282c..cca29a9d1 100644 --- a/permchain/langgraph/__init__.py +++ b/permchain/langgraph/__init__.py @@ -1,235 +1,127 @@ -from langchain_core.runnables import Runnable, RunnableMap, RunnableLambda, RunnablePassthrough -from typing import Callable, Union, Optional, List, Any, Dict -from permchain import Channel, Pregel +from collections import defaultdict +from typing import Any, Callable, Dict, NamedTuple -######################################################### -# NODE CLASSES # -######################################################### +from langchain_core.runnables import Runnable +from langchain_core.runnables.base import RunnableLike, coerce_to_runnable -class LangGraphNode: - def __init__(self, key: str): - self.key = key - - def get_runnable(self) -> Runnable: - pass +from permchain.pregel import Channel, Pregel -class Actor(LangGraphNode): - - def __init__(self, key: str, runnable: Runnable): - self.runnable = runnable - super().__init__(key) - - def get_runnable(self) -> Union[Runnable, Callable]: - return self.runnable +class Edge(NamedTuple): + start: str + end: str -class End(LangGraphNode): - def __init__(self): - super().__init__(key="end") +class Branch(NamedTuple): + condition: Callable[..., str] + ends: dict[str, str] - def get_runnable(self) -> Union[Runnable, Callable]: - raise NotImplementedError + def runnable(self, input: Any) -> Runnable: + result = self.condition(input) + return Channel.write_to(self.ends[result]) -class Branch(LangGraphNode): - - def __init__(self, parent_key: str, condition: str): - self.parent_key = parent_key - self.condition = condition - super().__init__(f"{self.parent_key}.{self.condition}") - - def get_runnable(self) -> Union[Runnable, Callable]: - # Only used for structure, so the runnable should never be called - raise NotImplementedError - - -class Conditional(LangGraphNode): - - def __init__(self, key: str, conditional_edge_mapping: Dict[str, str], callable: Callable): - self.callable = callable - self.branches = [] - - for condition, output in conditional_edge_mapping.items(): - self.branches.append(Branch(key, condition)) - - self.conditional_edge_mapping = conditional_edge_mapping - - super().__init__(key) - - def get_runnable(self) -> Union[Runnable, Callable]: - return self.callable - - - -######################################################### -# EDGE CLASSES # -######################################################### - -class LangGraphEdge: - def __init__(self, start_key: str, end_key: str): - self.start_key = start_key - self.end_key = end_key - - def flow(self, node_map: Dict[str, LangGraphNode]): - return ( - Channel.subscribe_to(self.start_key) | - node_map[self.start_key].get_runnable() | - Channel.write_to(self.end_key) - ) - - -class BranchEdge(LangGraphEdge): - - def flow(self, node_map: Dict[str, LangGraphNode]): - # flow should skip over the branch edge - raise NotImplementedError - - -class ConditionalEdge(LangGraphEdge): - - def __init__(self, base_key: str, branch_key: str): - self.base_key = base_key - self.branch_key = branch_key - if not branch_key.startswith(base_key) or not branch_key[len(base_key)] == ".": - raise ValueError(f"Invalid branch edge from {base_key} to {branch_key}") - - super().__init__(base_key, branch_key) - - def _branch(self, data, condition, mapping): - result = condition(data) - return Channel.write_to(mapping[result]) - - def flow(self, node_map: Dict[str, LangGraphNode]): - conditional_node = node_map[self.base_key] - - return ( - Channel.subscribe_to(self.start_key) | - ( - lambda x: self._branch( - x, - conditional_node.get_runnable(), - conditional_node.conditional_edge_mapping - ) - ) - ) +START = "__start__" +END = "__end__" class Graph: - def __init__(self): - end_node = End() - self.nodes = {end_node.key: end_node} - self.edges = [] - - # self.connections = {} - # self.branches = {} - self.entry_point: Optional[str] = None + self.nodes: dict[str, Runnable] = {} + self.edges = set[Edge]() + self.branches: defaultdict[str, list[Branch]] = defaultdict(list) - def add_node(self, node: Actor): - if node.key in self.nodes: - raise ValueError(f"Actor `{node.key}` already present.") - self.nodes[node.key] = node + def add_node(self, key: str, action: RunnableLike) -> None: + if key in self.nodes: + raise ValueError(f"Node `{key}` already present.") + self.nodes[key] = coerce_to_runnable(action) - def add_edge(self, start_key: str, end_key: str): + def add_edge(self, start_key: str, end_key: str) -> None: if start_key not in self.nodes: raise ValueError(f"Need to add_node `{start_key}` first") if end_key not in self.nodes: raise ValueError(f"Need to add_node `{end_key}` first") # TODO: support multiple message passing - if start_key in set(edge.start_key for edge in self.edges): + if start_key in set(start for start, _ in self.edges): raise ValueError(f"Already found path for {start_key}") - - self.edges.append(LangGraphEdge(start_key, end_key)) + + self.edges.add((start_key, end_key)) def add_conditional_edges( self, start_key: str, - condition: Callable[Any, str], - conditional_edge_mapping: Dict[str, str]): + condition: Callable[..., str], + conditional_edge_mapping: Dict[str, str], + ): + if start_key not in self.nodes: + raise ValueError(f"Need to add_node `{start_key}` first") - conditional_node = Conditional( - f"_conditional_from_{start_key}", - conditional_edge_mapping, - condition - ) + self.branches[start_key].append(Branch(condition, conditional_edge_mapping)) - self.add_node(conditional_node) - self.add_edge(start_key, conditional_node.key) - - for branch in conditional_node.branches: - self.add_node(branch) - self.edges.append(ConditionalEdge(conditional_node.key, branch.key)) - self.edges.append( - BranchEdge(branch.key, conditional_node.conditional_edge_mapping[branch.condition]) - ) - - def set_entry_point(self, key: str): if key not in self.nodes: - raise ValueError(f"Need to add_node `{node.key}` first") + raise ValueError(f"Need to add_node `{key}` first") self.entry_point = key def set_finish_point(self, key: str): if key not in self.nodes: - raise ValueError(f"Need to add_node `{node.key}` first") - self.add_edge(key, "end") + raise ValueError(f"Need to add_node `{key}` first") + self.finish_point = key def compile(self): - ################################################ # STEP 1: VALIDATE GRAPH STRUCTURE # ################################################ - seen_node_keys = set() - all_node_keys = set(self.nodes.keys()) - - edge_map = {} - for edge in self.edges: - edge_map[edge.start_key] = edge_map.get(edge.start_key, []) + [edge.end_key] - to_see = [self.entry_point] - while len(to_see) > 0: - current = to_see.pop(0) - if current in seen_node_keys: - continue + all_starts = ( + {start for start, _ in self.edges} + | {start for start in self.branches} + | ({self.finish_point} if hasattr(self, "finish_point") else set()) + ) + all_ends = ( + {end for _, end in self.edges} + | { + end + for branch_list in self.branches.values() + for branch in branch_list + for end in branch.ends.values() + } + | {self.entry_point} + ) - seen_node_keys.add(current) - next_nodes = edge_map.get(current, []) - to_see += next_nodes + for node in self.nodes: + if node not in all_ends: + raise ValueError(f"Node `{node}` is not reachable") + if node not in all_starts: + raise ValueError(f"Node `{node}` is a dead-end") - if len(next_nodes) == 0 and current != "end": - raise ValueError(f"Node {current} is a dead end") - - if seen_node_keys != all_node_keys: - raise ValueError(f"Found unreachable nodes: {list(all_node_keys - seen_node_keys)}") - - ################################################ # STEP 2: CREATE GRAPH # ################################################ + outgoing_edges = defaultdict(list) + for start, end in self.edges: + outgoing_edges[start].append(end) + if hasattr(self, "finish_point"): + outgoing_edges[self.finish_point].append(END) + chains = { - edge.start_key: edge.flow(self.nodes) - for edge in self.edges - # specifically skip over branch edges since they are defined purely for structure - if not isinstance(edge, BranchEdge) + key: ( + Channel.subscribe_to(key) + | node + | Channel.write_to(*outgoing_edges[key]) + ) + for key, node in self.nodes.items() } - - app = Pregel( + + for key, branches in self.branches.items(): + for branch in branches: + chains[key] |= branch.runnable + + return Pregel( chains=chains, input=self.entry_point, - output="end" + output=END, ) - return app - - - - - - - - - - diff --git a/permchain/pregel/__init__.py b/permchain/pregel/__init__.py index 1eee4933b..e97926b4c 100644 --- a/permchain/pregel/__init__.py +++ b/permchain/pregel/__init__.py @@ -633,7 +633,7 @@ def _updateable_channel_values(channels: Mapping[str, BaseChannel]) -> dict[str, """Return a dictionary of updateable channel values.""" values: dict[str, Any] = {} for k, v in channels.items(): - if isinstance(v, LastValue): + if isinstance(v, LastValue) and k not in [c.value for c in ReservedChannels]: try: values[k] = v.get() except EmptyChannelError: diff --git a/poetry.lock b/poetry.lock index f8f608c67..5a769b810 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1520,13 +1520,13 @@ files = [ [[package]] name = "langchain" -version = "0.0.353" +version = "0.1.0" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain-0.0.353-py3-none-any.whl", hash = "sha256:54cac8b74fbefacddcdf0c443619a7331d6b59fe94fa2a48a4d7da2b59cf1f63"}, - {file = "langchain-0.0.353.tar.gz", hash = "sha256:a095ea819f13a3606ced699182a8369eb2d77034ec8c913983675d6dd9a98196"}, + {file = "langchain-0.1.0-py3-none-any.whl", hash = "sha256:8652e74b039333a55c79faff4400b077ba1bd0ddce5255574e42d301c05c1733"}, + {file = "langchain-0.1.0.tar.gz", hash = "sha256:d43119f8d3fda2c8ddf8c3a19bd5b94b347e27d1867ff14a921b90bdbed0668a"}, ] [package.dependencies] @@ -1534,9 +1534,9 @@ aiohttp = ">=3.8.3,<4.0.0" async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""} dataclasses-json = ">=0.5.7,<0.7" jsonpatch = ">=1.33,<2.0" -langchain-community = ">=0.0.2,<0.1" -langchain-core = ">=0.1.4,<0.2" -langsmith = ">=0.0.70,<0.1.0" +langchain-community = ">=0.0.9,<0.1" +langchain-core = ">=0.1.7,<0.2" +langsmith = ">=0.0.77,<0.1.0" numpy = ">=1,<2" pydantic = ">=1,<3" PyYAML = ">=5.3" @@ -1551,7 +1551,7 @@ cli = ["typer (>=0.9.0,<0.10.0)"] cohere = ["cohere (>=4,<5)"] docarray = ["docarray[hnswlib] (>=0.32.0,<0.33.0)"] embeddings = ["sentence-transformers (>=2,<3)"] -extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "couchbase (>=4.1.9,<5.0.0)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"] +extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "couchbase (>=4.1.9,<5.0.0)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "langchain-openai (>=0.0.2,<0.1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"] javascript = ["esprima (>=4.0.1,<5.0.0)"] llms = ["clarifai (>=9.1.0)", "cohere (>=4,<5)", "huggingface_hub (>=0,<1)", "manifest-ml (>=0.0.1,<0.0.2)", "nlpcloud (>=1,<2)", "openai (<2)", "openlm (>=0.0.5,<0.0.6)", "torch (>=1,<3)", "transformers (>=4,<5)"] openai = ["openai (<2)", "tiktoken (>=0.3.2,<0.6.0)"] @@ -1560,19 +1560,19 @@ text-helpers = ["chardet (>=5.1.0,<6.0.0)"] [[package]] name = "langchain-community" -version = "0.0.7" +version = "0.0.9" description = "Community contributed LangChain integrations." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain_community-0.0.7-py3-none-any.whl", hash = "sha256:468af187bfffe753426cc4548132824be7df9404d38ceef2f873087290d8ff0e"}, - {file = "langchain_community-0.0.7.tar.gz", hash = "sha256:cfbeb25cac7dff3c021f3c82aa243fc80f80082d6f6fdcc79daf36b1408828cc"}, + {file = "langchain_community-0.0.9-py3-none-any.whl", hash = "sha256:21e1f96c776541255b7067f32aafbf065f78a33be8f0e2660080ddc3e9ed48b7"}, + {file = "langchain_community-0.0.9.tar.gz", hash = "sha256:b14f10b249fd61b0b8e3d2896f85c2d577eb4a5e2ae01291e2a4ebbe1bb3c370"}, ] [package.dependencies] aiohttp = ">=3.8.3,<4.0.0" dataclasses-json = ">=0.5.7,<0.7" -langchain-core = ">=0.1,<0.2" +langchain-core = ">=0.1.7,<0.2" langsmith = ">=0.0.63,<0.1.0" numpy = ">=1,<2" PyYAML = ">=5.3" @@ -1582,17 +1582,17 @@ tenacity = ">=8.1.0,<9.0.0" [package.extras] cli = ["typer (>=0.9.0,<0.10.0)"] -extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "azure-ai-documentintelligence (>=1.0.0b1,<2.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "gradientai (>=1.4.0,<2.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "oracle-ads (>=2.9.1,<3.0.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)"] +extended-testing = ["aiosqlite (>=0.19.0,<0.20.0)", "aleph-alpha-client (>=2.15.0,<3.0.0)", "anthropic (>=0.3.11,<0.4.0)", "arxiv (>=1.4,<2.0)", "assemblyai (>=0.17.0,<0.18.0)", "atlassian-python-api (>=3.36.0,<4.0.0)", "azure-ai-documentintelligence (>=1.0.0b1,<2.0.0)", "beautifulsoup4 (>=4,<5)", "bibtexparser (>=1.4.0,<2.0.0)", "cassio (>=0.1.0,<0.2.0)", "chardet (>=5.1.0,<6.0.0)", "cohere (>=4,<5)", "dashvector (>=1.0.1,<2.0.0)", "databricks-vectorsearch (>=0.21,<0.22)", "datasets (>=2.15.0,<3.0.0)", "dgml-utils (>=0.3.0,<0.4.0)", "esprima (>=4.0.1,<5.0.0)", "faiss-cpu (>=1,<2)", "feedparser (>=6.0.10,<7.0.0)", "fireworks-ai (>=0.9.0,<0.10.0)", "geopandas (>=0.13.1,<0.14.0)", "gitpython (>=3.1.32,<4.0.0)", "google-cloud-documentai (>=2.20.1,<3.0.0)", "gql (>=3.4.1,<4.0.0)", "gradientai (>=1.4.0,<2.0.0)", "hologres-vector (>=0.0.6,<0.0.7)", "html2text (>=2020.1.16,<2021.0.0)", "javelin-sdk (>=0.1.8,<0.2.0)", "jinja2 (>=3,<4)", "jq (>=1.4.1,<2.0.0)", "jsonschema (>1)", "lxml (>=4.9.2,<5.0.0)", "markdownify (>=0.11.6,<0.12.0)", "motor (>=3.3.1,<4.0.0)", "msal (>=1.25.0,<2.0.0)", "mwparserfromhell (>=0.6.4,<0.7.0)", "mwxml (>=0.3.3,<0.4.0)", "newspaper3k (>=0.2.8,<0.3.0)", "numexpr (>=2.8.6,<3.0.0)", "openai (<2)", "openapi-pydantic (>=0.3.2,<0.4.0)", "oracle-ads (>=2.9.1,<3.0.0)", "pandas (>=2.0.1,<3.0.0)", "pdfminer-six (>=20221105,<20221106)", "pgvector (>=0.1.6,<0.2.0)", "praw (>=7.7.1,<8.0.0)", "psychicapi (>=0.8.0,<0.9.0)", "py-trello (>=0.19.0,<0.20.0)", "pymupdf (>=1.22.3,<2.0.0)", "pypdf (>=3.4.0,<4.0.0)", "pypdfium2 (>=4.10.0,<5.0.0)", "pyspark (>=3.4.0,<4.0.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "rapidfuzz (>=3.1.1,<4.0.0)", "rapidocr-onnxruntime (>=1.3.2,<2.0.0)", "requests-toolbelt (>=1.0.0,<2.0.0)", "rspace_client (>=2.5.0,<3.0.0)", "scikit-learn (>=1.2.2,<2.0.0)", "sqlite-vss (>=0.1.2,<0.2.0)", "streamlit (>=1.18.0,<2.0.0)", "sympy (>=1.12,<2.0)", "telethon (>=1.28.5,<2.0.0)", "timescale-vector (>=0.0.1,<0.0.2)", "tqdm (>=4.48.0)", "upstash-redis (>=0.15.0,<0.16.0)", "xata (>=1.0.0a7,<2.0.0)", "xmltodict (>=0.13.0,<0.14.0)", "zhipuai (>=1.0.7,<2.0.0)"] [[package]] name = "langchain-core" -version = "0.1.4" +version = "0.1.7" description = "Building applications with LLMs through composability" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langchain_core-0.1.4-py3-none-any.whl", hash = "sha256:c62bd362d5abf5359436a99b29629e12a4d1ede9f1704dc958cdb8530a791efd"}, - {file = "langchain_core-0.1.4.tar.gz", hash = "sha256:f700138689c9014e23d3c29796a892dccf7f2a42901cb8817671823e1a24724c"}, + {file = "langchain_core-0.1.7-py3-none-any.whl", hash = "sha256:c66327dbb4b7d4ab911556aa0511ebf4f40801ad66d98778fb5566dba45b0091"}, + {file = "langchain_core-0.1.7.tar.gz", hash = "sha256:c05211a309721d67aa5a681c946a2f010e14632a2bea3728da0a30a2534efa9e"}, ] [package.dependencies] @@ -1625,13 +1625,13 @@ types-requests = ">=2.31.0.2,<3.0.0.0" [[package]] name = "langsmith" -version = "0.0.75" +version = "0.0.77" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.75-py3-none-any.whl", hash = "sha256:3e008854204c5eaae007f34c7e249059218605689c385c037f6a40cac044833b"}, - {file = "langsmith-0.0.75.tar.gz", hash = "sha256:3fd44c58bd53cb9366af3de129c7f11b6947914f1bb598a585240df0e2c566eb"}, + {file = "langsmith-0.0.77-py3-none-any.whl", hash = "sha256:750c0aa9177240c64e131d831e009ed08dd59038f7cabbd0bbcf62ccb7c8dcac"}, + {file = "langsmith-0.0.77.tar.gz", hash = "sha256:c4c8d3a96ad8671a41064f3ccc673e2e22a4153e823b19f915c9c9b8a4f33a2c"}, ] [package.dependencies] @@ -3629,4 +3629,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "4e4d02201942d821f9a687e6c0eb787ed841d537498a0fd118226cd48f8d022c" +content-hash = "4f1c52f5b61024577a687d37a2d42023f6519f31f36f47449779db8f4f3ca355" diff --git a/pyproject.toml b/pyproject.toml index 88bf3781c..ce2741652 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ optional = true [tool.poetry.group.dev.dependencies] jupyter = "^1.0.0" openai = "^0.27.8" -langchain = "^0.0.353" +langchain = "^0.1.0" langchainhub = "^0.1.14" [tool.ruff] diff --git a/tests/test_graph.py b/tests/test_graph.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_pregel.py b/tests/test_pregel.py index aad297018..163c9ea11 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -8,7 +8,7 @@ import pytest from langchain_core.runnables import RunnablePassthrough from pytest_mock import MockerFixture -from permchain import Channel, Pregel +from permchain import Channel, Graph, Pregel from permchain.channels.base import InvalidUpdateError from permchain.channels.binop import BinaryOperatorAggregate from permchain.channels.context import Context @@ -33,12 +33,19 @@ def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: input="input", output="output", ) + graph = Graph() + graph.add_node("add_one", add_one) + graph.set_entry_point("add_one") + graph.set_finish_point("add_one") + gapp = graph.compile() assert app.input_schema.schema() == {"title": "PregelInput", "type": "integer"} assert app.output_schema.schema() == {"title": "PregelOutput", "type": "integer"} assert app.invoke(2) == 3 assert repr(app), "does not raise recursion error" + assert gapp.invoke(2) == 3 + def test_invoke_single_process_in_out_implicit_channels(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) @@ -152,7 +159,6 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert view.values == { "inbox": 3, "input": 2, - "is_last_step": False, } assert output is None elif view.step == 2: @@ -160,7 +166,6 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "output": 4, "inbox": 3, "input": 2, - "is_last_step": False, } assert output == 4 @@ -169,7 +174,6 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert view.values == { "inbox": 3, "input": 2, - "is_last_step": False, } assert output is None # modify inbox value @@ -179,7 +183,49 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "output": 6, "inbox": 5, "input": 2, - "is_last_step": False, + } + # output is different now + assert output == 6 + + graph = Graph() + graph.add_node("add_one", add_one) + graph.add_node("add_one_more", add_one) + graph.set_entry_point("add_one") + graph.set_finish_point("add_one_more") + graph.add_edge("add_one", "add_one_more") + gapp = graph.compile() + + assert gapp.invoke(2) == 4 + + for output, view in gapp.step(2): + if view.step == 1: + assert view.values == { + "add_one": 2, + "add_one_more": 3, + } + assert output is None + elif view.step == 2: + assert view.values == { + "add_one": 2, + "add_one_more": 3, + "__end__": 4, + } + assert output == 4 + + for output, view in gapp.step(2): + if view.step == 1: + assert view.values == { + "add_one": 2, + "add_one_more": 3, + } + assert output is None + # modify inbox value + view.values["add_one_more"] = 5 + elif view.step == 2: + assert view.values == { + "add_one": 2, + "add_one_more": 5, + "__end__": 6, } # output is different now assert output == 6 @@ -217,6 +263,16 @@ def test_batch_two_processes_in_out() -> None: assert app.batch([3, 2, 1, 3, 5]) == [5, 4, 3, 5, 7] + graph = Graph() + graph.add_node("add_one", add_one_with_delay) + graph.add_node("add_one_more", add_one_with_delay) + graph.set_entry_point("add_one") + graph.set_finish_point("add_one_more") + graph.add_edge("add_one", "add_one_more") + gapp = graph.compile() + + assert gapp.batch([3, 2, 1, 3, 5]) == [5, 4, 3, 5, 7] + def test_invoke_many_processes_in_out(mocker: MockerFixture) -> None: test_size = 100 diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index b32dc17d2..90e82d94c 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -157,7 +157,6 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert view.values == { "inbox": 3, "input": 2, - "is_last_step": False, } assert output is None elif view.step == 2: @@ -165,7 +164,6 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "output": 4, "inbox": 3, "input": 2, - "is_last_step": False, } assert output == 4 @@ -174,7 +172,6 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert view.values == { "inbox": 3, "input": 2, - "is_last_step": False, } assert output is None # modify inbox value @@ -184,7 +181,6 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: "output": 6, "inbox": 5, "input": 2, - "is_last_step": False, } # output is different now assert output == 6