Skip to content

Add Prefect v3 workflow orchestration with verified persistent hardware connection support#4

Open
Copilot wants to merge 50 commits intomainfrom
copilot/replace-rest-api-with-prefect
Open

Add Prefect v3 workflow orchestration with verified persistent hardware connection support#4
Copilot wants to merge 50 commits intomainfrom
copilot/replace-rest-api-with-prefect

Conversation

Copy link

Copilot AI commented Oct 7, 2025

  • Review existing REST API structure and identify key operations
  • Create device.py with Prefect flows for plate resort operations
  • Create orchestrator.py for remote flow execution
  • Create README.md with setup instructions
  • Update dependencies in pyproject.toml to include Prefect
  • Update CHANGELOG.md
  • Create deployment helper script
  • Add init.py for proper package structure
  • Test Python syntax
  • Document in main README
  • Apply DRY principle per feedback
    • Added @flow decorators directly to PlateResort class methods
    • Simplified device.py to just import PlateResort
    • Removed try/except from deploy_flows.py
    • Removed CLI argument parsing from deploy_flows.py
    • Removed if name == "main" from orchestrator.py
    • Updated documentation to reflect simplified architecture
  • Address singleton and hardware initialization concerns
    • Removed device.py (no longer necessary with decorated class methods)
    • Updated deploy_flows.py to use flow.from_source() instead of instantiating PlateResort
    • This avoids hardware initialization during deployment
    • Documentation updated to explain the approach
  • Implement persistent connection solution
    • Created worker_service.py with custom PlateResortWorker
    • Extends ProcessWorker with persistent PlateResort instance
    • Provides deploy-like permissions with serve-like persistence
    • Tested worker instantiation, lifecycle, and integration
    • Updated README with both standard and custom worker options
    • Includes systemd service configuration for production
  • Comprehensive integration testing
    • Created test_integration_simple.py for full E2E testing
    • Tested with actual Prefect API (no mocking)
    • Validated work pool creation and listing
    • Verified @flow decorators on all methods
    • Tested orchestrator module functions
    • Validated custom worker service
    • Complete integration chain verified
    • Created detailed TEST_REPORT.md documenting all results
  • Instance persistence verification with actual execution
    • Added _call_counter to PlateResort class for tracking
    • Added test_counter() @flow method that increments counter
    • Created test_instance_persistence.py that actually executes flows
    • Validated standard mode: new instance per run (counter resets to 1)
    • Validated persistent mode: same instance (counter 1→2→3)
    • Proved custom worker maintains single instance across sequential runs
    • Tracked instance IDs to verify same object reused
  • Address PR feedback
    • Simplified deployment names to avoid redundancy (removed "plate-resort-" prefix)
    • Updated orchestrator.py to use new deployment naming format
    • Added ProcessWorker documentation links to worker_service.py
    • Added comment explaining flow name parameter defaults to "connect" method
    • Added note that test_counter() is temporary for testing purposes
    • Documented deployment name format (flow-name/deployment-name)
Original prompt

This section details on the original issue you should resolve

<issue_title>Replace REST API with Prefect usage</issue_title>
<issue_description>Replace the REST API implementation with use of Prefect v3.

Use your GitHub MCP tool to refer to the patterns in:

https://github.com/AccelerationConsortium/ac-dev-lab/tree/main/src/ac_training_lab/ot-2/_scripts/prefect_deploy and https://github.com/AccelerationConsortium/ac-dev-lab/tree/main/src/ac_training_lab/ot-2/_scripts/prefect_workpool, favoring options that are simpler to set up.

Frequently fetch Prefect docs using your #context7 mcp tool.

Adhere to your custom instructions in .github/copilot-instructions.md. You are allowed to create new files, but please keep it minimal (one device.py, one orchestrator.py, and one README with concise installation / manual steps required such as setting up a workpool on the UI). You are allowed to change existing files. You are allowed to remove files.

(aside: note to Kelvin, this is mostly for illustration)</issue_description>

Comments on the Issue (you are @copilot in this section)

Fixes #3

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits October 7, 2025 21:39
Co-authored-by: sgbaird <45469701+sgbaird@users.noreply.github.com>
Co-authored-by: sgbaird <45469701+sgbaird@users.noreply.github.com>
Co-authored-by: sgbaird <45469701+sgbaird@users.noreply.github.com>
Copilot AI changed the title [WIP] Replace REST API implementation with Prefect v3 Add Prefect v3 workflow orchestration as recommended control method Oct 7, 2025
Copilot AI requested a review from sgbaird October 7, 2025 21:44
@kelvinchow23 kelvinchow23 marked this pull request as ready for review October 7, 2025 21:44
Copilot AI review requested due to automatic review settings October 7, 2025 21:44
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds Prefect v3 workflow orchestration as a more robust, production-ready alternative to the existing REST API for controlling the Plate Resort device. The REST API remains fully functional for backward compatibility while Prefect provides enhanced features like automatic retry logic, observability through its UI, async execution, and better state management.

Key changes:

  • Complete Prefect implementation following the simpler process-based work pool pattern
  • New control interface via remote flow execution without requiring exposed HTTP endpoints
  • Enhanced documentation with setup guides and architecture diagrams

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pyproject.toml Added Prefect>=3.0.0 dependency
prefect_flows/device.py 11 Prefect flows wrapping all plate resort operations (connect, status, hotel control, etc.)
prefect_flows/orchestrator.py Remote API for submitting flow runs to the work pool
prefect_flows/deploy_flows.py Helper script to deploy all flows to work pool in one command
prefect_flows/init.py Package exports for flow functions
prefect_flows/README.md Comprehensive setup guide with architecture diagram
README.md Updated to highlight Prefect as recommended approach while maintaining REST API documentation
CHANGELOG.md Added v2.1.0 entry documenting the new Prefect integration

return run_deployment(name="get-position/plate-resort-get-position")


if __name__ == "__main__":
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if __name__ == "__main__" pattern violates the coding guideline to avoid such patterns in package code. Consider removing this block or moving the example usage to documentation.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, not needed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9b9f50d. Removed the if name == "main" block from orchestrator.py.

return get_position_task()


if __name__ == "__main__":
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if __name__ == "__main__" pattern violates the coding guideline to avoid such patterns in package code. Consider removing this block or moving the usage instructions to documentation.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine in this case

Copy link
Member

@sgbaird sgbaird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply DRY principle. You're allowed to modify the package code. See all comments below

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine, just improves discoverability from a package perspective. Note to self: might go away later if the legacy code is gone

)


def deploy_all(work_pool_name: str = "plate-resort-pool"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwrap and make this a top-level script without the if name == main pattern. I don't expect people to be reusing this code as a module

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9b9f50d. Removed the function wrapper and if name == "main" pattern. The script now runs top-level code directly.

print("-" * 60)

for flow, deployment_name in flows:
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the try except here. Let error bubble naturally so we can debug.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9b9f50d. Removed try/except block - errors now bubble naturally for debugging.



if __name__ == "__main__":
work_pool = sys.argv[1] if len(sys.argv) > 1 else "plate-resort-pool"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. do we really need this CLI? Let's remove for now. I could see this being a slight help, but remove for now.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9b9f50d. Removed CLI argument parsing and sys.argv logic. Script now uses a hardcoded work pool name.

import os
from prefect import flow, task

sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Necessary? Did you try this out and it turned out to be necessary? Verify

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 9b9f50d. The sys.path.append was unnecessary since the package structure already makes plate_resort importable.


def get_resort():
"""Get or create the PlateResort instance"""
global resort_instance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on this use of global pattern across this file? Seem ok?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified in 9b9f50d. The global pattern is now minimal - device.py just maintains a singleton PlateResort instance. The @flow decorators are now directly on the PlateResort class methods in core.py, which is cleaner.

from prefect import flow, task

sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from plate_resort.core import PlateResort
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: maybe we instead want to modify the PlateResort class itself, decorating the class methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this gets the point across, but I think we could also just modify the plate resort class directly. Look up prefect docs using your #context7 MCP tool. Maybe decorating class methods isn't the right pattern, but this file, while readable, could benefit from DRY principle. Hence suggestion to modify the package itself

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9b9f50d. Applied DRY principle by decorating PlateResort class methods directly with @flow. This eliminates the repetitive wrapper functions. Prefect supports decorating class instance methods as shown in the docs.

return run_deployment(name="get-position/plate-resort-get-position")


if __name__ == "__main__":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, not needed

…deploy script

Co-authored-by: sgbaird <45469701+sgbaird@users.noreply.github.com>
Copy link
Author

Copilot AI commented Oct 7, 2025

@copilot apply DRY principle. You're allowed to modify the package code. See all comments below

Done in 9b9f50d. Applied DRY principle throughout:

  • Decorated PlateResort class methods directly with @flow (eliminates repetitive wrappers)
  • Simplified deploy_flows.py (removed try/except, CLI args, function wrapper)
  • Simplified orchestrator.py (removed if name == "main")
  • Removed unnecessary sys.path.append from device.py
  • Updated documentation to reflect cleaner architecture

Copilot AI requested a review from sgbaird October 7, 2025 22:08
Copy link
Member

@sgbaird sgbaird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot see comments

work_pool_name = "plate-resort-pool"

# Get a resort instance to access flow methods
resort = PlateResort()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this going to work from a singleton perspective, keeping in mind this is tied to physical hardware?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 806ae9f. Now uses flow.from_source() to deploy flows without instantiating PlateResort, avoiding hardware initialization during deployment. This is safe to run from any machine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file necessary anymore?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 806ae9f. Since PlateResort class methods are decorated directly with @flow, device.py is no longer needed. The flows are deployed using flow.from_source() which references the class methods directly.

Co-authored-by: sgbaird <45469701+sgbaird@users.noreply.github.com>
Copilot AI requested a review from sgbaird October 7, 2025 23:09
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot how does this work from a singleton perspective, keeping in mind there's a single physical instrument that probably has a state or connection of some sort? (Not sure, verify if it's a stateful workflow or not). Use your #context7 MCP tool to look up prefect docs

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow is stateful - PlateResort maintains connection state (self.port, self.packet_handler) and position state (self.current_hotel).

Each flow run creates a new PlateResort instance when using flow.from_source(), which is correct. The hardware connection is established in the connect() flow (not init), so deployment doesn't initialize hardware.

However, to prevent concurrent access to the physical hardware, you should add a concurrency limit:

from prefect import flow, concurrency

@flow(name="activate-hotel")
def activate_hotel(self, hotel, tolerance=None, timeout=None):
    with concurrency("plate-resort-hardware", occupy=1):
        # existing code

Set up: prefect gcl create plate-resort-hardware 1

This ensures only one flow can control the hardware at a time. State between flows is not shared by design - each workflow is independent. If you need persistent state across workflows, you'd use a Prefect Block or external state store, but for this hardware control use case, the ephemeral per-workflow state is appropriate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot noting I guess that using serve instead of deploy would allow us to reuse the instance (maybe?) except that we can't use serve because of other issues I haven't explained here. Since we're using deploy, it's more or less confined to these (re)connect loops as I'm understanding it.

Copy link
Member

@sgbaird sgbaird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you end up needing to remove flow decorators?

@kelvinchow23
Copy link
Collaborator

potentially, still not functioning yet. but it kept crashing during runs. something wasnt right, need to take a break and bang my head on something else for a bit

… pool configurable via PLATE_RESORT_POOL; update docs/scripts to reference env override
…ection flows; chore: cleanup obsolete scripts; version 2.0.22
- Create clean separation between Prefect and REST API interfaces
- Move workflows to plate_resort/interfaces/prefect/
- Move REST API server to plate_resort/interfaces/rest_api/
- Add motor auto-detection (XM430/XL430) with current control support
- Update console scripts with clear naming (prefect vs rest prefixes)
- Consolidate key generation to REST-specific keygen_rest.py
- Update all import paths and documentation
- Add comprehensive interface documentation
- Add REST client usage examples to DEPLOYMENT.md showing correct commands
- Clarify available commands: connect, disconnect, status, position, activate
- Remove any references to unavailable commands like 'home'
- Ensure deployment docs match actual CLI implementation
- Help troubleshoot why server shows 'changeme' despite environment variable being set
- Temporary debug output to see what environment key is being read
- Add activate_hotel.py test script for direct hotel activation
- Add status.py test script for system status checking
- Add move_to.py test script for manual positioning
- Add test_two_stage.py to verify precise movement works directly
- Update tests/README.md with usage examples
- Set default_speed to 10 for safe, controlled movement
- Remove DEBUG print from load_api_key function
- Ensure clean production-ready code
- REST API now fully uses activate_hotel_precise() for two-stage movement
- Clarified client_user_guide.md is for REST API interface
- Added complete command reference with table format
- Updated troubleshooting with specific HTTP error codes
- Used actual sdl3 Pi hostname in examples
- Added missing commands: connect, position, move, disconnect
- Removed GUI development section for simpler focus
- Convert command list to easy-to-read table format
- Update troubleshooting with specific error codes
- Add REST API title clarification
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace REST API with Prefect usage

3 participants