Skip to content

Commit 4cfe566

Browse files
committed
fix: handle input output schema circular dependencies
1 parent aefbe61 commit 4cfe566

6 files changed

Lines changed: 483 additions & 144 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.0.145"
3+
version = "0.0.146"
44
description = "UiPath Langchain"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.10"

src/uipath_langchain/_cli/_runtime/_runtime.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,14 +490,14 @@ async def get_entrypoint(self) -> Entrypoint:
490490
"""Get entrypoint for this LangGraph runtime."""
491491
graph = await self.resolver()
492492
compiled_graph = graph.compile()
493-
schema = generate_schema_from_graph(compiled_graph)
493+
schema_details = generate_schema_from_graph(compiled_graph)
494494

495495
return Entrypoint(
496496
file_path=self.context.entrypoint, # type: ignore[call-arg]
497497
unique_id=str(uuid4()),
498498
type="agent",
499-
input=schema["input"],
500-
output=schema["output"],
499+
input=schema_details.schema["input"],
500+
output=schema_details.schema["output"],
501501
)
502502

503503
async def cleanup(self) -> None:

src/uipath_langchain/_cli/_utils/_schema.py

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,73 @@
1+
from dataclasses import dataclass
12
from typing import Any, Dict
23

34
from langgraph.graph.state import CompiledStateGraph
45

56

6-
def resolve_refs(schema, root=None):
7-
"""Recursively resolves $ref references in a JSON schema."""
7+
@dataclass
8+
class SchemaDetails:
9+
schema: dict[str, Any]
10+
has_input_circular_dependency: bool
11+
has_output_circular_dependency: bool
12+
13+
14+
def resolve_refs(schema, root=None, visited=None):
15+
"""Recursively resolves $ref references in a JSON schema, handling circular references.
16+
17+
Returns:
18+
tuple: (resolved_schema, has_circular_dependency)
19+
"""
820
if root is None:
9-
root = schema # Store the root schema to resolve $refs
21+
root = schema
22+
23+
if visited is None:
24+
visited = set()
25+
26+
has_circular = False
1027

1128
if isinstance(schema, dict):
1229
if "$ref" in schema:
13-
ref_path = schema["$ref"].lstrip("#/").split("/")
30+
ref_path = schema["$ref"]
31+
32+
if ref_path in visited:
33+
# Circular dependency detected
34+
return {
35+
"type": "object",
36+
"description": f"Circular reference to {ref_path}",
37+
}, True
38+
39+
visited.add(ref_path)
40+
41+
# Resolve the reference
42+
ref_parts = ref_path.lstrip("#/").split("/")
1443
ref_schema = root
15-
for part in ref_path:
44+
for part in ref_parts:
1645
ref_schema = ref_schema.get(part, {})
17-
return resolve_refs(ref_schema, root)
1846

19-
return {k: resolve_refs(v, root) for k, v in schema.items()}
47+
result, circular = resolve_refs(ref_schema, root, visited)
48+
has_circular = has_circular or circular
49+
50+
# Remove from visited after resolution (allows the same ref in different branches)
51+
visited.discard(ref_path)
52+
53+
return result, has_circular
54+
55+
resolved_dict = {}
56+
for k, v in schema.items():
57+
resolved_value, circular = resolve_refs(v, root, visited)
58+
resolved_dict[k] = resolved_value
59+
has_circular = has_circular or circular
60+
return resolved_dict, has_circular
2061

2162
elif isinstance(schema, list):
22-
return [resolve_refs(item, root) for item in schema]
63+
resolved_list = []
64+
for item in schema:
65+
resolved_item, circular = resolve_refs(item, root, visited)
66+
resolved_list.append(resolved_item)
67+
has_circular = has_circular or circular
68+
return resolved_list, has_circular
2369

24-
return schema
70+
return schema, False
2571

2672

2773
def process_nullable_types(
@@ -45,8 +91,10 @@ def process_nullable_types(
4591

4692
def generate_schema_from_graph(
4793
graph: CompiledStateGraph[Any, Any, Any],
48-
) -> Dict[str, Any]:
94+
) -> SchemaDetails:
4995
"""Extract input/output schema from a LangGraph graph"""
96+
input_circular_dependency = False
97+
output_circular_dependency = False
5098
schema = {
5199
"input": {"type": "object", "properties": {}, "required": []},
52100
"output": {"type": "object", "properties": {}, "required": []},
@@ -55,7 +103,9 @@ def generate_schema_from_graph(
55103
if hasattr(graph, "input_schema"):
56104
if hasattr(graph.input_schema, "model_json_schema"):
57105
input_schema = graph.input_schema.model_json_schema()
58-
unpacked_ref_def_properties = resolve_refs(input_schema)
106+
unpacked_ref_def_properties, input_circular_dependency = resolve_refs(
107+
input_schema
108+
)
59109

60110
# Process the schema to handle nullable types
61111
processed_properties = process_nullable_types(
@@ -70,7 +120,9 @@ def generate_schema_from_graph(
70120
if hasattr(graph, "output_schema"):
71121
if hasattr(graph.output_schema, "model_json_schema"):
72122
output_schema = graph.output_schema.model_json_schema()
73-
unpacked_ref_def_properties = resolve_refs(output_schema)
123+
unpacked_ref_def_properties, output_circular_dependency = resolve_refs(
124+
output_schema
125+
)
74126

75127
# Process the schema to handle nullable types
76128
processed_properties = process_nullable_types(
@@ -82,4 +134,4 @@ def generate_schema_from_graph(
82134
"required", []
83135
)
84136

85-
return schema
137+
return SchemaDetails(schema, input_circular_dependency, output_circular_dependency)

src/uipath_langchain/_cli/cli_init.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ async def langgraph_init_middleware_async(
175175
else loaded_graph
176176
)
177177
compiled_graph = state_graph.compile()
178-
graph_schema = generate_schema_from_graph(compiled_graph)
178+
schema_details = generate_schema_from_graph(compiled_graph)
179179

180180
mermaids[graph.name] = compiled_graph.get_graph(xray=1).draw_mermaid()
181181

@@ -197,11 +197,17 @@ async def langgraph_init_middleware_async(
197197
"filePath": graph.name,
198198
"uniqueId": str(uuid.uuid4()),
199199
"type": "agent",
200-
"input": graph_schema["input"],
201-
"output": graph_schema["output"],
200+
"input": schema_details.schema["input"],
201+
"output": schema_details.schema["output"],
202202
}
203203
entrypoints.append(new_entrypoint)
204204

205+
warning_circular_deps = f" schema of graph '{graph.name}' contains circular dependencies. Some types might not be correctly inferred."
206+
if schema_details.has_input_circular_dependency:
207+
console.warning("Input" + warning_circular_deps)
208+
if schema_details.has_output_circular_dependency:
209+
console.warning("Output" + warning_circular_deps)
210+
205211
except Exception as e:
206212
console.error(f"Error during graph load: {e}")
207213
return MiddlewareResult(
@@ -243,6 +249,7 @@ async def langgraph_init_middleware_async(
243249
should_continue=False,
244250
should_include_stacktrace=True,
245251
)
252+
246253
console.success(f"Created {click.style(config_path, fg='cyan')} file.")
247254

248255
generate_agents_md_files(options)

0 commit comments

Comments
 (0)