diff --git a/examples/langgraph.ipynb b/examples/langgraph.ipynb new file mode 100644 index 000000000..f3500d8bf --- /dev/null +++ b/examples/langgraph.ipynb @@ -0,0 +1,107 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "53b9dce4-e4ae-4bdb-b752-0f04350a2e3d", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.chat_models import ChatOpenAI\n", + "from langchain.agents import create_openai_functions_agent\n", + "from langchain_core.prompts import PromptTemplate\n", + "from langchain import hub\n", + "from langchain.agents import AgentExecutor, create_openai_functions_agent\n", + "from langchain_community.chat_models import ChatOpenAI\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.runnables import RunnablePassthrough, RunnableLambda\n", + "from permchain.langgraph import Actor, DecisionPoint, Graph\n", + "\n", + "tools = [TavilySearchResults(max_results=1)]\n", + "\n", + "# Get the prompt to use - you can modify this!\n", + "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", + "\n", + "# Choose the LLM that will drive the agent\n", + "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\")\n", + "\n", + "# Construct the OpenAI Functions agent\n", + "agent_runnable = create_openai_functions_agent(llm, tools, prompt)\n", + "\n", + "from langchain_core.agents import AgentFinish\n", + "# Define decision-making logic\n", + "def should_continue(data):\n", + " # Logic to decide whether to continue in the loop or exit\n", + " if isinstance(data['agent_outcome'], AgentFinish):\n", + " return \"exit\"\n", + " else:\n", + " return \"continue\"\n", + " \n", + "def execute_tools(data):\n", + " agent_action = data.pop('agent_outcome')\n", + " observation = {t.name: t for t in tools}[agent_action.tool].invoke(agent_action.tool_input)\n", + " data['intermediate_steps'].append((agent_action, observation))\n", + " return data\n", + " \n", + " \n", + "\n", + "# Define agents\n", + "agent = RunnablePassthrough.assign(\n", + " agent_outcome = agent_runnable\n", + ")\n", + "\n", + "# Define a new graph\n", + "workflow = Graph()\n", + "decision_point = DecisionPoint(\"exit?\", should_continue)\n", + "llm_agent = Actor(\"agent\", agent)\n", + "tool_actor = Actor(\"tools\", RunnableLambda(execute_tools))\n", + "\n", + "# Register actors\n", + "workflow.register(llm_agent)\n", + "workflow.register(tool_actor)\n", + "workflow.register(decision_point)\n", + "\n", + "# Define connections with conditional logic\n", + "workflow.connect(llm_agent, decision_point)\n", + "workflow.branch(decision_point, tool_actor, condition=\"continue\")\n", + "workflow.branch(decision_point, None, condition=\"exit\") # Exit the workflow\n", + "workflow.connect(tool_actor, llm_agent)\n", + "\n", + "# Define entry point and execute the graph\n", + "workflow.set_entry_point(llm_agent)\n", + "chain = workflow.compile()\n", + "\n", + "chain.invoke({\"input\": \"what is the weather in sf\", \"intermediate_steps\": []})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2515edbc-a9b6-42ce-bb5e-f1f2503d1bb4", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/permchain/langgraph/__init__.py b/permchain/langgraph/__init__.py new file mode 100644 index 000000000..f93eff29f --- /dev/null +++ b/permchain/langgraph/__init__.py @@ -0,0 +1,115 @@ +from langchain_core.runnables import Runnable, RunnableMap +from typing import Callable, Union, Optional +from permchain import Channel, Pregel + +class Actor: + + def __init__(self, name: str, runnable: Runnable): + self.name = name + self.runnable = runnable + + +class DecisionPoint: + + def __init__(self, name: str, callable: Callable): + self.name = name + self.callable = callable + +class End: + name = "end" + + +def branch(data, condition, mapping): + result = condition(data) + return Channel.write_to(mapping[result]) + +class Graph: + + def __init__(self): + self.nodes = {"end": End()} + self.connections = {} + self.branches = {} + self.entry_point: Optional[str] = None + self.finish_points = set() + + def register(self, node: Union[Actor, DecisionPoint]): + if node.name in self.nodes: + raise ValueError(f"Actor `{node.name}` already present.") + self.nodes[node.name] = node + + def connect(self, start: Actor, end: Union[Actor, DecisionPoint]): + if start.name not in self.nodes: + raise ValueError(f"Need to register `{start.name}` first") + if end.name not in self.nodes: + raise ValueError(f"Need to register `{end.name}` first") + if start.name in self.connections: + raise ValueError(f"Already found path for {start.name}") + self.connections[start.name] = end.name + + def branch(self, start: DecisionPoint, end: Optional[Actor], condition: str): + end = end or End() + if start.name not in self.nodes: + raise ValueError(f"Need to register `{start.name}` first") + if end.name not in self.nodes: + raise ValueError(f"Need to register `{end.name}` first") + if start.name not in self.branches: + self.branches[start.name] = {} + if condition in self.branches[start.name]: + raise ValueError(f"Already found a condition for {start.name} and {condition}") + self.branches[start.name][condition] = end.name + + def set_entry_point(self, node: Union[DecisionPoint, Actor]): + if node.name not in self.nodes: + raise ValueError(f"Need to register `{node.name}` first") + self.entry_point = node.name + + def set_finish_point(self, node: Actor): + if node.name not in self.nodes: + raise ValueError(f"Need to register `{node.name}` first") + self.finish_points |= node.name + + def compile(self): + # Validate all nodes have an entry point + all_nodes = set(self.nodes) + all_entry_points = set(self.connections).union(self.branches).union(self.finish_points) + branch_ends = set() + for v in self.branches.values(): + branch_ends.update(v.values()) + all_finish_points = set(self.connections.values()).union(branch_ends).union({self.entry_point}) + # If a node is not a finish point, then it is missing an entry point + missing_entry = all_nodes.difference(all_finish_points) + if missing_entry: + raise ValueError(f"Some nodes are missing entry points: {missing_entry}") + # If a node is not an entry point, then it is missing a finish point + missing_finish = all_nodes.difference(all_entry_points).difference({"end"}) + if missing_finish: + raise ValueError(f"Some nodes are missing finish points: {missing_finish}") + chains = { + start: Channel.subscribe_to(start) | self.nodes[start].runnable | Channel.write_to(end) + for start, end in self.connections.items() + } + + decisions = { + start: Channel.subscribe_to(start) | (lambda x: branch(x, self.nodes[start].callable, mapping)) + for start, mapping in self.branches.items() + } + endings = { + end: Channel.subscribe_to(end) | self.nodes[end].runnable | Channel.write_to("end") + for end in self.finish_points + } + app = Pregel( + chains = {**chains, **decisions, **endings}, + input=self.entry_point, + output="end" + ) + return app + + + + + + + + + +