Skip to content

Commit a4ebb66

Browse files
committed
Add ManageEngine step and make_api_request tool
1 parent 767c1cd commit a4ebb66

File tree

5 files changed

+260
-0
lines changed

5 files changed

+260
-0
lines changed

patchwork/common/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from patchwork.common.tools.bash_tool import BashTool
22
from patchwork.common.tools.code_edit_tools import CodeEditTool, FileViewTool
33
from patchwork.common.tools.grep_tool import FindTextTool, FindTool
4+
from patchwork.common.tools.api_tool import APIRequestTool
45
from patchwork.common.tools.tool import Tool
56

67
__all__ = [
@@ -10,4 +11,5 @@
1011
"FileViewTool",
1112
"FindTool",
1213
"FindTextTool",
14+
"APIRequestTool",
1315
]

patchwork/common/tools/api_tool.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import json
2+
from typing import Any, Dict, Optional
3+
4+
import requests
5+
from typing_extensions import Literal
6+
7+
from patchwork.common.tools.tool import Tool
8+
9+
10+
class APIRequestTool(Tool, tool_name="make_api_request", abc_register=False):
11+
__base_url = ""
12+
__headers = dict()
13+
__auth = None
14+
__data_prefix = ""
15+
16+
def __init__(
17+
self,
18+
base_url: str | None = None,
19+
headers: dict | None = None,
20+
username: str | None = None,
21+
password: str | None = None,
22+
data_prefix: str | None = None,
23+
**kwargs,
24+
):
25+
if base_url:
26+
self.__base_url = base_url
27+
if headers:
28+
self.__headers = headers
29+
if username and password:
30+
self.__auth = (username, password)
31+
if data_prefix:
32+
self.__data_prefix = data_prefix
33+
34+
@property
35+
def json_schema(self) -> dict:
36+
return {
37+
"name": "make_api_request",
38+
"description": """\
39+
A generic tool to make HTTP API requests with flexible configuration.
40+
41+
Supports various HTTP methods (GET, POST, PUT, DELETE, PATCH) with optional
42+
authentication, headers, query parameters, and request body.
43+
44+
Authentication can be configured via:
45+
- Basic Auth (username/password)
46+
- Bearer Token
47+
- API Key (in header or query param)
48+
""",
49+
"input_schema": {
50+
"type": "object",
51+
"properties": {
52+
"url": {
53+
"type": "string",
54+
"description": "Full URL for the API endpoint",
55+
},
56+
"method": {
57+
"type": "string",
58+
"enum": ["GET", "POST", "PUT", "DELETE", "PATCH"],
59+
"description": "HTTP method for the request",
60+
},
61+
"headers": {
62+
"type": "object",
63+
"description": "Optional custom headers",
64+
},
65+
"params": {
66+
"type": "object",
67+
"description": "Optional query parameters",
68+
},
69+
"data": {
70+
"type": "string",
71+
"description": "data for POST/PUT/PATCH requests. If you need to send json data, it should be converted to a string.",
72+
},
73+
},
74+
"required": ["url", "method"],
75+
},
76+
}
77+
78+
def execute(
79+
self,
80+
url: str,
81+
method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"] = "GET",
82+
headers: Optional[Dict[str, str]] = None,
83+
params: Optional[Dict[str, Any]] = None,
84+
data: Optional[str] = None,
85+
) -> str:
86+
# Combine with default headers
87+
request_headers = headers or {}
88+
request_headers.update(self.__headers)
89+
90+
# Prepare request
91+
try:
92+
response = requests.request(
93+
method=method,
94+
url=self.__base_url + url,
95+
headers=request_headers,
96+
params=params,
97+
data=(self.__data_prefix + data if data else None),
98+
auth=self.__auth,
99+
)
100+
101+
# Raise an exception for HTTP errors
102+
response.raise_for_status()
103+
104+
# Try to parse JSON, fallback to text
105+
try:
106+
return json.dumps(response.json(), indent=2)
107+
except ValueError:
108+
return response.text
109+
except requests.RequestException as e:
110+
if e.response is not None:
111+
response_text = e.response.text
112+
status_code = e.response.status_code
113+
headers = e.response.headers
114+
115+
header_string = "\n".join(
116+
f"{key}: {value}" for key, value in headers.items()
117+
)
118+
119+
return (
120+
f"HTTP/{e.response.raw.version / 10:.1f} {status_code} {e.response.reason}\n"
121+
f"{header_string}\n"
122+
f"\n"
123+
f"{response_text}"
124+
)
125+
else:
126+
return f"API Request Error: {str(e)}"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from patchwork.common.client.llm.aio import AioLlmClient
2+
from patchwork.common.multiturn_strategy.agentic_strategy_v2 import (
3+
AgentConfig,
4+
AgenticStrategyV2,
5+
)
6+
from patchwork.common.tools.api_tool import APIRequestTool
7+
from patchwork.step import Step
8+
from patchwork.steps.ManageEngine.typed import ManageEngineInputs, ManageEngineOutputs
9+
10+
11+
class ManageEngineStep(
12+
Step, input_class=ManageEngineInputs, output_class=ManageEngineOutputs
13+
):
14+
def __init__(self, inputs: ManageEngineInputs):
15+
super().__init__(inputs)
16+
17+
if not inputs.get("access_token"):
18+
raise ValueError("access_token is required")
19+
if not inputs.get("user_prompt"):
20+
raise ValueError("user_prompt is required")
21+
22+
# Configure conversation limit
23+
self.conversation_limit = int(inputs.get("max_agent_calls", 1))
24+
25+
# Prepare system prompt with ManageEngine context
26+
system_prompt = inputs.get(
27+
"system_prompt",
28+
"""
29+
Please summarise the conversation given and provide the result in the structure that is asked of you.
30+
""",
31+
)
32+
33+
self.headers = {
34+
"Authorization": f"Zoho-oauthtoken {inputs.get('access_token')}",
35+
"Content-Type": "application/x-www-form-urlencoded",
36+
"Accept": "application/vnd.manageengine.sdp.v3+json",
37+
}
38+
39+
llm_client = AioLlmClient.create_aio_client(inputs)
40+
41+
# Configure agentic strategy with ManageEngine-specific context
42+
self.agentic_strategy = AgenticStrategyV2(
43+
model="claude-3-7-sonnet-latest",
44+
llm_client=llm_client,
45+
system_prompt_template=system_prompt,
46+
template_data={},
47+
user_prompt_template=inputs.get("user_prompt"),
48+
agent_configs=[
49+
AgentConfig(
50+
name="ManageEngine Assistant",
51+
tool_set=dict(
52+
make_api_request=APIRequestTool(
53+
headers=self.headers, data_prefix="input_data="
54+
),
55+
),
56+
system_prompt="""\
57+
You are an senior software developer helping the program manager to interact with ManageEngine ServiceDesk via the ServiceDeskPlus API.
58+
Your goal is to retrieve, create, or modify service desk tickets and related information.
59+
Use the `make_api_request` tool to interact with the ManageEngine API.
60+
Skip the headers for the api requests as they are already provided.
61+
The base url for the ServiceDeskPlus API is https://sdpondemand.manageengine.com/app/itdesk/api/v3
62+
63+
For modifying or creating data, the data should be a json string which is prefixed with "input_data=".
64+
For example, if the data is {"example_key": "example_value"}, the data should be "input_data={\"example_key\": \"example_value\"}".
65+
When you have the result of the information user requested, return the response of the final result tool as is.
66+
""",
67+
)
68+
],
69+
example_json=inputs.get("example_json"),
70+
)
71+
72+
def run(self) -> dict:
73+
# Execute the agentic strategy
74+
result = self.agentic_strategy.execute(limit=self.conversation_limit)
75+
76+
# Return results with usage information
77+
return {**result, **self.agentic_strategy.usage()}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import Dict, Optional, TypedDict
2+
3+
from typing_extensions import Annotated
4+
5+
from patchwork.common.utils.step_typing import StepTypeConfig
6+
7+
8+
class ManageEngineInputs(TypedDict, total=False):
9+
"""
10+
Inputs for the ManageEngine agentic step
11+
"""
12+
13+
# Required inputs
14+
access_token: str
15+
16+
# Optional configuration
17+
max_agent_calls: int
18+
openai_api_key: Annotated[
19+
str,
20+
StepTypeConfig(
21+
is_config=True,
22+
or_op=["patched_api_key", "google_api_key", "anthropic_api_key"],
23+
),
24+
]
25+
anthropic_api_key: Annotated[
26+
str,
27+
StepTypeConfig(
28+
is_config=True,
29+
or_op=["patched_api_key", "google_api_key", "openai_api_key"],
30+
),
31+
]
32+
google_api_key: Annotated[
33+
str,
34+
StepTypeConfig(
35+
is_config=True,
36+
or_op=["patched_api_key", "openai_api_key", "anthropic_api_key"],
37+
),
38+
]
39+
40+
# Prompt and strategy configuration
41+
system_prompt: Optional[str]
42+
user_prompt: str
43+
example_json: Optional[Dict]
44+
45+
46+
class ManageEngineOutputs(TypedDict, total=False):
47+
"""
48+
Outputs from the ManageEngine agentic step
49+
"""
50+
51+
result: Dict
52+
usage: Dict
53+
error: Optional[str]

patchwork/steps/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from patchwork.steps.GitHubAgent.GitHubAgent import GitHubAgent
3434
from patchwork.steps.JoinList.JoinList import JoinList
3535
from patchwork.steps.LLM.LLM import LLM
36+
from patchwork.steps.ManageEngine.ManageEngineStep import ManageEngineStep
3637
from patchwork.steps.ModifyCode.ModifyCode import ModifyCode
3738
from patchwork.steps.ModifyCodeOnce.ModifyCodeOnce import ModifyCodeOnce
3839
from patchwork.steps.PR.PR import PR
@@ -108,4 +109,5 @@
108109
"JoinListPB",
109110
"GetTypescriptTypeInfo",
110111
"BrowserUse",
112+
"ManageEngineStep",
111113
]

0 commit comments

Comments
 (0)