Skip to content

Commit ab4d6c9

Browse files
authored
Merge pull request #1374 from patched-codes/feature/ManageEngineAgent
Add ManageEngineAgent
2 parents dfc3386 + d96b234 commit ab4d6c9

File tree

10 files changed

+236
-19
lines changed

10 files changed

+236
-19
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: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import json
2+
from typing import Any, Callable, 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+
def __init__(
12+
self,
13+
headers: Optional[Dict[str, str]] = dict(),
14+
username: Optional[str] = None,
15+
password: Optional[str] = None,
16+
preprocess_data: Callable[[str], str] = lambda x: x,
17+
**kwargs,
18+
):
19+
self._headers = headers
20+
self._auth = (username, password) if username and password else None
21+
self._preprocess_data = preprocess_data
22+
23+
@property
24+
def json_schema(self) -> dict:
25+
return {
26+
"name": "make_api_request",
27+
"description": """\
28+
A generic tool to make HTTP API requests with flexible configuration.
29+
30+
Supports various HTTP methods (GET, POST, PUT, DELETE, PATCH) with optional
31+
authentication, headers, query parameters, and request body.
32+
33+
Authentication can be configured via:
34+
- Basic Auth (username/password)
35+
- Bearer Token
36+
- API Key (in header or query param)
37+
""",
38+
"input_schema": {
39+
"type": "object",
40+
"properties": {
41+
"url": {
42+
"type": "string",
43+
"description": "Full URL for the API endpoint",
44+
},
45+
"method": {
46+
"type": "string",
47+
"enum": ["GET", "POST", "PUT", "DELETE", "PATCH"],
48+
"description": "HTTP method for the request",
49+
},
50+
"headers": {
51+
"type": "object",
52+
"description": "Optional custom headers",
53+
},
54+
"params": {
55+
"type": "object",
56+
"description": "Optional query parameters",
57+
},
58+
"data": {
59+
"type": "string",
60+
"description": "data for POST/PUT/PATCH requests. If you need to send json data, it should be converted to a string.",
61+
},
62+
},
63+
"required": ["url", "method"],
64+
},
65+
}
66+
67+
def execute(
68+
self,
69+
url: str,
70+
method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"] = "GET",
71+
headers: Optional[Dict[str, str]] = None,
72+
params: Optional[Dict[str, Any]] = None,
73+
data: Optional[str] = None,
74+
) -> str:
75+
# Combine with default headers
76+
request_headers = headers or {}
77+
request_headers.update(self._headers)
78+
79+
# Prepare request
80+
response = requests.request(
81+
method=method,
82+
url=url,
83+
headers=request_headers,
84+
params=params,
85+
data=(self._preprocess_data(data) if data else None),
86+
auth=self._auth,
87+
)
88+
89+
if not response.ok:
90+
response_text = response.text
91+
status_code = response.status_code
92+
headers = response.headers
93+
94+
header_string = "\n".join(
95+
f"{key}: {value}" for key, value in headers.items()
96+
)
97+
98+
return (
99+
f"HTTP/{response.raw.version / 10:.1f} {status_code} {response.reason}\n"
100+
f"{header_string}\n"
101+
f"\n"
102+
f"{response_text}"
103+
)
104+
105+
# Try to parse JSON, fallback to text
106+
try:
107+
return json.dumps(response.json())
108+
except ValueError:
109+
return response.text

patchwork/steps/BrowserUse/BrowserUse.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
from datetime import datetime
55

6+
from patchwork.common.utils.utils import mustache_render
67
from patchwork.step import Step
78
from patchwork.steps import SimplifiedLLMOnce
89
from patchwork.steps.BrowserUse.typed import BrowserUseInputs, BrowserUseOutputs
@@ -178,7 +179,7 @@ def run(self) -> dict:
178179
agent = Agent(
179180
browser=browser,
180181
controller=controller,
181-
task=self.inputs["task"],
182+
task=mustache_render(self.inputs["task"], self.inputs["task_value"]),
182183
llm=self.llm,
183184
generate_gif=self.generate_gif,
184185
validate_output=True,

patchwork/steps/BrowserUse/typed.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1-
from typing_extensions import Annotated, TypedDict
1+
from typing_extensions import Annotated, Any, Dict, Optional, TypedDict
22

33
from patchwork.common.utils.step_typing import StepTypeConfig
44

55

6-
class BrowserUseInputs(TypedDict, total=False):
6+
class __BrowserUseInputsRequired(TypedDict):
77
task: str
8-
example_json: str
9-
openai_api_key: Annotated[
10-
str,
11-
StepTypeConfig(is_config=True, or_op=["google_api_key", "anthropic_api_key"]),
12-
]
13-
anthropic_api_key: Annotated[str, StepTypeConfig(is_config=True, or_op=["google_api_key", "openai_api_key"])]
14-
google_api_key: Annotated[
15-
str,
16-
StepTypeConfig(is_config=True, or_op=["openai_api_key", "anthropic_api_key"]),
17-
]
18-
generate_gif: Annotated[bool, StepTypeConfig(is_config=True)]
8+
task_value: Dict[str, Any]
9+
10+
11+
class BrowserUseInputs(__BrowserUseInputsRequired, total=False):
12+
example_json: Optional[str]
13+
openai_api_key: Annotated[str, StepTypeConfig(or_op=["google_api_key", "anthropic_api_key"])]
14+
anthropic_api_key: Annotated[str, StepTypeConfig(or_op=["google_api_key", "openai_api_key"])]
15+
google_api_key: Annotated[str, StepTypeConfig(or_op=["openai_api_key", "anthropic_api_key"])]
16+
generate_gif: Optional[bool]
1917

2018

2119
class BrowserUseOutputs(TypedDict):

patchwork/steps/GitHubAgent/GitHubAgent.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
AgenticStrategyV2,
77
)
88
from patchwork.common.tools.github_tool import GitHubTool
9+
from patchwork.common.utils.utils import mustache_render
910
from patchwork.step import Step
1011
from patchwork.steps.GitHubAgent.typed import GitHubAgentInputs, GitHubAgentOutputs
1112

@@ -14,7 +15,8 @@ class GitHubAgent(Step, input_class=GitHubAgentInputs, output_class=GitHubAgentO
1415
def __init__(self, inputs):
1516
super().__init__(inputs)
1617
base_path = inputs.get("base_path", str(Path.cwd()))
17-
task = inputs["task"]
18+
data = inputs.get("prompt_value", {})
19+
task = mustache_render(inputs["task"], data)
1820
self.agentic_strategy = AgenticStrategyV2(
1921
model="claude-3-7-sonnet-latest",
2022
llm_client=AioLlmClient.create_aio_client(inputs),

patchwork/steps/GitHubAgent/typed.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
from patchwork.common.utils.step_typing import StepTypeConfig
44

55

6-
class GitHubAgentInputs(TypedDict, total=False):
6+
class __GitHubAgentRequiredInputs(TypedDict):
7+
task: str
8+
9+
class GitHubAgentInputs(__GitHubAgentRequiredInputs, total=False):
710
base_path: str
811
prompt_value: Dict[str, Any]
9-
system_prompt: str
10-
user_prompt: str
1112
max_llm_calls: Annotated[int, StepTypeConfig(is_config=True)]
1213
openai_api_key: Annotated[
1314
str, StepTypeConfig(is_config=True, or_op=["patched_api_key", "google_api_key", "anthropic_api_key"])
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.common.utils.utils import mustache_render
8+
from patchwork.step import Step
9+
10+
from .typed import ManageEngineAgentInputs, ManageEngineAgentOutputs
11+
12+
13+
class ManageEngineAgent(Step, input_class=ManageEngineAgentInputs, output_class=ManageEngineAgentOutputs):
14+
def __init__(self, inputs: dict):
15+
super().__init__(inputs)
16+
17+
if not inputs.get("me_access_token"):
18+
raise ValueError("me_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+
"Please summarise the conversation given and provide the result in the structure that is asked of you.",
29+
)
30+
31+
self.headers = {
32+
"Authorization": f"Zoho-oauthtoken {inputs.get('me_access_token')}",
33+
"Content-Type": "application/x-www-form-urlencoded",
34+
"Accept": "application/vnd.manageengine.sdp.v3+json",
35+
}
36+
37+
llm_client = AioLlmClient.create_aio_client(inputs)
38+
39+
# Configure agentic strategy with ManageEngine-specific context
40+
self.agentic_strategy = AgenticStrategyV2(
41+
model="claude-3-7-sonnet-latest",
42+
llm_client=llm_client,
43+
system_prompt_template=system_prompt,
44+
template_data={},
45+
user_prompt_template=mustache_render(inputs.get("user_prompt"), inputs.get("prompt_value")),
46+
agent_configs=[
47+
AgentConfig(
48+
name="ManageEngine Assistant",
49+
tool_set=dict(
50+
make_api_request=APIRequestTool(
51+
headers=self.headers,
52+
preprocess_data=lambda x: f"input_data={x}",
53+
)
54+
),
55+
system_prompt="""\
56+
You are an senior software developer helping the program manager to interact with ManageEngine ServiceDesk via the ServiceDeskPlus API.
57+
Your goal is to retrieve, create, or modify service desk tickets and related information.
58+
Use the `make_api_request` tool to interact with the ManageEngine API.
59+
Skip the headers for the api requests as they are already provided.
60+
The base url for the ServiceDeskPlus API is https://sdpondemand.manageengine.com/app/itdesk/api/v3
61+
62+
For modifying or creating data, the data should be a json string.
63+
When you have the result of the information user requested, return the response of the final result tool as is.
64+
""",
65+
)
66+
],
67+
example_json=inputs.get("example_json"),
68+
)
69+
70+
def run(self) -> dict:
71+
# Execute the agentic strategy
72+
result = self.agentic_strategy.execute(limit=self.conversation_limit)
73+
74+
# Return results with usage information
75+
return {**result, **self.agentic_strategy.usage()}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing_extensions import Annotated, Any, Dict, List, Optional, TypedDict
2+
3+
from patchwork.common.utils.step_typing import StepTypeConfig
4+
5+
6+
class __ManageEngineAgentInputsRequired(TypedDict):
7+
me_access_token: str
8+
user_prompt: str
9+
prompt_value: Dict[str, Any]
10+
11+
12+
class ManageEngineAgentInputs(__ManageEngineAgentInputsRequired, total=False):
13+
max_agent_calls: int
14+
openai_api_key: Annotated[str, StepTypeConfig(or_op=["google_api_key", "anthropic_api_key"])]
15+
anthropic_api_key: Annotated[str, StepTypeConfig(or_op=["google_api_key", "openai_api_key"])]
16+
google_api_key: Annotated[str, StepTypeConfig(or_op=["openai_api_key", "anthropic_api_key"])]
17+
18+
# Prompt and strategy configuration
19+
system_prompt: Optional[str]
20+
example_json: Optional[Dict]
21+
22+
23+
class ManageEngineAgentOutputs(TypedDict):
24+
conversation_history: List[Dict]
25+
tool_records: List[Dict]
26+
request_tokens: int
27+
response_tokens: int

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.ManageEngineAgent.ManageEngineAgent import ManageEngineAgent
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+
"ManageEngineAgent",
111113
]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "patchwork-cli"
3-
version = "0.0.106"
3+
version = "0.0.107"
44
description = ""
55
authors = ["patched.codes"]
66
license = "AGPL"

0 commit comments

Comments
 (0)