diff --git a/CHANGELOG.md b/CHANGELOG.md index bf317aa..5c4c546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ======= +## [0.2.20] - 2025-06-29 + +### Fixed +- Fixed self-service action creation when `invocationMethod` is provided as JSON string instead of object in Claude +- Enhanced tool description and field descriptions to guide AI models to provide correct format from start ## [0.2.19] - 2025-06-30 diff --git a/src/models/actions/action.py b/src/models/actions/action.py index 0a297c1..c8d62ce 100644 --- a/src/models/actions/action.py +++ b/src/models/actions/action.py @@ -131,7 +131,7 @@ class ActionCommon(BaseModel): trigger: ActionTrigger = Field(..., description="The trigger configuration") invocation_method: ActionInvocationMethod = Field( ..., - description="The invocation method configuration", + description="The invocation method configuration. Must be a JSON object (not a string) with 'type' field and method-specific properties.", alias="invocationMethod", serialization_alias="invocationMethod", ) @@ -180,4 +180,4 @@ class ActionCreate(ActionCommon): class ActionUpdate(ActionCommon): """Model for updating an existing action.""" - pass \ No newline at end of file + pass diff --git a/src/tools/action/create_action.py b/src/tools/action/create_action.py index 81c6607..45f0243 100644 --- a/src/tools/action/create_action.py +++ b/src/tools/action/create_action.py @@ -1,13 +1,40 @@ +import json from typing import Any +from pydantic import field_validator, model_validator + from src.client.client import PortClient from src.models.actions import Action, ActionCreate +from src.models.actions.action import ActionInvocationMethod from src.models.common.annotations import Annotations from src.models.tools.tool import Tool class CreateActionToolSchema(ActionCreate): - pass + @field_validator('invocation_method', mode='before') + @classmethod + def parse_invocation_method(cls, v) -> ActionInvocationMethod | dict: + """Parse invocation method if it's provided as a JSON string.""" + if isinstance(v, str): + try: + # Parse the JSON string into a dictionary + parsed = json.loads(v) + return parsed + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON string for invocationMethod: {e}") from e + return v + + @model_validator(mode='before') + @classmethod + def handle_invocation_method_alias(cls, values): + """Handle both invocationMethod and invocation_method field names.""" + if isinstance(values, dict) and 'invocationMethod' in values and isinstance(values['invocationMethod'], str): + # If invocationMethod is provided as a string, parse it + try: + values['invocationMethod'] = json.loads(values['invocationMethod']) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON string for invocationMethod: {e}") from e + return values class CreateActionTool(Tool[CreateActionToolSchema]): @@ -16,7 +43,7 @@ class CreateActionTool(Tool[CreateActionToolSchema]): def __init__(self, port_client: PortClient): super().__init__( name="create_action", - description="Create a new self-service action or automation in your Port account. To learn more about actions and automations, check out the documentation at https://docs.port.io/actions-and-automations/", + description="Create a new self-service action.", function=self.create_action, input_schema=CreateActionToolSchema, output_schema=Action, @@ -39,4 +66,4 @@ async def create_action(self, props: CreateActionToolSchema) -> dict[str, Any]: created_action = await self.port_client.create_action(action_data) created_action_dict = created_action.model_dump(exclude_unset=True, exclude_none=True) - return created_action_dict \ No newline at end of file + return created_action_dict diff --git a/tests/unit/tools/action/test_create_action.py b/tests/unit/tools/action/test_create_action.py index 4336a0d..e1f829a 100644 --- a/tests/unit/tools/action/test_create_action.py +++ b/tests/unit/tools/action/test_create_action.py @@ -203,4 +203,101 @@ async def test_create_action_tool_with_complex_trigger(mock_client_with_create_a assert result["trigger"]["operation"] == "DAY-2" assert result["trigger"]["blueprintIdentifier"] == "service" assert "userInputs" in result["trigger"] - assert len(result["trigger"]["userInputs"]["required"]) == 2 \ No newline at end of file + assert len(result["trigger"]["userInputs"]["required"]) == 2 + + +@pytest.mark.asyncio +async def test_create_action_tool_with_string_invocation_method(mock_client_with_create_action): + """Test creating an action when invocationMethod is provided as a JSON string.""" + from src.models.actions.action import ActionTrigger, ActionInvocationMethodWebhook + tool = CreateActionTool(mock_client_with_create_action) + + # Mock return value with webhook invocation + mock_client_with_create_action.create_action.return_value = Action( + identifier="string-invocation-action", + title="String Invocation Action", + description="Action with string invocation method", + trigger=ActionTrigger( + type="self-service", + operation="CREATE", + user_inputs={"properties": {}, "required": []}, + ), + invocation_method=ActionInvocationMethodWebhook( + type="WEBHOOK", + url="https://api.github.com/repos/test/test/issues", + method="POST", + headers={"Accept": "application/vnd.github+json"}, + body={"title": "{{ .inputs.title }}", "body": "{{ .inputs.body }}"} + ), + ) + + # Input data with invocationMethod as a JSON string (like Claude might provide) + input_data = { + "identifier": "string-invocation-action", + "title": "String Invocation Action", + "description": "Action with string invocation method", + "trigger": { + "type": "self-service", + "operation": "CREATE", + "userInputs": {"properties": {}, "required": []}, + }, + "invocationMethod": '{"type": "WEBHOOK", "url": "https://api.github.com/repos/test/test/issues", "method": "POST", "headers": {"Accept": "application/vnd.github+json"}, "body": {"title": "{{ .inputs.title }}", "body": "{{ .inputs.body }}"}}' + } + + # This should work after our fix + result = await tool.create_action(tool.validate_input(input_data)) + + # Verify the webhook-specific fields + assert result["identifier"] == "string-invocation-action" + assert result["invocationMethod"]["type"] == "WEBHOOK" + assert result["invocationMethod"]["url"] == "https://api.github.com/repos/test/test/issues" + assert result["invocationMethod"]["method"] == "POST" + assert result["invocationMethod"]["headers"]["Accept"] == "application/vnd.github+json" + + +@pytest.mark.asyncio +async def test_create_action_tool_with_string_github_invocation_method(mock_client_with_create_action): + """Test creating an action when GitHub invocationMethod is provided as a JSON string.""" + from src.models.actions.action import ActionTrigger, ActionInvocationMethodGitHub + tool = CreateActionTool(mock_client_with_create_action) + + # Mock return value with GitHub invocation + mock_client_with_create_action.create_action.return_value = Action( + identifier="string-github-action", + title="String GitHub Action", + description="Action with string GitHub invocation method", + trigger=ActionTrigger( + type="self-service", + operation="CREATE", + user_inputs={"properties": {}, "required": []}, + ), + invocation_method=ActionInvocationMethodGitHub( + type="GITHUB", + org="test-org", + repo="test-repo", + workflow="test-workflow.yml" + ), + ) + + # Input data with GitHub invocationMethod as a JSON string + input_data = { + "identifier": "string-github-action", + "title": "String GitHub Action", + "description": "Action with string GitHub invocation method", + "trigger": { + "type": "self-service", + "operation": "CREATE", + "userInputs": {"properties": {}, "required": []}, + }, + "invocationMethod": '{"type": "GITHUB", "org": "test-org", "repo": "test-repo", "workflow": "test-workflow.yml"}' + } + + # This should work after our fix + result = await tool.create_action(tool.validate_input(input_data)) + + # Verify the GitHub-specific fields + assert result["identifier"] == "string-github-action" + assert result["invocationMethod"]["type"] == "GITHUB" + assert result["invocationMethod"]["org"] == "test-org" + assert result["invocationMethod"]["repo"] == "test-repo" + assert result["invocationMethod"]["workflow"] == "test-workflow.yml" \ No newline at end of file