Skip to content

Commit ef8ac49

Browse files
committed
feat(tests): add small integration test
1 parent 90f021a commit ef8ac49

File tree

9 files changed

+327
-1
lines changed

9 files changed

+327
-1
lines changed

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.10
1+
3.11

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies = [
1616
"python-dotenv>=1.0.1",
1717
"httpx>=0.27.0",
1818
"openai>=1.65.5",
19+
"pytest-asyncio>=1.0.0",
1920
]
2021
classifiers = [
2122
"Development Status :: 3 - Alpha",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"dependencies": [
3+
"."
4+
],
5+
"graphs": {
6+
"agent": "./main.py:graph"
7+
},
8+
"env": ".env"
9+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import random
2+
from typing import Literal
3+
4+
from langgraph.checkpoint.memory import MemorySaver
5+
from langgraph.graph import END, START, StateGraph
6+
from langgraph.types import interrupt
7+
from typing_extensions import TypedDict
8+
from uipath.models import CreateAction
9+
10+
11+
# State
12+
class State(TypedDict):
13+
graph_state: str
14+
15+
16+
# Conditional edge
17+
def decide_mood(state) -> Literal["node_2", "node_3"]:
18+
# Often, we will use state to decide on the next node to visit
19+
user_input = state["graph_state"]
20+
21+
# x = interrupt("Xyz")
22+
x = interrupt(CreateAction(title="Xyz", description="Xyz description"))
23+
24+
# Here, let's just do a 50 / 50 split between nodes 2, 3
25+
if random.random() < 0.5:
26+
# 50% of the time, we return Node 2
27+
return "node_2"
28+
29+
# 50% of the time, we return Node 3
30+
return "node_3"
31+
32+
33+
# Nodes
34+
def node_1(state):
35+
print("---Node 1---")
36+
simple_interrupt = interrupt("question: Who are you?")
37+
38+
return {"graph_state": "Hello, I am " + simple_interrupt["answer"] + "!"}
39+
40+
41+
def node_2(state):
42+
print("---Node 2---")
43+
action_interrupt = interrupt(
44+
CreateAction(
45+
app_name="Test-app", title="Test-title", description="Test-description"
46+
)
47+
)
48+
return {"graph_state": state["graph_state"] + action_interrupt["ActionData"]}
49+
50+
51+
def node_3(state):
52+
print("---Node 3---")
53+
return {"graph_state": state["graph_state"] + " end"}
54+
55+
56+
builder = StateGraph(State)
57+
builder.add_node("node_1", node_1)
58+
builder.add_node("node_2", node_2)
59+
builder.add_node("node_3", node_3)
60+
61+
builder.add_edge(START, "node_1")
62+
builder.add_edge("node_1", "node_2")
63+
builder.add_edge("node_2", "node_3")
64+
builder.add_edge("node_3", END)
65+
66+
67+
memory = MemorySaver()
68+
69+
graph = builder.compile(checkpointer=memory)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[project]
2+
name = "c-host-in-uipath"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
authors = [{ name = "Eduard Stanculet", email = "[email protected]" }]
7+
requires-python = ">=3.13"
8+
dependencies = [
9+
"langchain-anthropic>=0.3.10",
10+
"langchain-community>=0.3.21",
11+
"langgraph>=0.3.29",
12+
"tavily-python>=0.5.4",
13+
"uipath>=2.0.8",
14+
"uipath-langchain>=0.0.88",
15+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"entryPoints": [
3+
{
4+
"filePath": "agent",
5+
"uniqueId": "dcc7a309-fbcc-4999-af4f-2a75a844b49a",
6+
"type": "agent",
7+
"input": {
8+
"type": "string",
9+
"title": "graph_state"
10+
},
11+
"output": {}
12+
}
13+
],
14+
"bindings": {
15+
"version": "2.0",
16+
"resources": []
17+
}
18+
}

tests/cli_run/test_run_sample.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import os
2+
import sys
3+
import uuid
4+
from unittest.mock import MagicMock, patch
5+
6+
import pytest
7+
from dotenv import load_dotenv
8+
from uipath._cli._runtime._contracts import UiPathTraceContext
9+
10+
from uipath_langchain._cli._runtime._context import LangGraphRuntimeContext
11+
from uipath_langchain._cli._runtime._runtime import LangGraphRuntime
12+
from uipath_langchain._cli._utils._graph import LangGraphConfig
13+
14+
load_dotenv()
15+
16+
17+
def _create_test_runtime_context(config: LangGraphConfig) -> LangGraphRuntimeContext:
18+
"""Helper function to create and configure LangGraphRuntimeContext for tests."""
19+
context = LangGraphRuntimeContext.from_config(
20+
os.environ.get("UIPATH_CONFIG_PATH", "uipath.json")
21+
)
22+
23+
context.entrypoint = (
24+
None # Or a specific graph name if needed, None will pick the single one
25+
)
26+
context.input = '{ "graph_state": "GET Assets API does not enforce proper permissions Assets.View" }'
27+
context.resume = False
28+
context.langgraph_config = config
29+
context.logs_min_level = os.environ.get("LOG_LEVEL", "INFO")
30+
context.job_id = str(uuid.uuid4())
31+
context.trace_id = str(uuid.uuid4())
32+
# Convert string "True" or "False" to boolean for tracing_enabled
33+
tracing_enabled_str = os.environ.get("UIPATH_TRACING_ENABLED", "True")
34+
context.tracing_enabled = tracing_enabled_str.lower() == "true"
35+
context.trace_context = UiPathTraceContext(
36+
enabled=context.tracing_enabled,
37+
trace_id=str(
38+
uuid.uuid4()
39+
), # Consider passing trace_id if it needs to match context.trace_id
40+
parent_span_id=os.environ.get("UIPATH_PARENT_SPAN_ID"),
41+
root_span_id=os.environ.get("UIPATH_ROOT_SPAN_ID"),
42+
job_id=os.environ.get(
43+
"UIPATH_JOB_KEY"
44+
), # Consider passing job_id if it needs to match context.job_id
45+
org_id=os.environ.get("UIPATH_ORGANIZATION_ID"),
46+
tenant_id=os.environ.get("UIPATH_TENANT_ID"),
47+
process_key=os.environ.get("UIPATH_PROCESS_UUID"),
48+
folder_key=os.environ.get("UIPATH_FOLDER_KEY"),
49+
)
50+
# Convert string "True" or "False" to boolean
51+
langsmith_tracing_enabled_str = os.environ.get("LANGSMITH_TRACING", "False")
52+
context.langsmith_tracing_enabled = langsmith_tracing_enabled_str.lower() == "true"
53+
return context
54+
55+
56+
@pytest.mark.asyncio
57+
async def test_langgraph_runtime():
58+
test_folder_path = os.path.dirname(os.path.abspath(__file__))
59+
sample_path = os.path.join(test_folder_path, "samples", "1-simple-graph")
60+
61+
sys.path.append(sample_path)
62+
os.chdir(sample_path)
63+
64+
config = LangGraphConfig()
65+
if not config.exists:
66+
raise AssertionError("langgraph.json not found in sample path")
67+
68+
context = _create_test_runtime_context(config)
69+
70+
# Mocking UiPath SDK for action creation
71+
with patch("uipath_langchain._cli._runtime._output.UiPath") as MockUiPathClass:
72+
mock_uipath_sdk_instance = MagicMock()
73+
MockUiPathClass.return_value = mock_uipath_sdk_instance
74+
mock_actions_client = MagicMock()
75+
mock_uipath_sdk_instance.actions = mock_actions_client
76+
77+
mock_created_action = MagicMock()
78+
mock_created_action.key = "mock_action_key_from_test"
79+
mock_actions_client.create.return_value = mock_created_action
80+
81+
result = None
82+
async with LangGraphRuntime.from_context(context) as runtime:
83+
result = await runtime.execute()
84+
print("Result:", result)
85+
86+
context.resume = True
87+
context.input = '{ "answer": "John Doe"}' # Simulate some resume data
88+
async with LangGraphRuntime.from_context(context) as runtime:
89+
result = await runtime.execute()
90+
print("Result:", result)
91+
92+
context.resume = True
93+
context.input = (
94+
'{ "ActionData": "Test-ActionData" }' # Simulate some resume data
95+
)
96+
async with LangGraphRuntime.from_context(context) as runtime:
97+
result = await runtime.execute()
98+
print("Result:", result)
99+
100+
assert result is not None, "Result should not be None after execution"
101+
assert (
102+
result.output["graph_state"] == "Hello, I am John Doe!Test-ActionData end"
103+
)

tests/conftest.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# import logging
2+
# import os
3+
# from os import environ as env
4+
# from typing import Generator, Optional
5+
6+
# import httpx
7+
# import pytest
8+
# from langchain.embeddings import CacheBackedEmbeddings
9+
# from langchain.globals import set_llm_cache
10+
# from langchain.storage import LocalFileStore
11+
# from langchain_community.cache import SQLiteCache
12+
13+
# from uipath_langchain.embeddings import UiPathOpenAIEmbeddings
14+
# from uipath_langchain.utils._settings import UiPathCachedPathsSettings
15+
16+
# test_cache_settings = UiPathCachedPathsSettings(
17+
# CACHED_COMPLETION_DB="tests/llm_cache/tests_uipath_cache.sqlite",
18+
# CACHED_EMBEDDINGS_DIR="tests/llm_cache/cached_embeddings",
19+
# )
20+
21+
22+
# def get_from_uipath_url():
23+
# try:
24+
# url = os.getenv("UIPATH_URL", "https://cloud.uipath.com/dummyOrg/dummyTennant/")
25+
# if url:
26+
# return "/".join(url.split("/", 3)[:3])
27+
# except Exception:
28+
# return "https://cloud.uipath.com/dummyOrg/dummyTennant/"
29+
# return None
30+
31+
32+
# def get_token():
33+
# url_get_token = f"{get_from_uipath_url().rstrip('/')}/identity_/connect/token"
34+
35+
# os.environ["UIPATH_REQUESTING_PRODUCT"] = "uipath-python-sdk"
36+
# os.environ["UIPATH_REQUESTING_FEATURE"] = "langgraph-agent"
37+
# os.environ["UIPATH_TESTS_CACHE_LLMGW"] = "true"
38+
39+
# token_credentials = {
40+
# "client_id": env.get("UIPATH_CLIENT_ID"),
41+
# "client_secret": env.get("UIPATH_CLIENT_SECRET"),
42+
# "grant_type": "client_credentials",
43+
# }
44+
45+
# try:
46+
# with httpx.Client() as client:
47+
# response = client.post(url_get_token, data=token_credentials)
48+
# response.raise_for_status()
49+
# res_json = response.json()
50+
# token = res_json.get("access_token")
51+
52+
# if not token:
53+
# pytest.skip("Authentication token is empty or missing")
54+
# except (httpx.HTTPError, ValueError, KeyError) as e:
55+
# pytest.skip(f"Failed to obtain authentication token: {str(e)}")
56+
57+
# return token
58+
59+
60+
# @pytest.fixture(autouse=True)
61+
# def setup_test_env():
62+
# env["UIPATH_ACCESS_TOKEN"] = get_token()
63+
64+
65+
# @pytest.fixture(scope="session")
66+
# def cached_llmgw_calls() -> Generator[Optional[SQLiteCache], None, None]:
67+
# if not os.environ.get("UIPATH_TESTS_CACHE_LLMGW"):
68+
# yield None
69+
# else:
70+
# logging.info("Setting up LLMGW cache")
71+
# db_path = test_cache_settings.cached_completion_db
72+
# os.makedirs(os.path.dirname(db_path), exist_ok=True)
73+
# cache = SQLiteCache(database_path=db_path)
74+
# set_llm_cache(cache)
75+
# yield cache
76+
# set_llm_cache(None)
77+
# return
78+
79+
80+
# @pytest.fixture(scope="session")
81+
# def cached_embedder() -> Generator[Optional[CacheBackedEmbeddings], None, None]:
82+
# if not os.environ.get("UIPATH_TESTS_CACHE_LLMGW"):
83+
# yield None
84+
# else:
85+
# logging.info("Setting up embeddings cache")
86+
# model = "text-embedding-3-large"
87+
# embedder = CacheBackedEmbeddings.from_bytes_store(
88+
# underlying_embeddings=UiPathOpenAIEmbeddings(model=model),
89+
# document_embedding_cache=LocalFileStore(
90+
# test_cache_settings.cached_embeddings_dir
91+
# ),
92+
# namespace=model,
93+
# )
94+
# yield embedder
95+
# return

uv.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)