Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/uipath-agent-framework/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-agent-framework"
version = "0.0.13"
version = "0.1.0"
description = "Python SDK that enables developers to build and deploy Microsoft Agent Framework agents to the UiPath Cloud Platform"
readme = "README.md"
requires-python = ">=3.11"
Expand All @@ -9,8 +9,8 @@ dependencies = [
"agent-framework-orchestrations>=1.0.0b260219",
"aiosqlite>=0.20.0",
"openinference-instrumentation-agent-framework>=0.1.0",
"uipath>=2.10.0, <2.11.0",
"uipath-runtime>=0.11.0, <0.12.0",
"uipath>=2.13.0, <2.14.0",
"uipath-runtime>=0.12.1, <0.13.0",
]
classifiers = [
"Intended Audience :: Developers",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,31 @@ async def delete_trigger(
self, runtime_id: str, trigger: UiPathResumeTrigger
) -> None:
"""Delete a specific resume trigger by runtime_id and interrupt_id."""
await self.delete_triggers(runtime_id, [trigger])

async def delete_triggers(
self, runtime_id: str, triggers: list[UiPathResumeTrigger]
) -> None:
"""Delete specific resume triggers by runtime_id and interrupt_id."""
interrupt_ids = [trigger.interrupt_id for trigger in triggers]
if not interrupt_ids:
return

conn = await self._get_conn()
async with self._lock:
await conn.execute(
"DELETE FROM resume_triggers WHERE runtime_id = ? AND interrupt_id = ?",
(runtime_id, trigger.interrupt_id),
(
"DELETE FROM resume_triggers "
"WHERE runtime_id = ? "
f"AND interrupt_id IN ({','.join('?' for _ in interrupt_ids)})"
),
(runtime_id, *interrupt_ids),
)
await conn.commit()

logger.debug(
"Deleted trigger %s for runtime_id=%s",
trigger.interrupt_id,
"Deleted %d triggers for runtime_id=%s",
len(interrupt_ids),
runtime_id,
)
Comment thread
radu-mocanu marked this conversation as resolved.

Expand Down
111 changes: 111 additions & 0 deletions packages/uipath-agent-framework/tests/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
import tempfile

from agent_framework import WorkflowCheckpoint
from uipath.runtime import (
UiPathResumeTrigger,
UiPathResumeTriggerName,
UiPathResumeTriggerType,
)

from uipath_agent_framework.runtime.resumable_storage import (
ScopedCheckpointStorage,
Expand Down Expand Up @@ -236,6 +241,112 @@ async def test_dispose_allows_reconnect(self):
assert loaded.checkpoint_id == "cp-persist"
await storage2.dispose()

async def test_delete_triggers(self):
"""delete_triggers removes sibling triggers and preserves runtime isolation."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "test.db")
storage = SqliteResumableStorage(db_path)
await storage.setup()

api_trigger = UiPathResumeTrigger(
trigger_type=UiPathResumeTriggerType.API,
trigger_name=UiPathResumeTriggerName.API,
item_key="first",
interrupt_id="interrupt-1",
)
timer_trigger = UiPathResumeTrigger(
trigger_type=UiPathResumeTriggerType.TIMER,
trigger_name=UiPathResumeTriggerName.TIMER,
item_key="third",
interrupt_id="interrupt-1",
)
unrelated_trigger = UiPathResumeTrigger(
trigger_type=UiPathResumeTriggerType.TASK,
trigger_name=UiPathResumeTriggerName.TASK,
item_key="second",
interrupt_id="interrupt-2",
)
other_runtime_trigger = UiPathResumeTrigger(
trigger_type=UiPathResumeTriggerType.API,
trigger_name=UiPathResumeTriggerName.API,
item_key="other-runtime",
interrupt_id="interrupt-1",
)

await storage.save_triggers(
"runtime-a", [api_trigger, timer_trigger, unrelated_trigger]
)
await storage.save_triggers("runtime-b", [other_runtime_trigger])

await storage.delete_triggers("runtime-a", [api_trigger, timer_trigger])

runtime_a_triggers = await storage.get_triggers("runtime-a")
assert runtime_a_triggers is not None
assert [trigger.interrupt_id for trigger in runtime_a_triggers] == [
"interrupt-2"
]
assert runtime_a_triggers[0].item_key == "second"

runtime_b_triggers = await storage.get_triggers("runtime-b")
assert runtime_b_triggers is not None
assert len(runtime_b_triggers) == 1
assert runtime_b_triggers[0].item_key == "other-runtime"
await storage.dispose()

async def test_delete_trigger_deletes_single_trigger(self):
"""delete_trigger removes one trigger through the compatibility wrapper."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "test.db")
storage = SqliteResumableStorage(db_path)
await storage.setup()

trigger = UiPathResumeTrigger(
trigger_type=UiPathResumeTriggerType.API,
trigger_name=UiPathResumeTriggerName.API,
item_key="first",
interrupt_id="interrupt-1",
)
unrelated_trigger = UiPathResumeTrigger(
trigger_type=UiPathResumeTriggerType.TASK,
trigger_name=UiPathResumeTriggerName.TASK,
item_key="second",
interrupt_id="interrupt-2",
)

await storage.save_triggers("runtime-a", [trigger, unrelated_trigger])

await storage.delete_trigger("runtime-a", trigger)

runtime_a_triggers = await storage.get_triggers("runtime-a")
assert runtime_a_triggers is not None
assert [trigger.interrupt_id for trigger in runtime_a_triggers] == [
"interrupt-2"
]
await storage.dispose()

async def test_delete_triggers_empty_list_is_noop(self):
"""delete_triggers with an empty list leaves stored triggers unchanged."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "test.db")
storage = SqliteResumableStorage(db_path)
await storage.setup()

trigger = UiPathResumeTrigger(
trigger_type=UiPathResumeTriggerType.API,
trigger_name=UiPathResumeTriggerName.API,
item_key="first",
interrupt_id="interrupt-1",
)
await storage.save_triggers("runtime-a", [trigger])

await storage.delete_triggers("runtime-a", [])

runtime_a_triggers = await storage.get_triggers("runtime-a")
assert runtime_a_triggers is not None
assert len(runtime_a_triggers) == 1
assert runtime_a_triggers[0].item_key == "first"
await storage.dispose()


class TestScopedCheckpointStorage:
"""Tests for ScopedCheckpointStorage prefix isolation."""
Expand Down
Loading
Loading