Repository: geekan/MetaGPT
File: metagpt/actions/action.py (Python)

Code before:
from metagpt.utils.project_repo import ProjectRepo


class Action(SerializationMixin, ContextMixin, BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    name: str = ""
    i_context: Union[
        dict, CodingContext, CodeSummarizeContext, TestingContext, RunCodeContext, CodePlanAndChangeContext, str, None
    ] = ""
    prefix: str = ""  # aask*时会加上prefix，作为system_message
    desc: str = ""  # for skill manager
    node: ActionNode = Field(default=None, exclude=True)
    # The model name or API type of LLM of the `models` in the `config2.yaml`;
    #   Using `None` to use the `llm` configuration in the `config2.yaml`.
    llm_name_or_type: Optional[str] = None

    @model_validator(mode="after")
    @classmethod
    def _update_private_llm(cls, data: Any) -> Any:
        config = ModelsConfig.default().get(data.llm_name_or_type)
        if config:
            llm = create_llm_instance(config)
            llm.cost_manager = data.llm.cost_manager
            data.llm = llm
            data.config = config   # if not set self.config, self.llm will be reset when you call Role.set_actions function
        return data

    @property
    def repo(self) -> ProjectRepo:
        if not self.context.repo:
            self.context.repo = ProjectRepo(self.context.git_repo)
        return self.context.repo

    @property
    def prompt_schema(self):
        return self.config.prompt_schema

    @property
    def project_name(self):
        return self.config.project_name

    @project_name.setter
    def project_name(self, value):
        self.config.project_name = value

    @property
    def project_path(self):
        return self.config.project_path

    @model_validator(mode="before")

Review comment (bug):
Thank you for providing the bug information.

The issue is caused by a logical error in the `Role._init_action` method in `metagpt/roles/role.py`. The `_init_action` method should check `private_llm` instead of `private_config`, because:

1. The user can set the LLM directly, in which case `private_config` is None.
2. If the user sets `private_config`, both `private_config` and `private_llm` will be set (not None).

Therefore, the solution to this problem should be to change `Role._init_action`, not the downstream logic.

```python
def _init_action(self, action: Action):
-        if not action.private_config:
+        if not action.private_llm:
             action.set_llm(self.llm, override=True)
         else:
             action.set_llm(self.llm, override=False)
```

Here is a unit test that you can use and add to `tests/metagpt/actions/test_action.py`:

```python
def test_set_llm():
    config1 = Config.default()
    config2 = Config.default()
    config2.llm.model = "deepseek-coder-mock"

    context = Context(config=config1)
    act = Action(context=context)
    assert act.config.llm.model == config1.llm.model

    llm2 = create_llm_instance(config2.llm)
    act.llm = llm2
    assert act.llm.model == llm2.model

    role = Role(context=context)
    role.set_actions([act])
    assert act.llm.model == llm2.model

    role1 = Role(context=context)
    act1 = Action(context=context)
    assert act1.config.llm.model == config1.llm.model
    act1.config = config2
    role1.set_actions([act1])
    assert act1.llm.model == llm2.model
```

Code after:
from metagpt.utils.project_repo import ProjectRepo


class Action(SerializationMixin, ContextMixin, BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    name: str = ""
    i_context: Union[
        dict, CodingContext, CodeSummarizeContext, TestingContext, RunCodeContext, CodePlanAndChangeContext, str, None
    ] = ""
    prefix: str = ""  # aask*时会加上prefix，作为system_message
    desc: str = ""  # for skill manager
    node: ActionNode = Field(default=None, exclude=True)
    # The model name or API type of LLM of the `models` in the `config2.yaml`;
    #   Using `None` to use the `llm` configuration in the `config2.yaml`.
    llm_name_or_type: Optional[str] = None

    @model_validator(mode="after")
    @classmethod
    def _update_private_llm(cls, data: Any) -> Any:
        config = ModelsConfig.default().get(data.llm_name_or_type)
        if config:
            llm = create_llm_instance(config)
            llm.cost_manager = data.llm.cost_manager
            data.llm = llm
        return data

    @property
    def repo(self) -> ProjectRepo:
        if not self.context.repo:
            self.context.repo = ProjectRepo(self.context.git_repo)
        return self.context.repo

    @property
    def prompt_schema(self):
        return self.config.prompt_schema

    @property
    def project_name(self):
        return self.config.project_name

    @project_name.setter
    def project_name(self, value):
        self.config.project_name = value

    @property
    def project_path(self):
        return self.config.project_path

    @model_validator(mode="before")
    @classmethod