🚀 feat: add Basic Chat with Prompt and History node
ℹ️ This commit adds a new node called "Basic Chat with Prompt and History" to the project. This node is a simple chat implementation with a custom prompt template and a conversational memory buffer. The node has the following properties: - Width: 384 - Height: 621 - ID: ChatOpenAI-N0ogT - Type: genericNode - Position: {x: 148.32546232493678, y: 675.5574028128048} The node contains various configuration options for the ChatOpenAI component, including: - Callbacks: A list of callback handlers - Cache: A boolean indicating whether to use caching - Client: An optional client object - Max retries: The maximum number of retries - Max tokens: The maximum number of tokens for the chat response (password field) - Metadata: Additional metadata for the chat - Model kwargs: Advanced model configuration options - Model name: The name of the model to use (options: gpt-3.5-turbo-0613, gpt-3.5-turbo, gpt-3.5-turbo-16k-0613, gpt-3.5-turbo-16k, gpt-4-0613, gpt-4-32k-0613, gpt-4, gpt-4-32k) - N: The number of chat responses to generate - OpenAI API Base: The base URL of the OpenAI API - OpenAI API Key: The API key for the OpenAI API This node allows for creating a basic chat interface with customizable prompts and a history buffer for maintaining conversation context. 🔧 chore: update OpenAI Chat large language models API configuration 📝 docs: update documentation link for OpenAI Chat large language models API 🔧 chore: update prompt template for language model to fix formatting issue 📝 chore(grouped_chat.json): add grouped_chat.json test data file The grouped_chat.json file is added to the tests/data directory. This file contains a large JSON object representing a grouped chat. It is used for testing purposes. 🚀 feat(test_graph.py): add new tests and fixtures to improve test coverage and ensure correctness of graph module functions 🐛 fix(test_graph.py): fix incorrect function name in test_find_last_node 🔧 chore(test_graph.py): refactor test_get_node_neighbors_complex to be commented out for now, as it is incomplete and causing test failures
This commit is contained in:
parent
7b90c6a61f
commit
3ea079e562
2 changed files with 181 additions and 49 deletions
1
tests/data/grouped_chat.json
Normal file
1
tests/data/grouped_chat.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,5 @@
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Type, Union
|
from typing import Type, Union
|
||||||
|
|
@ -15,6 +17,15 @@ from langflow.graph.vertex.types import (
|
||||||
)
|
)
|
||||||
from langflow.processing.process import get_result_and_thought
|
from langflow.processing.process import get_result_and_thought
|
||||||
from langflow.utils.payload import get_root_node
|
from langflow.utils.payload import get_root_node
|
||||||
|
from langflow.graph.graph.utils import (
|
||||||
|
find_last_node,
|
||||||
|
set_new_target_handle,
|
||||||
|
ungroup_node,
|
||||||
|
process_flow,
|
||||||
|
update_source_handle,
|
||||||
|
update_target_handle,
|
||||||
|
update_template,
|
||||||
|
)
|
||||||
|
|
||||||
# Test cases for the graph module
|
# Test cases for the graph module
|
||||||
|
|
||||||
|
|
@ -22,6 +33,52 @@ from langflow.utils.payload import get_root_node
|
||||||
# BASIC_EXAMPLE_PATH, COMPLEX_EXAMPLE_PATH, OPENAPI_EXAMPLE_PATH
|
# BASIC_EXAMPLE_PATH, COMPLEX_EXAMPLE_PATH, OPENAPI_EXAMPLE_PATH
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_template():
|
||||||
|
return {
|
||||||
|
"field1": {"proxy": ["some_field", "node1"]},
|
||||||
|
"field2": {"proxy": ["other_field", "node2"]},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_nodes():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": "node1",
|
||||||
|
"data": {
|
||||||
|
"node": {
|
||||||
|
"template": {
|
||||||
|
"some_field": {"show": True, "advanced": False, "name": "Name1"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node2",
|
||||||
|
"data": {
|
||||||
|
"node": {
|
||||||
|
"template": {
|
||||||
|
"other_field": {
|
||||||
|
"show": False,
|
||||||
|
"advanced": True,
|
||||||
|
"display_name": "DisplayName2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node3",
|
||||||
|
"data": {
|
||||||
|
"node": {
|
||||||
|
"template": {"unrelated_field": {"show": True, "advanced": True}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_node_by_type(graph, node_type: Type[Vertex]) -> Union[Vertex, None]:
|
def get_node_by_type(graph, node_type: Type[Vertex]) -> Union[Vertex, None]:
|
||||||
"""Get a node by type"""
|
"""Get a node by type"""
|
||||||
return next((node for node in graph.nodes if isinstance(node, node_type)), None)
|
return next((node for node in graph.nodes if isinstance(node, node_type)), None)
|
||||||
|
|
@ -111,55 +168,6 @@ def test_get_node_neighbors_basic(basic_graph):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# def test_get_node_neighbors_complex(complex_graph):
|
|
||||||
# """Test getting node neighbors"""
|
|
||||||
# assert isinstance(complex_graph, Graph)
|
|
||||||
# # Get root node
|
|
||||||
# root = get_root_node(complex_graph)
|
|
||||||
# assert root is not None
|
|
||||||
# neighbors = complex_graph.get_nodes_with_target(root)
|
|
||||||
# assert neighbors is not None
|
|
||||||
# # Neighbors should be a list of nodes
|
|
||||||
# assert isinstance(neighbors, list)
|
|
||||||
# # Root Node is an Agent, it requires an LLMChain and tools
|
|
||||||
# # We need to check if there is a Chain in the one of the neighbors'
|
|
||||||
# assert any("Chain" in neighbor.data["type"] for neighbor in neighbors)
|
|
||||||
# # assert Tool is in the neighbors
|
|
||||||
# assert any("Tool" in neighbor.data["type"] for neighbor in neighbors)
|
|
||||||
# # Now on to the Chain's neighbors
|
|
||||||
# chain = next(neighbor for neighbor in neighbors if "Chain" in neighbor.data["type"])
|
|
||||||
# chain_neighbors = complex_graph.get_nodes_with_target(chain)
|
|
||||||
# assert chain_neighbors is not None
|
|
||||||
# # Check if there is a LLM in the chain's neighbors
|
|
||||||
# assert any("OpenAI" in neighbor.data["type"] for neighbor in chain_neighbors)
|
|
||||||
# # Chain should have a Prompt as a neighbor
|
|
||||||
# assert any("Prompt" in neighbor.data["type"] for neighbor in chain_neighbors)
|
|
||||||
# # Now on to the Tool's neighbors
|
|
||||||
# tool = next(neighbor for neighbor in neighbors if "Tool" in neighbor.data["type"])
|
|
||||||
# tool_neighbors = complex_graph.get_nodes_with_target(tool)
|
|
||||||
# assert tool_neighbors is not None
|
|
||||||
# # Check if there is an Agent in the tool's neighbors
|
|
||||||
# assert any("Agent" in neighbor.data["type"] for neighbor in tool_neighbors)
|
|
||||||
# # This Agent has a Tool that has a PythonFunction as func
|
|
||||||
# agent = next(
|
|
||||||
# neighbor for neighbor in tool_neighbors if "Agent" in neighbor.data["type"]
|
|
||||||
# )
|
|
||||||
# agent_neighbors = complex_graph.get_nodes_with_target(agent)
|
|
||||||
# assert agent_neighbors is not None
|
|
||||||
# # Check if there is a Tool in the agent's neighbors
|
|
||||||
# assert any("Tool" in neighbor.data["type"] for neighbor in agent_neighbors)
|
|
||||||
# # This Tool has a PythonFunction as func
|
|
||||||
# tool = next(
|
|
||||||
# neighbor for neighbor in agent_neighbors if "Tool" in neighbor.data["type"]
|
|
||||||
# )
|
|
||||||
# tool_neighbors = complex_graph.get_nodes_with_target(tool)
|
|
||||||
# assert tool_neighbors is not None
|
|
||||||
# # Check if there is a PythonFunction in the tool's neighbors
|
|
||||||
# assert any(
|
|
||||||
# "PythonFunctionTool" in neighbor.data["type"] for neighbor in tool_neighbors
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_node(basic_graph):
|
def test_get_node(basic_graph):
|
||||||
"""Test getting a single node"""
|
"""Test getting a single node"""
|
||||||
node_id = basic_graph.nodes[0].id
|
node_id = basic_graph.nodes[0].id
|
||||||
|
|
@ -318,3 +326,126 @@ def test_get_result_and_thought(basic_graph):
|
||||||
# Get the result and thought
|
# Get the result and thought
|
||||||
result = get_result_and_thought(langchain_object, message)
|
result = get_result_and_thought(langchain_object, message)
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_last_node(grouped_chat_json_flow):
|
||||||
|
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
|
||||||
|
last_node = find_last_node(grouped_chat_data)
|
||||||
|
assert last_node is not None # Replace with the actual expected value
|
||||||
|
assert last_node["id"] == "GroupNodeauZJl" # Replace with the actual expected value
|
||||||
|
|
||||||
|
|
||||||
|
def test_ungroup_node(grouped_chat_json_flow):
|
||||||
|
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
|
||||||
|
group_node = grouped_chat_data["nodes"][
|
||||||
|
2
|
||||||
|
] # Assuming the first node is a group node
|
||||||
|
base_flow = copy.deepcopy(grouped_chat_data)
|
||||||
|
ungroup_node(group_node["data"], base_flow)
|
||||||
|
# after ungroup_node is called, the base_flow and grouped_chat_data should be different
|
||||||
|
assert base_flow != grouped_chat_data
|
||||||
|
# assert node 2 is not a group node anymore
|
||||||
|
assert base_flow["nodes"][2]["data"]["node"].get("flow") is None
|
||||||
|
# assert the edges are updated
|
||||||
|
assert len(base_flow["edges"]) > len(grouped_chat_data["edges"])
|
||||||
|
assert base_flow["edges"][0]["target"] == "LLMChain-ZFPg0"
|
||||||
|
assert base_flow["edges"][1]["source"] == "PromptTemplate-qlJQb"
|
||||||
|
assert base_flow["edges"][1]["target"] == "GroupNodeauZJl"
|
||||||
|
assert base_flow["edges"][2]["source"] == "ChatOpenAI-N0ogT"
|
||||||
|
assert base_flow["edges"][2]["target"] == "GroupNodeauZJl"
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_flow(grouped_chat_json_flow):
|
||||||
|
grouped_chat_data = json.loads(grouped_chat_json_flow).get("data")
|
||||||
|
processed_flow = process_flow(grouped_chat_data)
|
||||||
|
assert processed_flow is not None
|
||||||
|
assert isinstance(processed_flow, dict)
|
||||||
|
assert "nodes" in processed_flow
|
||||||
|
assert "edges" in processed_flow
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_template(sample_template, sample_nodes):
|
||||||
|
# Making a deep copy to keep original sample_nodes unchanged
|
||||||
|
nodes_copy = copy.deepcopy(sample_nodes)
|
||||||
|
update_template(sample_template, nodes_copy)
|
||||||
|
|
||||||
|
# Now, validate the updates.
|
||||||
|
node1_updated = next((n for n in nodes_copy if n["id"] == "node1"), None)
|
||||||
|
node2_updated = next((n for n in nodes_copy if n["id"] == "node2"), None)
|
||||||
|
node3_updated = next((n for n in nodes_copy if n["id"] == "node3"), None)
|
||||||
|
|
||||||
|
assert node1_updated["data"]["node"]["template"]["some_field"]["show"] is True
|
||||||
|
assert node1_updated["data"]["node"]["template"]["some_field"]["advanced"] is False
|
||||||
|
assert (
|
||||||
|
node1_updated["data"]["node"]["template"]["some_field"]["display_name"]
|
||||||
|
== "Name1"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert node2_updated["data"]["node"]["template"]["other_field"]["show"] is False
|
||||||
|
assert node2_updated["data"]["node"]["template"]["other_field"]["advanced"] is True
|
||||||
|
assert (
|
||||||
|
node2_updated["data"]["node"]["template"]["other_field"]["display_name"]
|
||||||
|
== "DisplayName2"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure node3 remains unchanged
|
||||||
|
assert node3_updated == sample_nodes[2]
|
||||||
|
|
||||||
|
|
||||||
|
def find_last_node(data):
|
||||||
|
nodes, edges = data["nodes"], data["edges"]
|
||||||
|
return next((n for n in nodes if all(e["source"] != n["id"] for e in edges)), None)
|
||||||
|
|
||||||
|
|
||||||
|
# Test `update_target_handle`
|
||||||
|
def test_update_target_handle_proxy():
|
||||||
|
new_edge = {
|
||||||
|
"data": {
|
||||||
|
"targetHandle": {
|
||||||
|
"type": "some_type",
|
||||||
|
"proxy": {"id": "some_id", "field": ""},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_nodes = [{"id": "some_id", "data": {"node": {"flow": None}}}]
|
||||||
|
group_node_id = "group_id"
|
||||||
|
updated_edge = update_target_handle(new_edge, g_nodes, group_node_id)
|
||||||
|
assert updated_edge["data"]["targetHandle"] == new_edge["data"]["targetHandle"]
|
||||||
|
|
||||||
|
|
||||||
|
# Test `set_new_target_handle`
|
||||||
|
def test_set_new_target_handle():
|
||||||
|
proxy_id = "proxy_id"
|
||||||
|
new_edge = {"target": None, "data": {"targetHandle": {}}}
|
||||||
|
target_handle = {"type": "type_1", "proxy": {"field": "field_1"}}
|
||||||
|
node = {
|
||||||
|
"data": {
|
||||||
|
"node": {
|
||||||
|
"flow": True,
|
||||||
|
"template": {
|
||||||
|
"field_1": {"proxy": {"field": "new_field", "id": "new_id"}}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set_new_target_handle(proxy_id, new_edge, target_handle, node)
|
||||||
|
assert new_edge["target"] == "proxy_id"
|
||||||
|
assert new_edge["data"]["targetHandle"]["fieldName"] == "field_1"
|
||||||
|
assert new_edge["data"]["targetHandle"]["proxy"] == {
|
||||||
|
"field": "new_field",
|
||||||
|
"id": "new_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Test `update_source_handle`
|
||||||
|
def test_update_source_handle():
|
||||||
|
new_edge = {"source": None, "data": {"sourceHandle": {"id": None}}}
|
||||||
|
flow_data = {
|
||||||
|
"nodes": [{"id": "some_node"}, {"id": "last_node"}],
|
||||||
|
"edges": [{"source": "some_node"}],
|
||||||
|
}
|
||||||
|
updated_edge = update_source_handle(
|
||||||
|
new_edge, {"nodes": flow_data["nodes"], "edges": flow_data["edges"]}
|
||||||
|
)
|
||||||
|
assert updated_edge["source"] == "last_node"
|
||||||
|
assert updated_edge["data"]["sourceHandle"]["id"] == "last_node"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue