diff --git a/.python-version b/.python-version index c8cfe39..2c07333 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.10 +3.11 diff --git a/pyproject.toml b/pyproject.toml index dc31d9c..a3de349 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.0.110" +version = "0.0.111" description = "UiPath Langchain" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10" @@ -16,6 +16,7 @@ dependencies = [ "python-dotenv>=1.0.1", "httpx>=0.27.0", "openai>=1.65.5", + "pytest-asyncio>=1.0.0", ] classifiers = [ "Development Status :: 3 - Alpha", diff --git a/tests/cli_run/samples/1-simple-graph/langgraph.json b/tests/cli_run/samples/1-simple-graph/langgraph.json new file mode 100644 index 0000000..171f11f --- /dev/null +++ b/tests/cli_run/samples/1-simple-graph/langgraph.json @@ -0,0 +1,9 @@ +{ + "dependencies": [ + "." + ], + "graphs": { + "agent": "./main.py:graph" + }, + "env": ".env" +} \ No newline at end of file diff --git a/tests/cli_run/samples/1-simple-graph/main.py b/tests/cli_run/samples/1-simple-graph/main.py new file mode 100644 index 0000000..e48e7a6 --- /dev/null +++ b/tests/cli_run/samples/1-simple-graph/main.py @@ -0,0 +1,52 @@ +import random +from typing import Literal + +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import END, START, StateGraph +from langgraph.types import interrupt +from typing_extensions import TypedDict +from uipath.models import CreateAction + + +# State +class State(TypedDict): + graph_state: str + + +# Nodes +def node_1(state): + print("---Node 1---") + simple_interrupt = interrupt("question: Who are you?") + + return {"graph_state": "Hello, I am " + simple_interrupt["answer"] + "!"} + + +def node_2(state): + print("---Node 2---") + action_interrupt = interrupt( + CreateAction( + app_name="Test-app", title="Test-title", description="Test-description" + ) + ) + return {"graph_state": state["graph_state"] + action_interrupt["ActionData"]} + + +def node_3(state): + print("---Node 3---") + return {"graph_state": state["graph_state"] + " end"} + + +builder = StateGraph(State) +builder.add_node("node_1", node_1) +builder.add_node("node_2", node_2) +builder.add_node("node_3", node_3) + +builder.add_edge(START, "node_1") +builder.add_edge("node_1", "node_2") +builder.add_edge("node_2", "node_3") +builder.add_edge("node_3", END) + + +memory = MemorySaver() + +graph = builder.compile(checkpointer=memory) diff --git a/tests/cli_run/samples/1-simple-graph/pyproject.toml b/tests/cli_run/samples/1-simple-graph/pyproject.toml new file mode 100644 index 0000000..e0fac4f --- /dev/null +++ b/tests/cli_run/samples/1-simple-graph/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "c-host-in-uipath" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [{ name = "Eduard Stanculet", email = "eduard.stanculet@uipath.com" }] +requires-python = ">=3.13" +dependencies = [ + "langchain-anthropic>=0.3.10", + "langchain-community>=0.3.21", + "langgraph>=0.3.29", + "tavily-python>=0.5.4", + "uipath>=2.0.8", + "uipath-langchain>=0.0.88", +] diff --git a/tests/cli_run/samples/1-simple-graph/uipath.json b/tests/cli_run/samples/1-simple-graph/uipath.json new file mode 100644 index 0000000..e3962bc --- /dev/null +++ b/tests/cli_run/samples/1-simple-graph/uipath.json @@ -0,0 +1,18 @@ +{ + "entryPoints": [ + { + "filePath": "agent", + "uniqueId": "dcc7a309-fbcc-4999-af4f-2a75a844b49a", + "type": "agent", + "input": { + "type": "string", + "title": "graph_state" + }, + "output": {} + } + ], + "bindings": { + "version": "2.0", + "resources": [] + } +} \ No newline at end of file diff --git a/tests/cli_run/test_run_sample.py b/tests/cli_run/test_run_sample.py new file mode 100644 index 0000000..d0322f8 --- /dev/null +++ b/tests/cli_run/test_run_sample.py @@ -0,0 +1,103 @@ +import os +import sys +import uuid +from unittest.mock import MagicMock, patch + +import pytest +from dotenv import load_dotenv +from uipath._cli._runtime._contracts import UiPathTraceContext + +from uipath_langchain._cli._runtime._context import LangGraphRuntimeContext +from uipath_langchain._cli._runtime._runtime import LangGraphRuntime +from uipath_langchain._cli._utils._graph import LangGraphConfig + +load_dotenv() + + +def _create_test_runtime_context(config: LangGraphConfig) -> LangGraphRuntimeContext: + """Helper function to create and configure LangGraphRuntimeContext for tests.""" + context = LangGraphRuntimeContext.from_config( + os.environ.get("UIPATH_CONFIG_PATH", "uipath.json") + ) + + context.entrypoint = ( + None # Or a specific graph name if needed, None will pick the single one + ) + context.input = '{ "graph_state": "GET Assets API does not enforce proper permissions Assets.View" }' + context.resume = False + context.langgraph_config = config + context.logs_min_level = os.environ.get("LOG_LEVEL", "INFO") + context.job_id = str(uuid.uuid4()) + context.trace_id = str(uuid.uuid4()) + # Convert string "True" or "False" to boolean for tracing_enabled + tracing_enabled_str = os.environ.get("UIPATH_TRACING_ENABLED", "True") + context.tracing_enabled = tracing_enabled_str.lower() == "true" + context.trace_context = UiPathTraceContext( + enabled=context.tracing_enabled, + trace_id=str( + uuid.uuid4() + ), # Consider passing trace_id if it needs to match context.trace_id + parent_span_id=os.environ.get("UIPATH_PARENT_SPAN_ID"), + root_span_id=os.environ.get("UIPATH_ROOT_SPAN_ID"), + job_id=os.environ.get( + "UIPATH_JOB_KEY" + ), # Consider passing job_id if it needs to match context.job_id + org_id=os.environ.get("UIPATH_ORGANIZATION_ID"), + tenant_id=os.environ.get("UIPATH_TENANT_ID"), + process_key=os.environ.get("UIPATH_PROCESS_UUID"), + folder_key=os.environ.get("UIPATH_FOLDER_KEY"), + ) + # Convert string "True" or "False" to boolean + langsmith_tracing_enabled_str = os.environ.get("LANGSMITH_TRACING", "False") + context.langsmith_tracing_enabled = langsmith_tracing_enabled_str.lower() == "true" + return context + + +@pytest.mark.asyncio +async def test_langgraph_runtime(): + test_folder_path = os.path.dirname(os.path.abspath(__file__)) + sample_path = os.path.join(test_folder_path, "samples", "1-simple-graph") + + sys.path.append(sample_path) + os.chdir(sample_path) + + config = LangGraphConfig() + if not config.exists: + raise AssertionError("langgraph.json not found in sample path") + + context = _create_test_runtime_context(config) + + # Mocking UiPath SDK for action creation + with patch("uipath_langchain._cli._runtime._output.UiPath") as MockUiPathClass: + mock_uipath_sdk_instance = MagicMock() + MockUiPathClass.return_value = mock_uipath_sdk_instance + mock_actions_client = MagicMock() + mock_uipath_sdk_instance.actions = mock_actions_client + + mock_created_action = MagicMock() + mock_created_action.key = "mock_action_key_from_test" + mock_actions_client.create.return_value = mock_created_action + + result = None + async with LangGraphRuntime.from_context(context) as runtime: + result = await runtime.execute() + print("Result:", result) + + context.resume = True + context.input = '{ "answer": "John Doe"}' # Simulate some resume data + async with LangGraphRuntime.from_context(context) as runtime: + result = await runtime.execute() + print("Result:", result) + + context.resume = True + context.input = ( + '{ "ActionData": "Test-ActionData" }' # Simulate some resume data + ) + async with LangGraphRuntime.from_context(context) as runtime: + result = await runtime.execute() + print("Result:", result) + + assert result is not None, "Result should not be None after execution" + assert ( + result.output["graph_state"] == "Hello, I am John Doe!Test-ActionData end" + ) diff --git a/uv.lock b/uv.lock index 898f757..557e77a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12.4'", @@ -2024,6 +2025,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] +[[package]] +name = "pytest-asyncio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976 }, +] + [[package]] name = "pytest-cov" version = "6.1.1" @@ -2478,6 +2491,7 @@ dependencies = [ { name = "langgraph-checkpoint-sqlite" }, { name = "openai" }, { name = "pydantic-settings" }, + { name = "pytest-asyncio" }, { name = "python-dotenv" }, { name = "uipath" }, ] @@ -2504,10 +2518,12 @@ requires-dist = [ { name = "langgraph-checkpoint-sqlite", specifier = ">=2.0.3" }, { name = "openai", specifier = ">=1.65.5" }, { name = "pydantic-settings", specifier = ">=2.6.0" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "uipath", specifier = ">=2.0.55,<2.1.0" }, { name = "uipath-langchain", marker = "extra == 'langchain'", specifier = ">=0.0.2" }, ] +provides-extras = ["langchain"] [package.metadata.requires-dev] dev = [