Skip to content

Commit 9275ed1

Browse files
CopilotMatanga1-2
andauthored
Fix self-service action creation when invocationMethod is provided as JSON string (#53)
* Initial plan * Initial investigation of self-service action creation issue Co-authored-by: Matanga1-2 <[email protected]> * Fix self-service action creation with string invocationMethod Co-authored-by: Matanga1-2 <[email protected]> * Improve tool guidance and update version for invocationMethod JSON string fix Co-authored-by: Matanga1-2 <[email protected]> * Update CHANGELOG.md * Update action.py * Update create_action.py * lint fixes * update poetry lock * Revert "lint fixes" This reverts commit 5a7d6a6. * lint fixes * upgrade version to 0.2.20 --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Matanga1-2 <[email protected]> Co-authored-by: Matanga1-2 <[email protected]>
1 parent 4ef80e7 commit 9275ed1

File tree

4 files changed

+135
-6
lines changed

4 files changed

+135
-6
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77
=======
88

9+
## [0.2.20] - 2025-06-29
10+
11+
### Fixed
12+
- Fixed self-service action creation when `invocationMethod` is provided as JSON string instead of object in Claude
13+
- Enhanced tool description and field descriptions to guide AI models to provide correct format from start
914

1015
## [0.2.19] - 2025-06-30
1116

src/models/actions/action.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ class ActionCommon(BaseModel):
131131
trigger: ActionTrigger = Field(..., description="The trigger configuration")
132132
invocation_method: ActionInvocationMethod = Field(
133133
...,
134-
description="The invocation method configuration",
134+
description="The invocation method configuration. Must be a JSON object (not a string) with 'type' field and method-specific properties.",
135135
alias="invocationMethod",
136136
serialization_alias="invocationMethod",
137137
)
@@ -180,4 +180,4 @@ class ActionCreate(ActionCommon):
180180
class ActionUpdate(ActionCommon):
181181
"""Model for updating an existing action."""
182182

183-
pass
183+
pass

src/tools/action/create_action.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,40 @@
1+
import json
12
from typing import Any
23

4+
from pydantic import field_validator, model_validator
5+
36
from src.client.client import PortClient
47
from src.models.actions import Action, ActionCreate
8+
from src.models.actions.action import ActionInvocationMethod
59
from src.models.common.annotations import Annotations
610
from src.models.tools.tool import Tool
711

812

913
class CreateActionToolSchema(ActionCreate):
10-
pass
14+
@field_validator('invocation_method', mode='before')
15+
@classmethod
16+
def parse_invocation_method(cls, v) -> ActionInvocationMethod | dict:
17+
"""Parse invocation method if it's provided as a JSON string."""
18+
if isinstance(v, str):
19+
try:
20+
# Parse the JSON string into a dictionary
21+
parsed = json.loads(v)
22+
return parsed
23+
except json.JSONDecodeError as e:
24+
raise ValueError(f"Invalid JSON string for invocationMethod: {e}") from e
25+
return v
26+
27+
@model_validator(mode='before')
28+
@classmethod
29+
def handle_invocation_method_alias(cls, values):
30+
"""Handle both invocationMethod and invocation_method field names."""
31+
if isinstance(values, dict) and 'invocationMethod' in values and isinstance(values['invocationMethod'], str):
32+
# If invocationMethod is provided as a string, parse it
33+
try:
34+
values['invocationMethod'] = json.loads(values['invocationMethod'])
35+
except json.JSONDecodeError as e:
36+
raise ValueError(f"Invalid JSON string for invocationMethod: {e}") from e
37+
return values
1138

1239

1340
class CreateActionTool(Tool[CreateActionToolSchema]):
@@ -16,7 +43,7 @@ class CreateActionTool(Tool[CreateActionToolSchema]):
1643
def __init__(self, port_client: PortClient):
1744
super().__init__(
1845
name="create_action",
19-
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/",
46+
description="Create a new self-service action.",
2047
function=self.create_action,
2148
input_schema=CreateActionToolSchema,
2249
output_schema=Action,
@@ -39,4 +66,4 @@ async def create_action(self, props: CreateActionToolSchema) -> dict[str, Any]:
3966
created_action = await self.port_client.create_action(action_data)
4067
created_action_dict = created_action.model_dump(exclude_unset=True, exclude_none=True)
4168

42-
return created_action_dict
69+
return created_action_dict

tests/unit/tools/action/test_create_action.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,101 @@ async def test_create_action_tool_with_complex_trigger(mock_client_with_create_a
203203
assert result["trigger"]["operation"] == "DAY-2"
204204
assert result["trigger"]["blueprintIdentifier"] == "service"
205205
assert "userInputs" in result["trigger"]
206-
assert len(result["trigger"]["userInputs"]["required"]) == 2
206+
assert len(result["trigger"]["userInputs"]["required"]) == 2
207+
208+
209+
@pytest.mark.asyncio
210+
async def test_create_action_tool_with_string_invocation_method(mock_client_with_create_action):
211+
"""Test creating an action when invocationMethod is provided as a JSON string."""
212+
from src.models.actions.action import ActionTrigger, ActionInvocationMethodWebhook
213+
tool = CreateActionTool(mock_client_with_create_action)
214+
215+
# Mock return value with webhook invocation
216+
mock_client_with_create_action.create_action.return_value = Action(
217+
identifier="string-invocation-action",
218+
title="String Invocation Action",
219+
description="Action with string invocation method",
220+
trigger=ActionTrigger(
221+
type="self-service",
222+
operation="CREATE",
223+
user_inputs={"properties": {}, "required": []},
224+
),
225+
invocation_method=ActionInvocationMethodWebhook(
226+
type="WEBHOOK",
227+
url="https://api.github.com/repos/test/test/issues",
228+
method="POST",
229+
headers={"Accept": "application/vnd.github+json"},
230+
body={"title": "{{ .inputs.title }}", "body": "{{ .inputs.body }}"}
231+
),
232+
)
233+
234+
# Input data with invocationMethod as a JSON string (like Claude might provide)
235+
input_data = {
236+
"identifier": "string-invocation-action",
237+
"title": "String Invocation Action",
238+
"description": "Action with string invocation method",
239+
"trigger": {
240+
"type": "self-service",
241+
"operation": "CREATE",
242+
"userInputs": {"properties": {}, "required": []},
243+
},
244+
"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 }}"}}'
245+
}
246+
247+
# This should work after our fix
248+
result = await tool.create_action(tool.validate_input(input_data))
249+
250+
# Verify the webhook-specific fields
251+
assert result["identifier"] == "string-invocation-action"
252+
assert result["invocationMethod"]["type"] == "WEBHOOK"
253+
assert result["invocationMethod"]["url"] == "https://api.github.com/repos/test/test/issues"
254+
assert result["invocationMethod"]["method"] == "POST"
255+
assert result["invocationMethod"]["headers"]["Accept"] == "application/vnd.github+json"
256+
257+
258+
@pytest.mark.asyncio
259+
async def test_create_action_tool_with_string_github_invocation_method(mock_client_with_create_action):
260+
"""Test creating an action when GitHub invocationMethod is provided as a JSON string."""
261+
from src.models.actions.action import ActionTrigger, ActionInvocationMethodGitHub
262+
tool = CreateActionTool(mock_client_with_create_action)
263+
264+
# Mock return value with GitHub invocation
265+
mock_client_with_create_action.create_action.return_value = Action(
266+
identifier="string-github-action",
267+
title="String GitHub Action",
268+
description="Action with string GitHub invocation method",
269+
trigger=ActionTrigger(
270+
type="self-service",
271+
operation="CREATE",
272+
user_inputs={"properties": {}, "required": []},
273+
),
274+
invocation_method=ActionInvocationMethodGitHub(
275+
type="GITHUB",
276+
org="test-org",
277+
repo="test-repo",
278+
workflow="test-workflow.yml"
279+
),
280+
)
281+
282+
# Input data with GitHub invocationMethod as a JSON string
283+
input_data = {
284+
"identifier": "string-github-action",
285+
"title": "String GitHub Action",
286+
"description": "Action with string GitHub invocation method",
287+
"trigger": {
288+
"type": "self-service",
289+
"operation": "CREATE",
290+
"userInputs": {"properties": {}, "required": []},
291+
},
292+
"invocationMethod": '{"type": "GITHUB", "org": "test-org", "repo": "test-repo", "workflow": "test-workflow.yml"}'
293+
}
294+
295+
# This should work after our fix
296+
result = await tool.create_action(tool.validate_input(input_data))
297+
298+
# Verify the GitHub-specific fields
299+
assert result["identifier"] == "string-github-action"
300+
assert result["invocationMethod"]["type"] == "GITHUB"
301+
assert result["invocationMethod"]["org"] == "test-org"
302+
assert result["invocationMethod"]["repo"] == "test-repo"
303+
assert result["invocationMethod"]["workflow"] == "test-workflow.yml"

0 commit comments

Comments
 (0)