Setup New Project template

(Remove react agent code)
This commit is contained in:
William FH
2024-09-13 17:12:07 -07:00
committed by William Fu-Hinthorn
21 changed files with 138 additions and 784 deletions

View File

@@ -1,7 +1,5 @@
TAVILY_API_KEY=...
# To separate your traces from other application
LANGSMITH_PROJECT=retrieval-agent
LANGSMITH_PROJECT=new-agent
# The following depend on your selected configuration

View File

@@ -36,7 +36,6 @@ jobs:
- name: Run integration tests
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
LANGSMITH_TRACING: true
run: |

1
.gitignore vendored
View File

@@ -160,3 +160,4 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
uv.lock

View File

@@ -9,6 +9,9 @@ TEST_FILE ?= tests/unit_tests/
test:
python -m pytest $(TEST_FILE)
integration_tests:
python -m pytest tests/integration_tests
test_watch:
python -m ptw --snapshot-update --now . -- -vv tests/unit_tests

438
README.md
View File

@@ -1,25 +1,25 @@
# LangGraph ReAct Agent Template
# New LangGraph Project
[![CI](https://github.com/langchain-ai/react-agent/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/langchain-ai/react-agent/actions/workflows/unit-tests.yml)
[![Integration Tests](https://github.com/langchain-ai/react-agent/actions/workflows/integration-tests.yml/badge.svg)](https://github.com/langchain-ai/react-agent/actions/workflows/integration-tests.yml)
[![CI](https://github.com/langchain-ai/new-langgraph-project/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/langchain-ai/new-langgraph-project/actions/workflows/unit-tests.yml)
[![Integration Tests](https://github.com/langchain-ai/new-langgraph-project/actions/workflows/integration-tests.yml/badge.svg)](https://github.com/langchain-ai/new-langgraph-project/actions/workflows/integration-tests.yml)
[![Open in - LangGraph Studio](https://img.shields.io/badge/Open_in-LangGraph_Studio-00324d.svg?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4NS4zMzMiIGhlaWdodD0iODUuMzMzIiB2ZXJzaW9uPSIxLjAiIHZpZXdCb3g9IjAgMCA2NCA2NCI+PHBhdGggZD0iTTEzIDcuOGMtNi4zIDMuMS03LjEgNi4zLTYuOCAyNS43LjQgMjQuNi4zIDI0LjUgMjUuOSAyNC41QzU3LjUgNTggNTggNTcuNSA1OCAzMi4zIDU4IDcuMyA1Ni43IDYgMzIgNmMtMTIuOCAwLTE2LjEuMy0xOSAxLjhtMzcuNiAxNi42YzIuOCAyLjggMy40IDQuMiAzLjQgNy42cy0uNiA0LjgtMy40IDcuNkw0Ny4yIDQzSDE2LjhsLTMuNC0zLjRjLTQuOC00LjgtNC44LTEwLjQgMC0xNS4ybDMuNC0zLjRoMzAuNHoiLz48cGF0aCBkPSJNMTguOSAyNS42Yy0xLjEgMS4zLTEgMS43LjQgMi41LjkuNiAxLjcgMS44IDEuNyAyLjcgMCAxIC43IDIuOCAxLjYgNC4xIDEuNCAxLjkgMS40IDIuNS4zIDMuMi0xIC42LS42LjkgMS40LjkgMS41IDAgMi43LS41IDIuNy0xIDAtLjYgMS4xLS44IDIuNi0uNGwyLjYuNy0xLjgtMi45Yy01LjktOS4zLTkuNC0xMi4zLTExLjUtOS44TTM5IDI2YzAgMS4xLS45IDIuNS0yIDMuMi0yLjQgMS41LTIuNiAzLjQtLjUgNC4yLjguMyAyIDEuNyAyLjUgMy4xLjYgMS41IDEuNCAyLjMgMiAyIDEuNS0uOSAxLjItMy41LS40LTMuNS0yLjEgMC0yLjgtMi44LS44LTMuMyAxLjYtLjQgMS42LS41IDAtLjYtMS4xLS4xLTEuNS0uNi0xLjItMS42LjctMS43IDMuMy0yLjEgMy41LS41LjEuNS4yIDEuNi4zIDIuMiAwIC43LjkgMS40IDEuOSAxLjYgMi4xLjQgMi4zLTIuMy4yLTMuMi0uOC0uMy0yLTEuNy0yLjUtMy4xLTEuMS0zLTMtMy4zLTMtLjUiLz48L3N2Zz4=)](https://langgraph-studio.vercel.app/templates/open?githubUrl=https://github.com/langchain-ai/new-langgraph-project)
This template showcases a [ReAct agent](https://arxiv.org/abs/2210.03629) implemented using [LangGraph](https://github.com/langchain-ai/langgraph), designed for [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio). ReAct agents are uncomplicated, prototypical agents that can be flexibly extended to many tools.
This template demonstrates a simple chatbot implemented using [LangGraph](https://github.com/langchain-ai/langgraph), designed for [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio). The chatbot maintains persistent chat memory, allowing for coherent conversations across multiple interactions.
![Graph view in LangGraph studio UI](./static/studio_ui.png)
The core logic, defined in `src/react_agent/graph.py`, demonstrates a flexible ReAct agent that iteratively reasons about user queries and executes actions, showcasing the power of this approach for complex problem-solving tasks.
The core logic, defined in `src/agent/graph.py`, showcases a straightforward chatbot that responds to user queries while maintaining context from previous messages.
## What it does
The ReAct agent:
The simple chatbot:
1. Takes a user **query** as input
2. Reasons about the query and decides on an action
3. Executes the chosen action using available tools
4. Observes the result of the action
5. Repeats steps 2-4 until it can provide a final answer
1. Takes a user **message** as input
2. Maintains a history of the conversation
3. Generates a response based on the current message and conversation history
4. Updates the conversation history with the new interaction
By default, it's set up with a basic set of tools, but can be easily extended with custom tools to suit various use cases.
This template provides a foundation that can be easily customized and extended to create more complex conversational agents.
## Getting Started
@@ -33,41 +33,44 @@ cp .env.example .env
2. Define required API keys in your `.env` file.
The primary [search tool](./src/react_agent/tools.py) [^1] used is [Tavily](https://tavily.com/). Create an API key [here](https://app.tavily.com/sign-in).
<!--
Setup instruction auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
-->
Set up your LLM API keys. This repo defaults to using [Claude](https://console.anthropic.com/login).
<!--
End setup instructions
-->
3. Customize whatever you'd like in the code.
4. Open the folder LangGraph Studio!
3. Customize the code as needed.
4. Open the folder in LangGraph Studio!
## How to customize
1. **Add new tools**: Extend the agent's capabilities by adding new tools in [tools.py](./src/react_agent/tools.py). These can be any Python functions that perform specific tasks.
1. **Modify the system prompt**: The default system prompt is defined in [configuration.py](./src/agent/configuration.py). You can easily update this via configuration in the studio to change the chatbot's personality or behavior.
2. **Select a different model**: We default to Anthropic's Claude 3 Sonnet. You can select a compatible chat model using `provider/model-name` via configuration. Example: `openai/gpt-4-turbo-preview`.
3. **Customize the prompt**: We provide a default system prompt in [configuration.py](./src/react_agent/configuration.py). You can easily update this via configuration in the studio.
3. **Extend the graph**: The core logic of the chatbot is defined in [graph.py](./src/agent/graph.py). You can modify this file to add new nodes, edges, or change the flow of the conversation.
You can also quickly extend this template by:
- Modifying the agent's reasoning process in [graph.py](./src/react_agent/graph.py).
- Adjusting the ReAct loop or adding additional steps to the agent's decision-making process.
- Adding custom tools or functions to enhance the chatbot's capabilities.
- Implementing additional logic for handling specific types of user queries or tasks.
- Integrating external APIs or databases to provide more dynamic responses.
## Development
While iterating on your graph, you can edit past state and rerun your app from past states to debug specific nodes. Local changes will be automatically applied via hot reload. Try adding an interrupt before the agent calls tools, updating the default system message in `src/react_agent/configuration.py` to take on a persona, or adding additional nodes and edges!
While iterating on your graph, you can edit past state and rerun your app from previous states to debug specific nodes. Local changes will be automatically applied via hot reload. Try experimenting with:
Follow up requests will be appended to the same thread. You can create an entirely new thread, clearing previous history, using the `+` button in the top right.
- Modifying the system prompt to give your chatbot a unique personality.
- Adding new nodes to the graph for more complex conversation flows.
- Implementing conditional logic to handle different types of user inputs.
You can find the latest (under construction) docs on [LangGraph](https://github.com/langchain-ai/langgraph) here, including examples and other references. Using those guides can help you pick the right patterns to adapt here for your use case.
Follow-up requests will be appended to the same thread. You can create an entirely new thread, clearing previous history, using the `+` button in the top right.
LangGraph Studio also integrates with [LangSmith](https://smith.langchain.com/) for more in-depth tracing and collaboration with teammates.
For more advanced features and examples, refer to the [LangGraph documentation](https://github.com/langchain-ai/langgraph). These resources can help you adapt this template for your specific use case and build more sophisticated conversational agents.
LangGraph Studio also integrates with [LangSmith](https://smith.langchain.com/) for more in-depth tracing and collaboration with teammates, allowing you to analyze and optimize your chatbot's performance.
<!--
Configuration auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
@@ -75,389 +78,8 @@ Configuration auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
"config_schemas": {
"agent": {
"type": "object",
"properties": {
"system_prompt": {
"type": "string",
"default": "You are a helpful AI assistant.\n\nSystem time: {system_time}"
},
"model_name": {
"type": "string",
"default": "anthropic/claude-3-5-sonnet-20240620",
"environment": [
{
"value": "anthropic/claude-1.2",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-2.0",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-2.1",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-3-5-sonnet-20240620",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-3-haiku-20240307",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-3-opus-20240229",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-3-sonnet-20240229",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-instant-1.2",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "fireworks/gemma2-9b-it",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3-70b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3-70b-instruct-hf",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3-8b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3-8b-instruct-hf",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3p1-405b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3p1-405b-instruct-long",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3p1-70b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3p1-8b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/mixtral-8x22b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/mixtral-8x7b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/mixtral-8x7b-instruct-hf",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/mythomax-l2-13b",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/phi-3-vision-128k-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/phi-3p5-vision-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/starcoder-16b",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/yi-large",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-0125",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-0301",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-0613",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-1106",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-16k",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-16k-0613",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-0125-preview",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-0314",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-0613",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-1106-preview",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-32k",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-32k-0314",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-32k-0613",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-turbo",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-turbo-preview",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-vision-preview",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4o",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4o-mini",
"variables": "OPENAI_API_KEY"
}
]
},
"scraper_tool_model_name": {
"type": "string",
"default": "accounts/fireworks/models/firefunction-v2",
"environment": [
{
"value": "anthropic/claude-1.2",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-2.0",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-2.1",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-3-5-sonnet-20240620",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-3-haiku-20240307",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-3-opus-20240229",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-3-sonnet-20240229",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "anthropic/claude-instant-1.2",
"variables": "ANTHROPIC_API_KEY"
},
{
"value": "fireworks/gemma2-9b-it",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3-70b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3-70b-instruct-hf",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3-8b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3-8b-instruct-hf",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3p1-405b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3p1-405b-instruct-long",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3p1-70b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/llama-v3p1-8b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/mixtral-8x22b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/mixtral-8x7b-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/mixtral-8x7b-instruct-hf",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/mythomax-l2-13b",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/phi-3-vision-128k-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/phi-3p5-vision-instruct",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/starcoder-16b",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "fireworks/yi-large",
"variables": "FIREWORKS_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-0125",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-0301",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-0613",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-1106",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-16k",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-3.5-turbo-16k-0613",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-0125-preview",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-0314",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-0613",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-1106-preview",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-32k",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-32k-0314",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-32k-0613",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-turbo",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-turbo-preview",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4-vision-preview",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4o",
"variables": "OPENAI_API_KEY"
},
{
"value": "openai/gpt-4o-mini",
"variables": "OPENAI_API_KEY"
}
]
},
"max_search_results": {
"type": "integer",
"default": 10
}
}
"properties": {}
}
}
}
-->
-->

View File

@@ -1,7 +1,7 @@
{
"dependencies": ["."],
"graphs": {
"agent": "./src/react_agent/graph.py:graph"
"agent": "./src/agent/graph.py:graph"
},
"env": ".env"
}

View File

@@ -1,7 +1,7 @@
[project]
name = "react-agent"
name = "agent"
version = "0.0.1"
description = "Starter template for making a custom Reasoning and Action agent (using tool calling) in LangGraph."
description = "Starter template for making a new agent LangGraph."
authors = [
{ name = "William Fu-Hinthorn", email = "13333726+hinthornw@users.noreply.github.com" },
]
@@ -10,13 +10,7 @@ license = { text = "MIT" }
requires-python = ">=3.9"
dependencies = [
"langgraph>=0.2.6",
"langchain-openai>=0.1.22",
"langchain-anthropic>=0.1.23",
"langchain>=0.2.14",
"langchain-fireworks>=0.1.7",
"python-dotenv>=1.0.1",
"langchain-community>=0.2.17",
"tavily-python>=0.4.0",
]
@@ -28,10 +22,10 @@ requires = ["setuptools>=73.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["langgraph.templates.react_agent", "react_agent"]
packages = ["langgraph.templates.agent", "agent"]
[tool.setuptools.package-dir]
"langgraph.templates.react_agent" = "src/react_agent"
"react_agent" = "src/react_agent"
"langgraph.templates.agent" = "src/agent"
"agent" = "src/agent"
[tool.setuptools.package-data]

8
src/agent/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
"""New LangGraph Agent.
This module defines a custom graph.
"""
from agent.graph import graph
__all__ = ["graph"]

View File

@@ -0,0 +1,28 @@
"""Define the configurable parameters for the agent."""
from __future__ import annotations
from dataclasses import dataclass, fields
from typing import Optional
from langchain_core.runnables import RunnableConfig
@dataclass(kw_only=True)
class Configuration:
"""The configuration for the agent."""
# Changeme: Add configurable values here!
# these values can be pre-set when you
# create assistants (https://langchain-ai.github.io/langgraph/cloud/how-tos/configuration_cloud/)
# and when you invoke the graph
my_configurable_param: str = "changeme"
@classmethod
def from_runnable_config(
cls, config: Optional[RunnableConfig] = None
) -> Configuration:
"""Create a Configuration instance from a RunnableConfig object."""
configurable = (config.get("configurable") or {}) if config else {}
_fields = {f.name for f in fields(cls) if f.init}
return cls(**{k: v for k, v in configurable.items() if k in _fields})

38
src/agent/graph.py Normal file
View File

@@ -0,0 +1,38 @@
"""Define a simple chatbot agent.
This agent returns a predefined response without using an actual LLM.
"""
from typing import Any, Dict
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph
from agent.configuration import Configuration
from agent.state import State
async def my_node(state: State, config: RunnableConfig) -> Dict[str, Any]:
"""Each node does work."""
configuration = Configuration.from_runnable_config(config)
# configuration = Configuration.from_runnable_config(config)
# You can use runtime configuration to alter the behavior of your
# graph.
return {
"changeme": "output from my_node. "
f"Configured with {configuration.my_configurable_param}"
}
# Define a new graph
workflow = StateGraph(State, config_schema=Configuration)
# Add the node to the graph
workflow.add_node("my_node", my_node)
# Set the entrypoint as `call_model`
workflow.add_edge("__start__", "my_node")
# Compile the workflow into an executable graph
graph = workflow.compile()
graph.name = "New Graph" # This defines the custom name in LangSmith

17
src/agent/state.py Normal file
View File

@@ -0,0 +1,17 @@
"""Define the state structures for the agent."""
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class State:
"""Defines the input state for the agent, representing a narrower interface to the outside world.
This class is used to define the initial state and structure of incoming data.
See: https://langchain-ai.github.io/langgraph/concepts/low_level/#state
for more information.
"""
changeme: str = "example"

View File

@@ -1,9 +0,0 @@
"""React Agent.
This module defines a custom reasoning and action agent graph.
It invokes tools in a simple loop.
"""
from react_agent.graph import graph
__all__ = ["graph"]

View File

@@ -1,49 +0,0 @@
"""Define the configurable parameters for the agent."""
from __future__ import annotations
from dataclasses import dataclass, field, fields
from typing import Annotated, Optional
from langchain_core.runnables import RunnableConfig, ensure_config
from react_agent import prompts
@dataclass(kw_only=True)
class Configuration:
"""The configuration for the agent."""
system_prompt: str = field(default=prompts.SYSTEM_PROMPT)
"""The system prompt to use for the agent's interactions.
This prompt sets the context and behavior for the agent.
"""
model_name: Annotated[str, {"__template_metadata__": {"kind": "llm"}}] = (
"anthropic/claude-3-5-sonnet-20240620"
)
"""The name of the language model to use for the agent's main interactions.
Should be in the form: provider/model-name.
"""
scraper_tool_model_name: Annotated[
str, {"__template_metadata__": {"kind": "llm"}}
] = "accounts/fireworks/models/firefunction-v2"
"""The name of the language model to use for the web scraping tool.
This model is specifically used for summarizing and extracting information from web pages.
"""
max_search_results: int = 10
"""The maximum number of search results to return for each search query."""
@classmethod
def from_runnable_config(
cls, config: Optional[RunnableConfig] = None
) -> Configuration:
"""Create a Configuration instance from a RunnableConfig object."""
config = ensure_config(config)
configurable = config.get("configurable") or {}
_fields = {f.name for f in fields(cls) if f.init}
return cls(**{k: v for k, v in configurable.items() if k in _fields})

View File

@@ -1,127 +0,0 @@
"""Define a custom Reasoning and Action agent.
Works with a chat model with tool calling support.
"""
from datetime import datetime, timezone
from typing import Dict, List, Literal, cast
from langchain_core.messages import AIMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph
from langgraph.prebuilt import ToolNode
from react_agent.configuration import Configuration
from react_agent.state import InputState, State
from react_agent.tools import TOOLS
from react_agent.utils import load_chat_model
# Define the function that calls the model
async def call_model(
state: State, config: RunnableConfig
) -> Dict[str, List[AIMessage]]:
"""Call the LLM powering our "agent".
This function prepares the prompt, initializes the model, and processes the response.
Args:
state (State): The current state of the conversation.
config (RunnableConfig): Configuration for the model run.
Returns:
dict: A dictionary containing the model's response message.
"""
configuration = Configuration.from_runnable_config(config)
# Create a prompt template. Customize this to change the agent's behavior.
prompt = ChatPromptTemplate.from_messages(
[("system", configuration.system_prompt), ("placeholder", "{messages}")]
)
# Initialize the model with tool binding. Change the model or add more tools here.
model = load_chat_model(configuration.model_name).bind_tools(TOOLS)
# Prepare the input for the model, including the current system time
message_value = await prompt.ainvoke(
{
"messages": state.messages,
"system_time": datetime.now(tz=timezone.utc).isoformat(),
},
config,
)
# Get the model's response
response = cast(AIMessage, await model.ainvoke(message_value, config))
# Handle the case when it's the last step and the model still wants to use a tool
if state.is_last_step and response.tool_calls:
return {
"messages": [
AIMessage(
id=response.id,
content="Sorry, I could not find an answer to your question in the specified number of steps.",
)
]
}
# Return the model's response as a list to be added to existing messages
return {"messages": [response]}
# Define a new graph
workflow = StateGraph(State, input=InputState, config_schema=Configuration)
# Define the two nodes we will cycle between
workflow.add_node(call_model)
workflow.add_node("tools", ToolNode(TOOLS))
# Set the entrypoint as `call_model`
# This means that this node is the first one called
workflow.add_edge("__start__", "call_model")
def route_model_output(state: State) -> Literal["__end__", "tools"]:
"""Determine the next node based on the model's output.
This function checks if the model's last message contains tool calls.
Args:
state (State): The current state of the conversation.
Returns:
str: The name of the next node to call ("__end__" or "tools").
"""
last_message = state.messages[-1]
if not isinstance(last_message, AIMessage):
raise ValueError(
f"Expected AIMessage in output edges, but got {type(last_message).__name__}"
)
# If there is no tool call, then we finish
if not last_message.tool_calls:
return "__end__"
# Otherwise we execute the requested actions
return "tools"
# Add a conditional edge to determine the next step after `call_model`
workflow.add_conditional_edges(
"call_model",
# After call_model finishes running, the next node(s) are scheduled
# based on the output from route_model_output
route_model_output,
)
# Add a normal edge from `tools` to `call_model`
# This creates a cycle: after using tools, we always return to the model
workflow.add_edge("tools", "call_model")
# Compile the workflow into an executable graph
# You can customize this by adding interrupt points for state updates
graph = workflow.compile(
interrupt_before=[], # Add node names here to update state before they're called
interrupt_after=[], # Add node names here to update state after they're called
)

View File

@@ -1,5 +0,0 @@
"""Default prompts used by the agent."""
SYSTEM_PROMPT = """You are a helpful AI assistant.
System time: {system_time}"""

View File

@@ -1,60 +0,0 @@
"""Define the state structures for the agent."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Sequence
from langchain_core.messages import AnyMessage
from langgraph.graph import add_messages
from langgraph.managed import IsLastStep
from typing_extensions import Annotated
@dataclass
class InputState:
"""Defines the input state for the agent, representing a narrower interface to the outside world.
This class is used to define the initial state and structure of incoming data.
"""
messages: Annotated[Sequence[AnyMessage], add_messages] = field(
default_factory=list
)
"""
Messages tracking the primary execution state of the agent.
Typically accumulates a pattern of:
1. HumanMessage - user input
2. AIMessage with .tool_calls - agent picking tool(s) to use to collect information
3. ToolMessage(s) - the responses (or errors) from the executed tools
4. AIMessage without .tool_calls - agent responding in unstructured format to the user
5. HumanMessage - user responds with the next conversational turn
Steps 2-5 may repeat as needed.
The `add_messages` annotation ensures that new messages are merged with existing ones,
updating by ID to maintain an "append-only" state unless a message with the same ID is provided.
"""
@dataclass
class State(InputState):
"""Represents the complete state of the agent, extending InputState with additional attributes.
This class can be used to store any information needed throughout the agent's lifecycle.
"""
is_last_step: IsLastStep = field(default=False)
"""
Indicates whether the current step is the last one before the graph raises an error.
This is a 'managed' variable, controlled by the state machine rather than user code.
It is set to 'True' when the step count reaches recursion_limit - 1.
"""
# Additional attributes can be added here as needed.
# Common examples include:
# retrieved_documents: List[Document] = field(default_factory=list)
# extracted_entities: Dict[str, Any] = field(default_factory=dict)
# api_connections: Dict[str, Any] = field(default_factory=dict)

View File

@@ -1,74 +0,0 @@
"""This module provides example tools for web scraping and search functionality.
It includes:
- A web scraper that uses an LLM to summarize content based on instructions
- A basic Tavily search function
These tools are intended as free examples to get started. For production use,
consider implementing more robust and specialized tools tailored to your needs.
"""
from datetime import datetime, timezone
from typing import Any, Callable, List, Optional, cast
import httpx
from langchain.chat_models import init_chat_model
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.runnables import RunnableConfig
from langchain_core.tools import InjectedToolArg
from typing_extensions import Annotated
from react_agent.configuration import Configuration
from react_agent.utils import get_message_text
# note that arguments typed as "RunnableConfig" in tools will be excluded from the schema generated
# for the model.
# They are treated as "injected arguments"
async def scrape_webpage(url: str, instructions: str, *, config: RunnableConfig) -> str:
"""Scrape the given webpage and return a summary of text based on the instructions.
Args:
url: The URL of the webpage to scrape.
instructions: The instructions to give to the scraper. An LLM will be used to respond using the
instructions and the scraped text.
"""
async with httpx.AsyncClient() as client:
response = await client.get(url)
web_text = response.text
configuration = Configuration.from_runnable_config(config)
model = init_chat_model(configuration.model_name)
response_msg = await model.ainvoke(
[
(
"system",
"You are a helpful web scraper AI assistant. You are working in extractive Q&A mode, meaning you refrain from making overly abstractive responses."
"Respond to the user's instructions."
" Based on the provided webpage. If you are unable to answer the question, let the user know. Do not guess."
" Provide citations and direct quotes when possible."
f" \n\n<webpage_text>\n{web_text}\n</webpage_text>"
f"\n\nSystem time: {datetime.now(tz=timezone.utc)}",
),
("user", instructions),
]
)
return get_message_text(response_msg)
async def search(
query: str, *, config: Annotated[RunnableConfig, InjectedToolArg]
) -> Optional[list[dict[str, Any]]]:
"""Search for general web results.
This function performs a search using the Tavily search engine, which is designed
to provide comprehensive, accurate, and trusted results. It's particularly useful
for answering questions about current events.
"""
configuration = Configuration.from_runnable_config(config)
wrapped = TavilySearchResults(max_results=configuration.max_search_results)
result = await wrapped.ainvoke({"query": query})
return cast(list[dict[str, Any]], result)
TOOLS: List[Callable[..., Any]] = [scrape_webpage, search]

View File

@@ -1,27 +0,0 @@
"""Utility & helper functions."""
from langchain.chat_models import init_chat_model
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import BaseMessage
def get_message_text(msg: BaseMessage) -> str:
"""Get the text content of a message."""
content = msg.content
if isinstance(content, str):
return content
elif isinstance(content, dict):
return content.get("text", "")
else:
txts = [c if isinstance(c, str) else (c.get("text") or "") for c in content]
return "".join(txts).strip()
def load_chat_model(fully_specified_name: str) -> BaseChatModel:
"""Load a chat model from a fully specified name.
Args:
fully_specified_name (str): String in the format 'provider/model'.
"""
provider, model = fully_specified_name.split("/", maxsplit=1)
return init_chat_model(model, model_provider=provider)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,14 +1,11 @@
import pytest
from langsmith import unit
from react_agent import graph
from agent import graph
@pytest.mark.asyncio
@unit
async def test_react_agent_simple_passthrough() -> None:
res = await graph.ainvoke(
{"messages": [("user", "Who is the founder of LangChain?")]}
)
assert "harrison" in str(res["messages"][-1].content).lower()
async def test_agent_simple_passthrough() -> None:
res = await graph.ainvoke({"changeme": "some_val"})
assert res is not None

View File

@@ -1,5 +1,5 @@
from react_agent.configuration import Configuration
from agent.configuration import Configuration
def test_configuration_empty():
def test_configuration_empty() -> None:
Configuration.from_runnable_config({})