From 866f907d34fe2a9afede6202446f9139d3521f63 Mon Sep 17 00:00:00 2001 From: Cristian Pufu Date: Thu, 22 May 2025 12:06:36 -0700 Subject: [PATCH 1/2] chore: run dev build if tag is present --- .github/workflows/publish-dev.yml | 254 +++++++++++++++--------------- 1 file changed, 127 insertions(+), 127 deletions(-) diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index ec1c004..d4cf9fa 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -1,127 +1,127 @@ -name: Publish Dev Build - -on: - pull_request: - types: [opened, synchronize, reopened, labeled] - -jobs: - publish-dev: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - - # Only run if PR has the build:dev label - # if: contains(github.event.pull_request.labels.*.name, 'build:dev') - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version-file: ".python-version" - - - name: Install dependencies - run: uv sync --all-extras - - - name: Set development version - shell: pwsh - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - $pyprojcontent = Get-Content pyproject.toml -Raw - - $PROJECT_NAME = ($pyprojcontent | Select-String -Pattern '(?m)^\[(project|tool\.poetry)\][^\[]*?name\s*=\s*"([^"]*)"' -AllMatches).Matches[0].Groups[2].Value - $CURRENT_VERSION = ($pyprojcontent | Select-String -Pattern '(?m)^\[(project|tool\.poetry)\][^\[]*?version\s*=\s*"([^"]*)"' -AllMatches).Matches[0].Groups[2].Value - - # Get PR number and run number with proper padding - $PR_NUM = [int]"${{ github.event.pull_request.number }}" - $PADDED_PR = "{0:D5}" -f [int]"${{ github.event.pull_request.number }}" - $PADDED_RUN = "{0:D4}" -f [int]"${{ github.run_number }}" - $PADDED_NEXT_PR = "{0:D5}" -f ($PR_NUM + 1) - - # Create version range strings for PR - $MIN_VERSION = "$CURRENT_VERSION.dev1$PADDED_PR" + "0000" - $MAX_VERSION = "$CURRENT_VERSION.dev1$PADDED_NEXT_PR" + "0000" - - # Create unique dev version with PR number and run ID - $DEV_VERSION = "$CURRENT_VERSION.dev1$PADDED_PR$PADDED_RUN" - - # Update version in pyproject.toml - (Get-Content pyproject.toml) -replace "version = `"$CURRENT_VERSION`"", "version = `"$DEV_VERSION`"" | Set-Content pyproject.toml - - Write-Output "Package version set to $DEV_VERSION" - - $dependencyMessage = @" - ## Development Package - - - Add this package as a dependency in your pyproject.toml: - - ``````toml - [project] - dependencies = [ - # Exact version: - "$PROJECT_NAME==$DEV_VERSION", - - # Any version from PR - "$PROJECT_NAME>=$MIN_VERSION,<$MAX_VERSION" - ] - - [[tool.uv.index]] - name = "testpypi" - url = "https://test.pypi.org/simple/" - publish-url = "https://test.pypi.org/legacy/" - explicit = true - - [tool.uv.sources] - $PROJECT_NAME = { index = "testpypi" } - `````` - "@ - - # Get the owner and repo from the GitHub repository - $owner = "${{ github.repository_owner }}" - $repo = "${{ github.repository }}".Split('/')[1] - $prNumber = $PR_NUM - - # Get the current PR description - $prUri = "https://api.github.com/repos/$owner/$repo/pulls/$prNumber" - $headers = @{ - Authorization = "token $env:GITHUB_TOKEN" - Accept = "application/vnd.github.v3+json" - } - - $pr = Invoke-RestMethod -Uri $prUri -Method Get -Headers $headers - $currentBody = $pr.body - - # Check if there's already a development package section - if ($currentBody -match '## Development Package') { - # Replace the existing section with the new dependency message - $newBody = $currentBody -replace '## Development Package(\r?\n|.)*?(?=##|$)', $dependencyMessage - } else { - # Append the dependency message to the end of the description - $newBody = if ($currentBody) { "$currentBody`n`n$dependencyMessage" } else { $dependencyMessage } - } - - # Update the PR description - $updateBody = @{ - body = $newBody - } | ConvertTo-Json - - Invoke-RestMethod -Uri $prUri -Method Patch -Headers $headers -Body $updateBody -ContentType "application/json" - - Write-Output "Updated PR description with development package information" - - - name: Build package - run: uv build - - - name: Publish - run: uv publish --index testpypi - env: - UV_PUBLISH_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }} \ No newline at end of file +name: Publish Dev Build + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + +jobs: + publish-dev: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + # Only run if PR has the build:dev label + if: contains(github.event.pull_request.labels.*.name, 'build:dev') + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version-file: ".python-version" + + - name: Install dependencies + run: uv sync --all-extras + + - name: Set development version + shell: pwsh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $pyprojcontent = Get-Content pyproject.toml -Raw + + $PROJECT_NAME = ($pyprojcontent | Select-String -Pattern '(?m)^\[(project|tool\.poetry)\][^\[]*?name\s*=\s*"([^"]*)"' -AllMatches).Matches[0].Groups[2].Value + $CURRENT_VERSION = ($pyprojcontent | Select-String -Pattern '(?m)^\[(project|tool\.poetry)\][^\[]*?version\s*=\s*"([^"]*)"' -AllMatches).Matches[0].Groups[2].Value + + # Get PR number and run number with proper padding + $PR_NUM = [int]"${{ github.event.pull_request.number }}" + $PADDED_PR = "{0:D5}" -f [int]"${{ github.event.pull_request.number }}" + $PADDED_RUN = "{0:D4}" -f [int]"${{ github.run_number }}" + $PADDED_NEXT_PR = "{0:D5}" -f ($PR_NUM + 1) + + # Create version range strings for PR + $MIN_VERSION = "$CURRENT_VERSION.dev1$PADDED_PR" + "0000" + $MAX_VERSION = "$CURRENT_VERSION.dev1$PADDED_NEXT_PR" + "0000" + + # Create unique dev version with PR number and run ID + $DEV_VERSION = "$CURRENT_VERSION.dev1$PADDED_PR$PADDED_RUN" + + # Update version in pyproject.toml + (Get-Content pyproject.toml) -replace "version = `"$CURRENT_VERSION`"", "version = `"$DEV_VERSION`"" | Set-Content pyproject.toml + + Write-Output "Package version set to $DEV_VERSION" + + $dependencyMessage = @" + ## Development Package + + - Add this package as a dependency in your pyproject.toml: + + ``````toml + [project] + dependencies = [ + # Exact version: + "$PROJECT_NAME==$DEV_VERSION", + + # Any version from PR + "$PROJECT_NAME>=$MIN_VERSION,<$MAX_VERSION" + ] + + [[tool.uv.index]] + name = "testpypi" + url = "https://test.pypi.org/simple/" + publish-url = "https://test.pypi.org/legacy/" + explicit = true + + [tool.uv.sources] + $PROJECT_NAME = { index = "testpypi" } + `````` + "@ + + # Get the owner and repo from the GitHub repository + $owner = "${{ github.repository_owner }}" + $repo = "${{ github.repository }}".Split('/')[1] + $prNumber = $PR_NUM + + # Get the current PR description + $prUri = "https://api.github.com/repos/$owner/$repo/pulls/$prNumber" + $headers = @{ + Authorization = "token $env:GITHUB_TOKEN" + Accept = "application/vnd.github.v3+json" + } + + $pr = Invoke-RestMethod -Uri $prUri -Method Get -Headers $headers + $currentBody = $pr.body + + # Check if there's already a development package section + if ($currentBody -match '## Development Package') { + # Replace the existing section with the new dependency message + $newBody = $currentBody -replace '## Development Package(\r?\n|.)*?(?=##|$)', $dependencyMessage + } else { + # Append the dependency message to the end of the description + $newBody = if ($currentBody) { "$currentBody`n`n$dependencyMessage" } else { $dependencyMessage } + } + + # Update the PR description + $updateBody = @{ + body = $newBody + } | ConvertTo-Json + + Invoke-RestMethod -Uri $prUri -Method Patch -Headers $headers -Body $updateBody -ContentType "application/json" + + Write-Output "Updated PR description with development package information" + + - name: Build package + run: uv build + + - name: Publish + run: uv publish --index testpypi + env: + UV_PUBLISH_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }} From 78ed79de7d7504e420fb2aad840425e8d8c9571e Mon Sep 17 00:00:00 2001 From: Cristian Pufu Date: Thu, 22 May 2025 14:49:33 -0700 Subject: [PATCH 2/2] feat: api trigger hitl --- pyproject.toml | 2 +- .../_cli/_runtime/_runtime.py | 119 ++++++++++++++++-- 2 files changed, 113 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8691ca2..c72caa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-llamaindex" -version = "0.0.12" +version = "0.0.13" description = "UiPath LlamaIndex SDK" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10" diff --git a/src/uipath_llamaindex/_cli/_runtime/_runtime.py b/src/uipath_llamaindex/_cli/_runtime/_runtime.py index 8355857..f7e7c12 100644 --- a/src/uipath_llamaindex/_cli/_runtime/_runtime.py +++ b/src/uipath_llamaindex/_cli/_runtime/_runtime.py @@ -1,17 +1,29 @@ import json import logging +import os +import pickle +import uuid from contextlib import suppress -from typing import Optional +from typing import Any, Optional +from llama_index.core.workflow import ( + Context, + HumanResponseEvent, + InputRequiredEvent, + JsonPickleSerializer, +) from openinference.instrumentation.llama_index import LlamaIndexInstrumentor from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from uipath import UiPath from uipath._cli._runtime._contracts import ( + UiPathApiTrigger, UiPathBaseRuntime, UiPathErrorCategory, + UiPathResumeTrigger, UiPathRuntimeResult, + UiPathRuntimeStatus, ) from .._tracing._oteladapter import LlamaIndexExporter @@ -55,19 +67,45 @@ async def execute(self) -> Optional[UiPathRuntimeResult]: try: start_event_class = self.context.workflow._start_event_class - ev = start_event_class(**self.context.input_json) - handler = self.context.workflow.run(start_event=ev) + ctx: Context = self._get_context() + + handler = self.context.workflow.run(start_event=ev, ctx=ctx) + + resume_trigger: UiPathResumeTrigger = None async for event in handler.stream_events(): + if isinstance(event, InputRequiredEvent): + resume_trigger = UiPathResumeTrigger( + api_resume=UiPathApiTrigger( + inbox_id=str(uuid.uuid4()), request=event.prefix + ) + ) + break print(event) - output = await handler + if resume_trigger is None: + output = await handler + self.context.result = UiPathRuntimeResult( + output=self._serialize_object(output), + status=UiPathRuntimeStatus.SUCCESSFUL, + ) + else: + self.context.result = UiPathRuntimeResult( + output=self._serialize_object(output), + status=UiPathRuntimeStatus.SUSPENDED, + resume=resume_trigger, + ) - self.context.result = UiPathRuntimeResult( - output=self._serialize_object(output) - ) + if self.context.state_file: + serializer = JsonPickleSerializer() + ctx_dict = ctx.to_dict(serializer=serializer) + ctx_dict["uipath_resume_trigger"] = ( + serializer.serialize(resume_trigger) if resume_trigger else None + ) + with open(self.context.state_file, "wb") as f: + pickle.dump(ctx_dict, f) return self.context.result @@ -172,6 +210,73 @@ async def cleanup(self) -> None: """Clean up all resources.""" pass + async def _get_context(self) -> Context: + """ + Get the context for the LlamaIndex agent. + + Returns: + The context object for the LlamaIndex agent. + """ + logger.debug(f"Resumed: {self.context.resume} Input: {self.context.input_json}") + + if not self.context.resume: + return Context(self.context.workflow) + + if not self.context.state_file or not os.path.exists(self.context.state_file): + return Context(self.context.workflow) + + serializer = JsonPickleSerializer() + ctx: Context = None + + with open(self.context.state_file, "rb") as f: + loaded_ctx_dict = pickle.load(f) + ctx = Context.from_dict( + self.context.workflow, + loaded_ctx_dict, + serializer=serializer, + ) + + if self.context.input_json: + ctx.send_event(HumanResponseEvent(response=self.context.input_json)) + + resumed_trigger_data = loaded_ctx_dict["uipath_resume_trigger"] + if resumed_trigger_data: + resumed_trigger: UiPathResumeTrigger = serializer.deserialize( + resumed_trigger_data, UiPathResumeTrigger + ) + inbox_id = resumed_trigger.api_resume.inbox_id + payload = await self._get_api_payload(inbox_id) + ctx.send_event(HumanResponseEvent(response=payload)) + + return ctx + + async def _get_api_payload(self, inbox_id: str) -> Any: + """ + Fetch payload data for API triggers. + + Args: + inbox_id: The Id of the inbox to fetch the payload for. + + Returns: + The value field from the API response payload, or None if an error occurs. + """ + try: + response = self._uipath.api_client.request( + "GET", + f"/orchestrator_/api/JobTriggers/GetPayload/{inbox_id}", + include_folder_headers=True, + ) + data = response.json() + return data.get("payload") + except Exception as e: + raise UiPathLlamaIndexRuntimeError( + "API_CONNECTION_ERROR", + "Failed to get trigger payload", + f"Error fetching API trigger payload for inbox {inbox_id}: {str(e)}", + UiPathErrorCategory.SYSTEM, + response.status_code, + ) from e + def _serialize_object(self, obj): """Recursively serializes an object and all its nested components.""" # Handle Pydantic models