Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring deployment a bit #15

Merged
merged 16 commits into from
Feb 16, 2024
Merged

Refactoring deployment a bit #15

merged 16 commits into from
Feb 16, 2024

Conversation

kongzii
Copy link
Contributor

@kongzii kongzii commented Feb 15, 2024

Summary by CodeRabbit

  • Refactor
    • Enhanced and modularized deployment functionality for both local and Google Cloud Platform (GCP) scenarios.
  • New Features
    • Added support for deploying with updated parameters such as memory and cron schedule on GCP.
  • Bug Fixes
    • Fixed parameter types to allow None for labels, env_vars, and secrets in GCP deployment functions.
  • Tests
    • Updated tests to reflect changes in deployment method calls.

Copy link

coderabbitai bot commented Feb 15, 2024

Walkthrough

The recent updates focus on refining deployment processes for both local environments and Google Cloud Platform (GCP). Key enhancements include the introduction of DeployableCoinFlipAgent for streamlined GCP deployment with updated parameters, and significant refactoring in deployment functionality, especially for GCP, with added methods for a more modular and efficient deployment process. Changes also involve adjustments in parameter handling and the removal of specific dependencies and functionalities in favor of a more streamlined deployment approach.

Changes

File Path Summary
.../cloud_deployment/gcp/deploy.py Refactored to use DeployableCoinFlipAgent with updated deployment parameters like memory and cron schedule.
.../deploy/agent.py
.../deploy/gcp/deploy.py
Enhanced deployment functionality with added methods for local and GCP deployment, including function creation, deployment, and scheduling. Adjusted parameter handling for labels, env_vars, and secrets.
.../deploy/agent_example.py Modified DeployableCoinFlipAgent class by removing functions_framework and flask.wrappers.Request imports and the main function, streamlining the deployment process.
.../deploy/gcp/utils.py Updated function to make labels, env_vars, and secrets parameters optional and added checks to build command strings only if these dictionaries are provided.
tests/deploy/test_deploy.py Changed method call from deploy to deploy_local on DeployableCoinFlipAgent instance, removing the deployment_type parameter to align with the refactored deployment approach.

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share

Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit-tests for this file.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit tests for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository from git and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit tests.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger a review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • The JSON schema for the configuration file is available here.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/coderabbit-overrides.v2.json

CodeRabbit Discord Community

Join our Discord Community to get help, request features, and share feedback.

Base automatically changed from peter/gcp-secrets-labels to main February 15, 2024 12:23
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 0

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 878ffea and 0c3ad47.
Files ignored due to path filters (2)
  • mypy.ini is excluded by: !**/*.ini
  • poetry.lock is excluded by: !**/*.lock
Files selected for processing (6)
  • examples/cloud_deployment/gcp/deploy.py (2 hunks)
  • prediction_market_agent_tooling/deploy/agent.py (4 hunks)
  • prediction_market_agent_tooling/deploy/agent_example.py (2 hunks)
  • prediction_market_agent_tooling/deploy/gcp/deploy.py (3 hunks)
  • prediction_market_agent_tooling/deploy/gcp/utils.py (2 hunks)
  • tests/deploy/test_deploy.py (2 hunks)
Additional comments: 6
prediction_market_agent_tooling/deploy/agent_example.py (1)
  • 1-7: > 📝 NOTE

This review was outside the diff hunks and was mapped to the diff hunk with the greatest overlap. Original lines [1-11]

Changes align with PR objectives to streamline and simplify the deployment process by removing unnecessary imports and the Flask-based HTTP entry point. The refactoring enhances modularity and maintainability.

tests/deploy/test_deploy.py (1)
  • 1-6: > 📝 NOTE

This review was outside the diff hunks and was mapped to the diff hunk with the greatest overlap. Original lines [3-19]

The change from deploy to deploy_local and the removal of the deployment_type parameter align with the PR objectives, contributing to a more modular and clear deployment process.

examples/cloud_deployment/gcp/deploy.py (1)
  • 1-13: > 📝 NOTE

This review was outside the diff hunks and was mapped to the diff hunk with the greatest overlap. Original lines [3-21]

The use of DeployableCoinFlipAgent and the deploy_gcp method with updated parameters like memory and cron schedule aligns with the PR objectives, improving the deployment process's modularity and efficiency.

prediction_market_agent_tooling/deploy/gcp/deploy.py (1)
  • 23-29: The addition of the gcp_fname parameter and the update to allow None types for labels, env_vars, and secrets enhance the function's flexibility and align with the PR objectives, contributing to a more flexible and refined deployment process.
prediction_market_agent_tooling/deploy/gcp/utils.py (1)
  • 27-41: > 📝 NOTE

This review was outside the diff hunks and was mapped to the diff hunk with the greatest overlap. Original lines [14-38]

Making labels, env_vars, and secrets optional and adding checks to ensure they are not None before use simplifies the deployment command building process and aligns with the PR objectives, contributing to a more streamlined and simplified deployment process.

prediction_market_agent_tooling/deploy/agent.py (1)
  • 35-106: > 📝 NOTE

This review was outside the diff hunks and was mapped to the diff hunk with the greatest overlap. Original lines [1-122]

The introduction of deploy_local and deploy_gcp methods, along with the additional logic for GCP deployment, aligns with the PR objectives, significantly improving the modularity and efficiency of the deployment process.

examples/cloud_deployment/gcp/deploy.py Show resolved Hide resolved
def main(request) -> str:
{self.__class__.__name__}().run(market_type={market_type.__class__.__name__}.{market_type.name})
return "Success"
"""
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not happy with this, but it works for now. We can iterate again later.

During the implementation, I somehow didn't see how static functions would help in the end. I wanted to use them but then ended up with the current solution.

Please comment if I missed something.

Copy link
Contributor

Choose a reason for hiding this comment

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

Wooo nice. The problem I saw with not making run a static function is that the user can now define their agent like:

from pydantic import BaseModel
from prediction_market_agent_tooling.deploy.agent import DeployableAgent

class Foo(BaseModel):
    x: int

class DeployableCoinFlipAgent(DeployableAgent):
    foo: Foo

    ...

so now can have arbitrary dependencies that we need to know about to initialize the agent. e.g. in this example we need to import and create an instance of Foo, so deployment will fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But even if run is a static function, isn't the problem still the same? Even if run is static, they are still able to do

class DeployableCoinFlipAgent(DeployableAgent):
    foo: Foo

so, nothing changes, or does it?

Doing{self.__class__.__name__}() assumes that the class doesn't take any init arguments, but if run is static, the class needs to be initialised somewhere, so it's the same issue.

What we could do is

class DeployableAgent:
   @staticmethod
   def create_agent() -> "DeployableAgent":
      """Override this method if your agents needs custom initialization"""
      return DeployableAgent()

and use {self.__class__.__name__}.create_agent()

Copy link
Contributor

Choose a reason for hiding this comment

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

I would say that because the static method can't access self, it doesn't depend on Foo to run it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Just tried with this branch, and e.g.

from pydantic import BaseModel

from prediction_market_agent_tooling.deploy.agent import DeployableAgent
from prediction_market_agent_tooling.markets.data_models import AgentMarket
from prediction_market_agent_tooling.markets.markets import MarketType

class Foo(BaseModel):
    answer: bool

class FooAgent(DeployableAgent):
    def __init__(self, foo: Foo):
        self.foo = foo

    def answer_binary_market(self, market: AgentMarket) -> bool:
        return self.foo.answer

if __name__ == "__main__":
    agent = FooAgent(foo=Foo(answer=True))
    agent.deploy_gcp(
        repository="git+https://github.com/gnosis/prediction-market-agent-tooling.git@peter/refactor-deployment",
        market_type=MarketType.MANIFOLD,
        memory=1024,
    )

which produced

import functions_framework
from <some path> import *

from prediction_market_agent_tooling.markets.markets import MarketType


@functions_framework.http
def main(request) -> str:
    FooAgent().run(market_type=MarketType.MANIFOLD)
    return "Success"

(the <some path> string was a bit funky, but ignoring for now). This failed with:

    FooAgent().run(market_type=MarketType.MANIFOLD)
TypeError: FooAgent.__init__() missing 1 required positional argument: 'foo'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'd expect it to fail, as foo is the required argument in your FooAgent.

So you could do

class FooAgent(DeployableAgent):
    def __init__(self, foo: Foo):
        self.foo = foo

    def answer_binary_market(self, market: AgentMarket) -> bool:
        return self.foo.answer

    @staticmethod
    def create_agent() -> "FooAgent":
        return FooAgent(foo=...)

And instead of calling FooAgent() in the generated function, we would call FooAgent.create_agent().

I think our situation is very similar to the MLFlow, https://mlflow.org/docs/latest/_modules/mlflow/pyfunc/model.html#PythonModel, they also have some "parent class" that needs to be subclassed, and then this subclass can be deployed by MLFlow-tools without any worries. They don't allow to change init as well, instead there are special methods that are auto-called in the deployment.


But back to the staticmethod, let's say we have this:

class DeployableAgent:
   ...
   
   @staticmethod
   def run(market_type: MarketType, _place_bet: bool = True) -> None:
        available_markets = [
            x.to_agent_market() for x in get_binary_markets(market_type)
        ]
        markets = self.pick_markets(available_markets)
        for market in markets:
            result = self.answer_binary_market(market)
            if _place_bet:
                print(f"Placing bet on {market} with result {result}")
                place_bet(
                    market=market.original_market,
                    amount=get_tiny_bet(market_type),
                    outcome=result,
                    omen_auto_deposit=True,
                )

Now we don't have self anymore, so we need to change it, but how? We can't do

result = DeployableAgent.answer_binary_market(market)

because that way, anything subclassed won't be used, the parent abstract class will always be used.

And we also can't do

   @staticmethod
   def run(agent: DeployableAgent, market_type: MarketType, _place_bet: bool = True) -> None:
       ...

because we would be again at the square 0, we would need to initialise the agent inside of the generated main function, and that initialisation could fail, so we would need something as create_agent I proposed above anyway.

Or did you have something else in mind?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah you're right! Happy to go with this option then. We can add a guard to the base class to throw an exception if the derived class tries to override the init method:

class Base:
    def __init__(self):
        pass

    def __init_subclass__(cls, **kwargs):
        if cls.__init__ is not Base.__init__:
            raise TypeError("Cannot override __init__ method of Base class")


class Derived(Base):
    # Raises a TypeError
    def __init__(self):
        pass

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wau I didn't know about __init_subclass__ :o Added!

secrets: dict[str, str] | None = None,
cron_schedule: str | None = None,
) -> None:
path_to_agent_file = os.path.relpath(inspect.getfile(self.__class__))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will return path to the agent's implementation, not to this abstract class.

Screenshot by Dropbox Capture

def get_gcloud_fname(cls, market_type: MarketType) -> str:
return f"{cls.__class__.__name__.lower()}-{market_type}-{int(time.time())}"
def get_gcloud_fname(self, market_type: MarketType) -> str:
return f"{self.__class__.__name__.lower()}-{market_type}-{int(time.time())}"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to a normal method with self, fixed the name in gcp:

Screenshot by Dropbox Capture

examples/cloud_deployment/gcp/deploy.py Show resolved Hide resolved
def main(request) -> str:
{self.__class__.__name__}().run(market_type={market_type.__class__.__name__}.{market_type.name})
return "Success"
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

Wooo nice. The problem I saw with not making run a static function is that the user can now define their agent like:

from pydantic import BaseModel
from prediction_market_agent_tooling.deploy.agent import DeployableAgent

class Foo(BaseModel):
    x: int

class DeployableCoinFlipAgent(DeployableAgent):
    foo: Foo

    ...

so now can have arbitrary dependencies that we need to know about to initialize the agent. e.g. in this example we need to import and create an instance of Foo, so deployment will fail.

@@ -27,18 +17,6 @@
secrets={
"MANIFOLD_API_KEY": f"JUNG_PERSONAL_GMAIL_MANIFOLD_API_KEY:latest"
}, # Must be in the format "env_var_in_container => secret_name:version", you can create secrets using `gcloud secrets create --labels owner=<your-name> <secret-name>` command.
memory=512,
memory=256,
cron_schedule="0 */2 * * *",
Copy link
Contributor

Choose a reason for hiding this comment

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

much cleaner :). Removing remove_deployed_gcp_function though - living a high-risk lifestyle!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should it be auto-removed after it's scheduled to run every 2 hours? I thought that was there only for the example purposes.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess the example was there originally as something that demos all the deployment functionality but destroys everything at the end, kind of like a test that the user could run. Just guarding against the user running it without realising that they're spawning a forever-alive agent! No big deal though

labels: dict[str, str],
env_vars: dict[str, str],
secrets: dict[str, str],
labels: dict[str, str] | None,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could just make them default empty dicts to save a few chars below on if statements

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Isn't defaulting to dicts/lists/... forbidden pattern in Python, because of mutability issues?

Copy link
Contributor

Choose a reason for hiding this comment

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

😱 I wasn't aware of that before! TIL...
Explained well here: https://chat.openai.com/share/045afe93-640a-43e7-91b7-24c7e32a197e

Not an issue in this case I guess, as these dicts are only read, not written to. But thanks for the lesson and I retract my suggestion!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review Status

Actionable comments generated: 0

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 0c3ad47 and 1a15c43.
Files selected for processing (1)
  • prediction_market_agent_tooling/deploy/agent.py (4 hunks)
Files skipped from review as they are similar to previous changes (1)
  • prediction_market_agent_tooling/deploy/agent.py

@kongzii kongzii merged commit b04007c into main Feb 16, 2024
6 checks passed
@kongzii kongzii deleted the peter/refactor-deployment branch February 16, 2024 10:40
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.

2 participants