Compare commits

..
Author SHA1 Message Date
William Fu-Hinthorn 0f1f0836a4 feat: langgraph.json schema 2024-11-26 22:21:55 -08:00
10 changed files with 120 additions and 157 deletions
@@ -0,0 +1,104 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LangGraph Configuration Schema",
"description": "Schema for LangGraph configuration file (langgraph.json)",
"type": "object",
"oneOf": [
{
"type": "object",
"required": ["node_version", "graphs"],
"properties": {
"node_version": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "Node.js major version number (e.g. '20'). Must be >= 20."
},
"dockerfile_lines": {
"type": "array",
"items": {
"type": "string"
},
"description": "Additional lines to add to the Dockerfile"
},
"graphs": {
"type": "object",
"minProperties": 1,
"additionalProperties": {
"type": "string",
"pattern": "^[^:]+:[^:]+$",
"description": "Import string in format '<module>:<attribute>'"
},
"description": "Dictionary mapping graph IDs to import strings"
},
"env": {
"oneOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "Environment variables as object or path to .env file"
}
}
},
{
"type": "object",
"required": ["dependencies", "graphs"],
"properties": {
"python_version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+$",
"description": "Python version in 'major.minor' format (e.g. '3.11'). Must be >= 3.11."
},
"pip_config_file": {
"type": "string",
"description": "Path to pip configuration file"
},
"dockerfile_lines": {
"type": "array",
"items": {
"type": "string"
},
"description": "Additional lines to add to the Dockerfile"
},
"dependencies": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
},
"description": "List of dependencies (PyPI packages or local paths)"
},
"graphs": {
"type": "object",
"minProperties": 1,
"additionalProperties": {
"type": "string",
"pattern": "^[^:]+:[^:]+$",
"description": "Import string in format '<module>:<attribute>'"
},
"description": "Dictionary mapping graph IDs to import strings"
},
"env": {
"oneOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "string"
}
],
"description": "Environment variables as object or path to .env file"
}
}
}
]
}
@@ -212,7 +212,6 @@ def create_react_agent(
Args:
model: The `LangChain` chat model that supports tool calling.
tools: A list of tools, a ToolExecutor, or a ToolNode instance.
If an empty list is provided, the agent will consist of a single LLM node without tool calling.
state_schema: An optional state schema that defines graph state.
Must have `messages` and `is_last_step` keys.
Defaults to `AgentState` that defines those two keys.
@@ -541,11 +540,20 @@ def create_react_agent(
# get the tool functions wrapped in a tool class from the ToolNode
tool_classes = list(tool_node.tools_by_name.values())
tool_calling_enabled = len(tool_classes) > 0
if _should_bind_tools(model, tool_classes) and tool_calling_enabled:
if _should_bind_tools(model, tool_classes):
model = cast(BaseChatModel, model).bind_tools(tool_classes)
# Define the function that determines whether to continue or not
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
messages = state["messages"]
last_message = messages[-1]
# If there is no function call, then we finish
if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
return "__end__"
# Otherwise if there is, we continue
else:
return "tools"
# we're passing store here for validation
preprocessor = _get_model_preprocessing_runnable(
state_modifier, messages_modifier, store
@@ -627,30 +635,6 @@ def create_react_agent(
# We return a list, because this will get added to the existing list
return {"messages": [response]}
if not tool_calling_enabled:
# Define a new graph
workflow = StateGraph(state_schema or AgentState)
workflow.add_node("agent", RunnableCallable(call_model, acall_model))
workflow.set_entry_point("agent")
return workflow.compile(
checkpointer=checkpointer,
store=store,
interrupt_before=interrupt_before,
interrupt_after=interrupt_after,
debug=debug,
)
# Define the function that determines whether to continue or not
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
messages = state["messages"]
last_message = messages[-1]
# If there is no function call, then we finish
if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
return "__end__"
# Otherwise if there is, we continue
else:
return "tools"
# Define a new graph
workflow = StateGraph(state_schema or AgentState)
-8
View File
@@ -1,4 +1,3 @@
import sys
from collections import defaultdict, deque
from functools import partial
from hashlib import sha1
@@ -67,7 +66,6 @@ from langgraph.types import All, LoopProtocol, PregelExecutableTask, PregelTask
from langgraph.utils.config import merge_configs, patch_config
GetNextVersion = Callable[[Optional[V], BaseChannel], V]
SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
class WritesProtocol(Protocol):
@@ -636,12 +634,6 @@ def prepare_single_task(
)
except StopIteration:
return
except Exception as exc:
if SUPPORTS_EXC_NOTES:
exc.add_note(
f"Before task with name '{name}' and path '{task_path[:3]}'"
)
raise
# create task id
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
-6
View File
@@ -1,7 +1,6 @@
import asyncio
import logging
import random
import sys
import time
from dataclasses import replace
from functools import partial
@@ -19,7 +18,6 @@ from langgraph.types import Command, PregelExecutableTask, RetryPolicy
from langgraph.utils.config import patch_configurable
logger = logging.getLogger(__name__)
SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
def run_with_retry(
@@ -62,8 +60,6 @@ def run_with_retry(
# if interrupted, end
raise
except Exception as exc:
if SUPPORTS_EXC_NOTES:
exc.add_note(f"During task with name '{task.name}' and id '{task.id}'")
if retry_policy is None:
raise
# increment attempts
@@ -156,8 +152,6 @@ async def arun_with_retry(
# if interrupted, end
raise
except Exception as exc:
if SUPPORTS_EXC_NOTES:
exc.add_note(f"During task with name '{task.name}' and id '{task.id}'")
if retry_policy is None:
raise
# increment attempts
-3
View File
@@ -102,9 +102,6 @@ class FakeToolCallingModel(BaseChatModel):
tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
**kwargs: Any,
) -> Runnable[LanguageModelInput, BaseMessage]:
if len(tools) == 0:
raise ValueError("Must provide at least one tool")
tool_dicts = []
for tool in tools:
if not isinstance(tool, BaseTool):
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.29",
"version": "0.0.28",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
-1
View File
@@ -4,7 +4,6 @@ import PQueueMod from "p-queue";
const STATUS_NO_RETRY = [
400, // Bad Request
401, // Unauthorized
402, // Payment required
403, // Forbidden
404, // Not Found
405, // Method Not Allowed
+2 -3
View File
@@ -59,7 +59,6 @@ from langgraph_sdk.schema import (
ThreadStatus,
ThreadUpdateStateResponse,
)
from langgraph_sdk.sse import EventSource
logger = logging.getLogger(__name__)
@@ -293,7 +292,7 @@ class HttpClient:
else:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
async for event in EventSource(sse.response).aiter_sse():
async for event in sse.aiter_sse():
yield StreamPart(
event.event, orjson.loads(event.data) if event.data else None
)
@@ -2427,7 +2426,7 @@ class SyncHttpClient:
else:
logger.error(f"Error from langgraph-api: {body}", exc_info=e)
raise e
for event in EventSource(sse.response).iter_sse():
for event in sse.iter_sse():
yield StreamPart(
event.event, orjson.loads(event.data) if event.data else None
)
-106
View File
@@ -1,106 +0,0 @@
"""Adapted from httpx_sse to split lines on \n, \r, \r\n per the SSE spec."""
import io
from typing import AsyncIterator, Iterator
import httpx
import httpx_sse
import httpx_sse._decoders
class BytesLineDecoder:
"""
Handles incrementally reading lines from text.
Has the same behaviour as the stdllib bytes splitlines,
but handling the input iteratively.
"""
def __init__(self) -> None:
self.buffer = io.BytesIO()
self.trailing_cr: bool = False
def decode(self, text: bytes) -> list[bytes]:
# See https://docs.python.org/3/glossary.html#term-universal-newlines
NEWLINE_CHARS = b"\n\r"
# We always push a trailing `\r` into the next decode iteration.
if self.trailing_cr:
text = b"\r" + text
self.trailing_cr = False
if text.endswith(b"\r"):
self.trailing_cr = True
text = text[:-1]
if not text:
# NOTE: the edge case input of empty text doesn't occur in practice,
# because other httpx internals filter out this value
return [] # pragma: no cover
trailing_newline = text[-1] in NEWLINE_CHARS
lines = text.splitlines()
if len(lines) == 1 and not trailing_newline:
# No new lines, buffer the input and continue.
self.buffer.append(lines[0])
return []
if self.buffer:
# Include any existing buffer in the first portion of the
# splitlines result.
lines = [self.buffer.getvalue() + lines[0]] + lines[1:]
self.buffer.truncate(0)
if not trailing_newline:
# If the last segment of splitlines is not newline terminated,
# then drop it from our output and start a new buffer.
self.buffer.write(lines.pop())
return lines
def flush(self) -> list[bytes]:
if not self.buffer and not self.trailing_cr:
return []
lines = [self.buffer.getvalue()] if self.buffer else []
self.buffer.truncate(0)
self.trailing_cr = False
return lines
async def aiter_lines_raw(response: httpx.Response) -> AsyncIterator[bytes]:
decoder = BytesLineDecoder()
async for chunk in response.aiter_bytes():
for line in decoder.decode(chunk):
yield line
for line in decoder.flush():
yield line
def iter_lines_raw(response: httpx.Response) -> Iterator[bytes]:
decoder = BytesLineDecoder()
for chunk in response.iter_bytes():
for line in decoder.decode(chunk):
yield line
for line in decoder.flush():
yield line
class EventSource(httpx_sse.EventSource):
async def aiter_sse(self) -> AsyncIterator[httpx_sse.ServerSentEvent]:
self._check_content_type()
decoder = httpx_sse._decoders.SSEDecoder()
async for line in aiter_lines_raw(self._response):
line = line.rstrip(b"\n")
sse = decoder.decode(line.decode())
if sse is not None:
yield sse
def iter_sse(self) -> Iterator[httpx_sse.ServerSentEvent]:
self._check_content_type()
decoder = httpx_sse._decoders.SSEDecoder()
for line in iter_lines_raw(self._response):
line = line.rstrip(b"\n")
sse = decoder.decode(line.decode())
if sse is not None:
yield sse
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
version = "0.1.37"
version = "0.1.36"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"