Skip to content

Commit 9a13b50

Browse files
authored
Merge pull request #1042 from UiPath/mj/fix-progress-reporting
fix: populate inputSchema and outputSchema for coded agents
2 parents af5fb57 + af335ce commit 9a13b50

File tree

4 files changed

+269
-3
lines changed

4 files changed

+269
-3
lines changed

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"
3-
version = "2.2.37"
3+
version = "2.2.38"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/_cli/_evals/_progress_reporter.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,12 +537,57 @@ def _serialize_justification(
537537

538538
return justification
539539

540-
def _extract_agent_snapshot(self, entrypoint: str) -> StudioWebAgentSnapshot:
540+
def _extract_agent_snapshot(self, entrypoint: str | None) -> StudioWebAgentSnapshot:
541+
"""Extract agent snapshot from entry points configuration or low-code agent file.
542+
543+
For coded agents, reads from entry-points.json configuration file.
544+
For low-code agents (*.json files like agent.json), reads inputSchema
545+
and outputSchema directly from the agent file.
546+
547+
Args:
548+
entrypoint: The entrypoint file path to look up
549+
550+
Returns:
551+
StudioWebAgentSnapshot with input and output schemas
552+
"""
553+
if not entrypoint:
554+
logger.warning(
555+
"Entrypoint not provided - falling back to empty inputSchema "
556+
"and outputSchema"
557+
)
558+
return StudioWebAgentSnapshot(input_schema={}, output_schema={})
559+
541560
try:
561+
# Check if entrypoint is a low-code agent JSON file (e.g., agent.json)
562+
if entrypoint.endswith(".json"):
563+
agent_file_path = os.path.join(os.getcwd(), entrypoint)
564+
if os.path.exists(agent_file_path):
565+
with open(agent_file_path, "r") as f:
566+
agent_data = json.load(f)
567+
568+
# Low-code agent files have inputSchema and outputSchema at root
569+
input_schema = agent_data.get("inputSchema", {})
570+
output_schema = agent_data.get("outputSchema", {})
571+
572+
logger.debug(
573+
f"Extracted agent snapshot from low-code agent '{entrypoint}': "
574+
f"inputSchema={json.dumps(input_schema)}, "
575+
f"outputSchema={json.dumps(output_schema)}"
576+
)
577+
578+
return StudioWebAgentSnapshot(
579+
input_schema=input_schema, output_schema=output_schema
580+
)
581+
582+
# Fall back to entry-points.json for coded agents
542583
entry_points_file_path = os.path.join(
543584
os.getcwd(), str(UiPathConfig.entry_points_file_path)
544585
)
545586
if not os.path.exists(entry_points_file_path):
587+
logger.debug(
588+
f"Entry points file not found at {entry_points_file_path}, "
589+
"using empty schemas"
590+
)
546591
return StudioWebAgentSnapshot(input_schema={}, output_schema={})
547592

548593
with open(entry_points_file_path, "r") as f:
@@ -563,6 +608,12 @@ def _extract_agent_snapshot(self, entrypoint: str) -> StudioWebAgentSnapshot:
563608
input_schema = ep.get("input", {})
564609
output_schema = ep.get("output", {})
565610

611+
logger.debug(
612+
f"Extracted agent snapshot for entrypoint '{entrypoint}': "
613+
f"inputSchema={json.dumps(input_schema)}, "
614+
f"outputSchema={json.dumps(output_schema)}"
615+
)
616+
566617
return StudioWebAgentSnapshot(
567618
input_schema=input_schema, output_schema=output_schema
568619
)
@@ -724,6 +775,14 @@ def _update_eval_run_spec(
724775
# Both coded and legacy send payload directly at root level
725776
payload = inner_payload
726777

778+
# Log the payload for debugging eval run updates
779+
agent_type = "coded" if is_coded else "low-code"
780+
logger.debug(
781+
f"Updating eval run (type={agent_type}): "
782+
f"evalRunId={eval_run_id}, success={success}"
783+
)
784+
logger.debug(f"Full eval run update payload: {json.dumps(payload, indent=2)}")
785+
727786
return RequestSpec(
728787
method="PUT",
729788
endpoint=Endpoint(
@@ -762,6 +821,16 @@ def _update_coded_eval_run_spec(
762821
"evaluatorRuns": evaluator_runs,
763822
}
764823

824+
# Log the payload for debugging coded eval run updates
825+
agent_type = "coded" if is_coded else "low-code"
826+
logger.debug(
827+
f"Updating coded eval run (type={agent_type}): "
828+
f"evalRunId={eval_run_id}, success={success}"
829+
)
830+
logger.debug(
831+
f"Full coded eval run update payload: {json.dumps(payload, indent=2)}"
832+
)
833+
765834
return RequestSpec(
766835
method="PUT",
767836
endpoint=Endpoint(
@@ -826,6 +895,14 @@ def _create_eval_run_spec(
826895
# Both coded and legacy send payload directly at root level
827896
payload = inner_payload
828897

898+
# Log the payload for debugging eval run reporting
899+
agent_type = "coded" if is_coded else "low-code"
900+
logger.debug(
901+
f"Creating eval run (type={agent_type}): "
902+
f"evalSetRunId={eval_set_run_id}, evalItemId={eval_item.id}"
903+
)
904+
logger.debug(f"Full eval run payload: {json.dumps(payload, indent=2)}")
905+
829906
return RequestSpec(
830907
method="POST",
831908
endpoint=Endpoint(
@@ -872,6 +949,16 @@ def _create_eval_set_run_spec(
872949
# Both coded and legacy send payload directly at root level
873950
payload = inner_payload
874951

952+
# Log the payload for debugging eval set run reporting
953+
agent_type = "coded" if is_coded else "low-code"
954+
logger.info(
955+
f"Creating eval set run (type={agent_type}): "
956+
f"evalSetId={eval_set_id}, "
957+
f"inputSchema={json.dumps(payload.get('agentSnapshot', {}).get('inputSchema', {}))}, "
958+
f"outputSchema={json.dumps(payload.get('agentSnapshot', {}).get('outputSchema', {}))}"
959+
)
960+
logger.debug(f"Full eval set run payload: {json.dumps(payload, indent=2)}")
961+
875962
return RequestSpec(
876963
method="POST",
877964
endpoint=Endpoint(
@@ -926,6 +1013,17 @@ def _update_eval_set_run_spec(
9261013
# Both coded and legacy send payload directly at root level
9271014
payload = inner_payload
9281015

1016+
# Log the payload for debugging eval set run updates
1017+
agent_type = "coded" if is_coded else "low-code"
1018+
logger.info(
1019+
f"Updating eval set run (type={agent_type}): "
1020+
f"evalSetRunId={eval_set_run_id}, success={success}, "
1021+
f"evaluatorScores={json.dumps(payload.get('evaluatorScores', []))}"
1022+
)
1023+
logger.debug(
1024+
f"Full eval set run update payload: {json.dumps(payload, indent=2)}"
1025+
)
1026+
9291027
return RequestSpec(
9301028
method="PUT",
9311029
endpoint=Endpoint(

tests/cli/eval/test_progress_reporter.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,3 +551,171 @@ def test_update_eval_set_run_spec_with_failure_legacy(self, progress_reporter):
551551
assert spec.json["evalSetRunId"] == "test-run-id"
552552
# Backend expects integer status
553553
assert spec.json["status"] == 3 # FAILED
554+
555+
556+
# Tests for agent snapshot extraction
557+
class TestAgentSnapshotExtraction:
558+
"""Tests for extracting agent snapshot with proper schema handling."""
559+
560+
def test_extract_agent_snapshot_reads_from_entry_points(
561+
self, progress_reporter, tmp_path, monkeypatch
562+
):
563+
"""Test that agent snapshot reads schemas from entry points file."""
564+
import os
565+
566+
# Create a temporary entry points file with full schemas
567+
entry_points_data = {
568+
"entryPoints": [
569+
{
570+
"filePath": "test_agent",
571+
"uniqueId": "test-uuid",
572+
"type": "agent",
573+
"input": {
574+
"type": "object",
575+
"properties": {"query": {"type": "string"}},
576+
},
577+
"output": {
578+
"type": "object",
579+
"properties": {"response": {"type": "string"}},
580+
},
581+
}
582+
]
583+
}
584+
585+
entry_points_file = tmp_path / "entry-points.json"
586+
with open(entry_points_file, "w") as f:
587+
json.dump(entry_points_data, f)
588+
589+
# Change to the temp directory so the reporter finds the file
590+
original_cwd = os.getcwd()
591+
os.chdir(tmp_path)
592+
593+
try:
594+
snapshot = progress_reporter._extract_agent_snapshot(
595+
entrypoint="test_agent"
596+
)
597+
598+
# Should read full schemas from entry points
599+
assert snapshot.input_schema == {
600+
"type": "object",
601+
"properties": {"query": {"type": "string"}},
602+
}
603+
assert snapshot.output_schema == {
604+
"type": "object",
605+
"properties": {"response": {"type": "string"}},
606+
}
607+
finally:
608+
os.chdir(original_cwd)
609+
610+
def test_extract_agent_snapshot_returns_empty_when_no_file(self, progress_reporter):
611+
"""Test that empty schemas are returned when entry points file doesn't exist."""
612+
snapshot = progress_reporter._extract_agent_snapshot(
613+
entrypoint="nonexistent_agent"
614+
)
615+
616+
assert snapshot.input_schema == {}
617+
assert snapshot.output_schema == {}
618+
619+
def test_extract_agent_snapshot_warns_when_entrypoint_is_none(
620+
self, progress_reporter, caplog
621+
):
622+
"""Test that a warning is logged when entrypoint is None."""
623+
import logging
624+
625+
with caplog.at_level(logging.WARNING):
626+
snapshot = progress_reporter._extract_agent_snapshot(entrypoint=None)
627+
628+
assert snapshot.input_schema == {}
629+
assert snapshot.output_schema == {}
630+
assert "Entrypoint not provided" in caplog.text
631+
assert "falling back to empty inputSchema" in caplog.text
632+
633+
def test_extract_agent_snapshot_warns_when_entrypoint_is_empty(
634+
self, progress_reporter, caplog
635+
):
636+
"""Test that a warning is logged when entrypoint is empty string."""
637+
import logging
638+
639+
with caplog.at_level(logging.WARNING):
640+
snapshot = progress_reporter._extract_agent_snapshot(entrypoint="")
641+
642+
assert snapshot.input_schema == {}
643+
assert snapshot.output_schema == {}
644+
assert "Entrypoint not provided" in caplog.text
645+
646+
def test_extract_agent_snapshot_returns_empty_when_entrypoint_not_found(
647+
self, progress_reporter, tmp_path
648+
):
649+
"""Test that empty schemas are returned when entrypoint is not in file."""
650+
import os
651+
652+
# Create entry points file without the requested entrypoint
653+
entry_points_data = {
654+
"entryPoints": [
655+
{
656+
"filePath": "other_agent",
657+
"uniqueId": "test-uuid",
658+
"type": "agent",
659+
"input": {"type": "object"},
660+
"output": {"type": "object"},
661+
}
662+
]
663+
}
664+
665+
entry_points_file = tmp_path / "entry-points.json"
666+
with open(entry_points_file, "w") as f:
667+
json.dump(entry_points_data, f)
668+
669+
original_cwd = os.getcwd()
670+
os.chdir(tmp_path)
671+
672+
try:
673+
snapshot = progress_reporter._extract_agent_snapshot(
674+
entrypoint="nonexistent_agent"
675+
)
676+
677+
assert snapshot.input_schema == {}
678+
assert snapshot.output_schema == {}
679+
finally:
680+
os.chdir(original_cwd)
681+
682+
def test_agent_snapshot_serializes_with_camel_case(
683+
self, progress_reporter, tmp_path
684+
):
685+
"""Test that agent snapshot serializes to correct JSON format with camelCase."""
686+
import os
687+
688+
entry_points_data = {
689+
"entryPoints": [
690+
{
691+
"filePath": "test_agent",
692+
"uniqueId": "test-uuid",
693+
"type": "agent",
694+
"input": {"type": "object", "properties": {}},
695+
"output": {"type": "object", "properties": {}},
696+
}
697+
]
698+
}
699+
700+
entry_points_file = tmp_path / "entry-points.json"
701+
with open(entry_points_file, "w") as f:
702+
json.dump(entry_points_data, f)
703+
704+
original_cwd = os.getcwd()
705+
os.chdir(tmp_path)
706+
707+
try:
708+
snapshot = progress_reporter._extract_agent_snapshot(
709+
entrypoint="test_agent"
710+
)
711+
712+
# Serialize using pydantic
713+
serialized = snapshot.model_dump(by_alias=True)
714+
715+
# Should have camelCase keys
716+
assert "inputSchema" in serialized
717+
assert "outputSchema" in serialized
718+
assert serialized["inputSchema"] == {"type": "object", "properties": {}}
719+
assert serialized["outputSchema"] == {"type": "object", "properties": {}}
720+
finally:
721+
os.chdir(original_cwd)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)