patchpal 0.4.5__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- patchpal/__init__.py +1 -1
- patchpal/agent.py +108 -9
- patchpal/cli.py +23 -2
- patchpal/tool_schema.py +288 -0
- patchpal/tools.py +17 -1
- {patchpal-0.4.5.dist-info → patchpal-0.6.0.dist-info}/METADATA +373 -5
- patchpal-0.6.0.dist-info/RECORD +15 -0
- patchpal-0.4.5.dist-info/RECORD +0 -14
- {patchpal-0.4.5.dist-info → patchpal-0.6.0.dist-info}/WHEEL +0 -0
- {patchpal-0.4.5.dist-info → patchpal-0.6.0.dist-info}/entry_points.txt +0 -0
- {patchpal-0.4.5.dist-info → patchpal-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {patchpal-0.4.5.dist-info → patchpal-0.6.0.dist-info}/top_level.txt +0 -0
patchpal/__init__.py
CHANGED
patchpal/agent.py
CHANGED
|
@@ -811,12 +811,17 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
|
|
|
811
811
|
class PatchPalAgent:
|
|
812
812
|
"""Simple agent that uses LiteLLM for tool calling."""
|
|
813
813
|
|
|
814
|
-
def __init__(self, model_id: str = "anthropic/claude-sonnet-4-5"):
|
|
814
|
+
def __init__(self, model_id: str = "anthropic/claude-sonnet-4-5", custom_tools=None):
|
|
815
815
|
"""Initialize the agent.
|
|
816
816
|
|
|
817
817
|
Args:
|
|
818
818
|
model_id: LiteLLM model identifier
|
|
819
|
+
custom_tools: Optional list of Python functions to add as tools
|
|
819
820
|
"""
|
|
821
|
+
# Store custom tools
|
|
822
|
+
self.custom_tools = custom_tools or []
|
|
823
|
+
self.custom_tool_funcs = {func.__name__: func for func in self.custom_tools}
|
|
824
|
+
|
|
820
825
|
# Convert ollama/ to ollama_chat/ for LiteLLM compatibility
|
|
821
826
|
if model_id.startswith("ollama/"):
|
|
822
827
|
model_id = model_id.replace("ollama/", "ollama_chat/", 1)
|
|
@@ -1029,6 +1034,67 @@ class PatchPalAgent:
|
|
|
1029
1034
|
if self.enable_auto_compact and self.context_manager.needs_compaction(self.messages):
|
|
1030
1035
|
self._perform_auto_compaction()
|
|
1031
1036
|
|
|
1037
|
+
# Agent loop with interrupt handling
|
|
1038
|
+
try:
|
|
1039
|
+
return self._run_agent_loop(max_iterations)
|
|
1040
|
+
except KeyboardInterrupt:
|
|
1041
|
+
# Clean up conversation state if interrupted mid-execution
|
|
1042
|
+
self._cleanup_interrupted_state()
|
|
1043
|
+
raise # Re-raise so CLI can handle it
|
|
1044
|
+
|
|
1045
|
+
def _cleanup_interrupted_state(self):
|
|
1046
|
+
"""Clean up conversation state after KeyboardInterrupt.
|
|
1047
|
+
|
|
1048
|
+
If the last message is an assistant message with tool_calls but no
|
|
1049
|
+
corresponding tool responses, we need to either remove the message
|
|
1050
|
+
or add error responses to maintain valid conversation structure.
|
|
1051
|
+
"""
|
|
1052
|
+
if not self.messages:
|
|
1053
|
+
return
|
|
1054
|
+
|
|
1055
|
+
last_msg = self.messages[-1]
|
|
1056
|
+
|
|
1057
|
+
# Check if last message is assistant with tool_calls
|
|
1058
|
+
if last_msg.get("role") == "assistant" and last_msg.get("tool_calls"):
|
|
1059
|
+
tool_calls = last_msg["tool_calls"]
|
|
1060
|
+
|
|
1061
|
+
# Check if we have tool responses for all tool_calls
|
|
1062
|
+
tool_call_ids = {tc.id for tc in tool_calls}
|
|
1063
|
+
|
|
1064
|
+
# Look for tool responses after this assistant message
|
|
1065
|
+
# (should be immediately following, but scan to be safe)
|
|
1066
|
+
response_ids = set()
|
|
1067
|
+
for msg in self.messages[self.messages.index(last_msg) + 1 :]:
|
|
1068
|
+
if msg.get("role") == "tool":
|
|
1069
|
+
response_ids.add(msg.get("tool_call_id"))
|
|
1070
|
+
|
|
1071
|
+
# If we're missing responses, add error responses for all tool calls
|
|
1072
|
+
if tool_call_ids != response_ids:
|
|
1073
|
+
missing_ids = tool_call_ids - response_ids
|
|
1074
|
+
|
|
1075
|
+
# Add error tool responses for the missing tool calls
|
|
1076
|
+
for tool_call in tool_calls:
|
|
1077
|
+
if tool_call.id in missing_ids:
|
|
1078
|
+
self.messages.append(
|
|
1079
|
+
{
|
|
1080
|
+
"role": "tool",
|
|
1081
|
+
"tool_call_id": tool_call.id,
|
|
1082
|
+
"name": tool_call.function.name,
|
|
1083
|
+
"content": "Error: Operation interrupted by user (Ctrl-C)",
|
|
1084
|
+
}
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
def _run_agent_loop(self, max_iterations: int) -> str:
|
|
1088
|
+
"""Internal method that runs the agent loop.
|
|
1089
|
+
|
|
1090
|
+
Separated from run() to enable proper interrupt handling.
|
|
1091
|
+
|
|
1092
|
+
Args:
|
|
1093
|
+
max_iterations: Maximum number of agent iterations
|
|
1094
|
+
|
|
1095
|
+
Returns:
|
|
1096
|
+
The agent's final response
|
|
1097
|
+
"""
|
|
1032
1098
|
# Agent loop
|
|
1033
1099
|
for iteration in range(max_iterations):
|
|
1034
1100
|
# Show thinking message
|
|
@@ -1042,10 +1108,18 @@ class PatchPalAgent:
|
|
|
1042
1108
|
|
|
1043
1109
|
# Use LiteLLM for all providers
|
|
1044
1110
|
try:
|
|
1111
|
+
# Build tool list (built-in + custom)
|
|
1112
|
+
tools = list(TOOLS)
|
|
1113
|
+
if self.custom_tools:
|
|
1114
|
+
from patchpal.tool_schema import function_to_tool_schema
|
|
1115
|
+
|
|
1116
|
+
for func in self.custom_tools:
|
|
1117
|
+
tools.append(function_to_tool_schema(func))
|
|
1118
|
+
|
|
1045
1119
|
response = litellm.completion(
|
|
1046
1120
|
model=self.model_id,
|
|
1047
1121
|
messages=messages,
|
|
1048
|
-
tools=
|
|
1122
|
+
tools=tools,
|
|
1049
1123
|
tool_choice="auto",
|
|
1050
1124
|
**self.litellm_kwargs,
|
|
1051
1125
|
)
|
|
@@ -1099,15 +1173,25 @@ class PatchPalAgent:
|
|
|
1099
1173
|
tool_result = f"Error: Invalid JSON arguments for {tool_name}"
|
|
1100
1174
|
print(f"\033[1;31m✗ {tool_name}: Invalid arguments\033[0m")
|
|
1101
1175
|
else:
|
|
1102
|
-
# Get the tool function
|
|
1103
|
-
tool_func =
|
|
1176
|
+
# Get the tool function (check custom tools first, then built-in)
|
|
1177
|
+
tool_func = self.custom_tool_funcs.get(tool_name) or TOOL_FUNCTIONS.get(
|
|
1178
|
+
tool_name
|
|
1179
|
+
)
|
|
1104
1180
|
if tool_func is None:
|
|
1105
1181
|
tool_result = f"Error: Unknown tool {tool_name}"
|
|
1106
1182
|
print(f"\033[1;31m✗ Unknown tool: {tool_name}\033[0m")
|
|
1107
1183
|
else:
|
|
1108
1184
|
# Show tool call message
|
|
1109
|
-
|
|
1110
|
-
|
|
1185
|
+
if tool_name in self.custom_tool_funcs:
|
|
1186
|
+
# Custom tool - show generic message with args
|
|
1187
|
+
args_preview = str(tool_args)[:60]
|
|
1188
|
+
if len(str(tool_args)) > 60:
|
|
1189
|
+
args_preview += "..."
|
|
1190
|
+
print(
|
|
1191
|
+
f"\033[2m🔧 {tool_name}({args_preview})\033[0m",
|
|
1192
|
+
flush=True,
|
|
1193
|
+
)
|
|
1194
|
+
elif tool_name == "read_file":
|
|
1111
1195
|
print(
|
|
1112
1196
|
f"\033[2m📖 Reading: {tool_args.get('path', '')}\033[0m",
|
|
1113
1197
|
flush=True,
|
|
@@ -1250,7 +1334,7 @@ class PatchPalAgent:
|
|
|
1250
1334
|
tool_result = tool_func(**filtered_args)
|
|
1251
1335
|
except Exception as e:
|
|
1252
1336
|
tool_result = f"Error executing {tool_name}: {e}"
|
|
1253
|
-
print(f"\033[1;31m✗ {
|
|
1337
|
+
print(f"\033[1;31m✗ {tool_name}: {e}\033[0m")
|
|
1254
1338
|
|
|
1255
1339
|
# Add tool result to messages
|
|
1256
1340
|
self.messages.append(
|
|
@@ -1299,18 +1383,33 @@ class PatchPalAgent:
|
|
|
1299
1383
|
)
|
|
1300
1384
|
|
|
1301
1385
|
|
|
1302
|
-
def create_agent(model_id: str = "anthropic/claude-sonnet-4-5") -> PatchPalAgent:
|
|
1386
|
+
def create_agent(model_id: str = "anthropic/claude-sonnet-4-5", custom_tools=None) -> PatchPalAgent:
|
|
1303
1387
|
"""Create and return a PatchPal agent.
|
|
1304
1388
|
|
|
1305
1389
|
Args:
|
|
1306
1390
|
model_id: LiteLLM model identifier (default: anthropic/claude-sonnet-4-5)
|
|
1391
|
+
custom_tools: Optional list of Python functions to use as custom tools.
|
|
1392
|
+
Each function should have type hints and a docstring.
|
|
1307
1393
|
|
|
1308
1394
|
Returns:
|
|
1309
1395
|
A configured PatchPalAgent instance
|
|
1396
|
+
|
|
1397
|
+
Example:
|
|
1398
|
+
def calculator(x: int, y: int) -> str:
|
|
1399
|
+
'''Add two numbers.
|
|
1400
|
+
|
|
1401
|
+
Args:
|
|
1402
|
+
x: First number
|
|
1403
|
+
y: Second number
|
|
1404
|
+
'''
|
|
1405
|
+
return str(x + y)
|
|
1406
|
+
|
|
1407
|
+
agent = create_agent(custom_tools=[calculator])
|
|
1408
|
+
response = agent.run("What's 5 + 3?")
|
|
1310
1409
|
"""
|
|
1311
1410
|
# Reset session todos for new session
|
|
1312
1411
|
from patchpal.tools import reset_session_todos
|
|
1313
1412
|
|
|
1314
1413
|
reset_session_todos()
|
|
1315
1414
|
|
|
1316
|
-
return PatchPalAgent(model_id=model_id)
|
|
1415
|
+
return PatchPalAgent(model_id=model_id, custom_tools=custom_tools)
|
patchpal/cli.py
CHANGED
|
@@ -211,9 +211,26 @@ Supported models: Any LiteLLM-supported model
|
|
|
211
211
|
# Determine model to use (priority: CLI arg > env var > default)
|
|
212
212
|
model_id = args.model or os.getenv("PATCHPAL_MODEL") or "anthropic/claude-sonnet-4-5"
|
|
213
213
|
|
|
214
|
-
#
|
|
214
|
+
# Discover custom tools from ~/.patchpal/tools/
|
|
215
|
+
from patchpal.tool_schema import discover_tools, list_custom_tools
|
|
216
|
+
|
|
217
|
+
custom_tools = discover_tools()
|
|
218
|
+
|
|
219
|
+
# Show custom tools info if any were loaded
|
|
220
|
+
custom_tool_info = list_custom_tools()
|
|
221
|
+
if custom_tool_info:
|
|
222
|
+
tool_names = [name for name, _, _ in custom_tool_info]
|
|
223
|
+
tools_str = ", ".join(tool_names)
|
|
224
|
+
# Store for later display (after model info)
|
|
225
|
+
custom_tools_message = (
|
|
226
|
+
f"\033[1;36m🔧 Loaded {len(custom_tool_info)} custom tool(s): {tools_str}\033[0m"
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
custom_tools_message = None
|
|
230
|
+
|
|
231
|
+
# Create the agent with the specified model and custom tools
|
|
215
232
|
# LiteLLM will handle API key validation and provide appropriate error messages
|
|
216
|
-
agent = create_agent(model_id=model_id)
|
|
233
|
+
agent = create_agent(model_id=model_id, custom_tools=custom_tools)
|
|
217
234
|
|
|
218
235
|
# Get max iterations from environment variable or use default
|
|
219
236
|
max_iterations = int(os.getenv("PATCHPAL_MAX_ITERATIONS", "100"))
|
|
@@ -238,6 +255,10 @@ Supported models: Any LiteLLM-supported model
|
|
|
238
255
|
print("=" * 80)
|
|
239
256
|
print(f"\nUsing model: {model_id}")
|
|
240
257
|
|
|
258
|
+
# Show custom tools info if any were loaded
|
|
259
|
+
if custom_tools_message:
|
|
260
|
+
print(custom_tools_message)
|
|
261
|
+
|
|
241
262
|
# Show require-permission-for-all indicator if active
|
|
242
263
|
if args.require_permission_for_all:
|
|
243
264
|
print("\033[1;33m🔒 Permission required for ALL operations (including reads)\033[0m")
|
patchpal/tool_schema.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Utility to automatically convert Python functions to LiteLLM tool schemas.
|
|
2
|
+
|
|
3
|
+
Also provides custom tools discovery system for loading user-defined tools
|
|
4
|
+
from ~/.patchpal/tools/
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
import sys
|
|
9
|
+
from importlib import util
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Union, get_args, get_origin, get_type_hints
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def python_type_to_json_schema(py_type: Any) -> Dict[str, Any]:
|
|
15
|
+
"""Convert Python type hint to JSON schema type.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
py_type: Python type hint
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
JSON schema type dict
|
|
22
|
+
"""
|
|
23
|
+
if py_type is type(None):
|
|
24
|
+
return {"type": "null"}
|
|
25
|
+
|
|
26
|
+
origin = get_origin(py_type)
|
|
27
|
+
|
|
28
|
+
# Handle Optional/Union types
|
|
29
|
+
if origin is Union:
|
|
30
|
+
args = get_args(py_type)
|
|
31
|
+
non_none = [a for a in args if a is not type(None)]
|
|
32
|
+
if non_none:
|
|
33
|
+
return python_type_to_json_schema(non_none[0])
|
|
34
|
+
|
|
35
|
+
# Handle List
|
|
36
|
+
if origin is list:
|
|
37
|
+
args = get_args(py_type)
|
|
38
|
+
if args:
|
|
39
|
+
return {"type": "array", "items": python_type_to_json_schema(args[0])}
|
|
40
|
+
return {"type": "array"}
|
|
41
|
+
|
|
42
|
+
# Handle Dict
|
|
43
|
+
if origin is dict:
|
|
44
|
+
return {"type": "object"}
|
|
45
|
+
|
|
46
|
+
# Basic types
|
|
47
|
+
type_map = {
|
|
48
|
+
str: {"type": "string"},
|
|
49
|
+
int: {"type": "integer"},
|
|
50
|
+
float: {"type": "number"},
|
|
51
|
+
bool: {"type": "boolean"},
|
|
52
|
+
list: {"type": "array"},
|
|
53
|
+
dict: {"type": "object"},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return type_map.get(py_type, {"type": "string"})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_docstring_params(docstring: str) -> Dict[str, str]:
|
|
60
|
+
"""Parse parameter descriptions from Google-style docstring.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
docstring: Function docstring
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dict mapping parameter names to descriptions
|
|
67
|
+
"""
|
|
68
|
+
if not docstring:
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
params = {}
|
|
72
|
+
lines = docstring.split("\n")
|
|
73
|
+
in_args = False
|
|
74
|
+
|
|
75
|
+
for i, line in enumerate(lines):
|
|
76
|
+
stripped = line.strip()
|
|
77
|
+
|
|
78
|
+
if stripped.lower() in ("args:", "arguments:", "parameters:"):
|
|
79
|
+
in_args = True
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if in_args:
|
|
83
|
+
# Check if we left the Args section
|
|
84
|
+
if stripped and not line.startswith((" ", "\t")) and ":" in stripped:
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
# Parse "param_name: description"
|
|
88
|
+
if ":" in stripped:
|
|
89
|
+
parts = stripped.split(":", 1)
|
|
90
|
+
param_name = parts[0].strip()
|
|
91
|
+
description = parts[1].strip()
|
|
92
|
+
|
|
93
|
+
# Collect continuation lines
|
|
94
|
+
for j in range(i + 1, len(lines)):
|
|
95
|
+
next_line = lines[j].strip()
|
|
96
|
+
if not next_line or ":" in next_line:
|
|
97
|
+
break
|
|
98
|
+
description += " " + next_line
|
|
99
|
+
|
|
100
|
+
params[param_name] = description
|
|
101
|
+
|
|
102
|
+
return params
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def function_to_tool_schema(func: Callable) -> Dict[str, Any]:
|
|
106
|
+
"""Convert a Python function to LiteLLM tool schema.
|
|
107
|
+
|
|
108
|
+
Extracts schema from function signature and docstring.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
func: Python function with type hints and docstring
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
LiteLLM tool schema dict
|
|
115
|
+
"""
|
|
116
|
+
sig = inspect.signature(func)
|
|
117
|
+
docstring = inspect.getdoc(func) or ""
|
|
118
|
+
|
|
119
|
+
# Extract description (first paragraph)
|
|
120
|
+
description = (
|
|
121
|
+
docstring.split("\n\n")[0].replace("\n", " ").strip() or f"Execute {func.__name__}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Parse parameter descriptions
|
|
125
|
+
param_descriptions = parse_docstring_params(docstring)
|
|
126
|
+
|
|
127
|
+
# Get type hints
|
|
128
|
+
try:
|
|
129
|
+
type_hints = get_type_hints(func)
|
|
130
|
+
except Exception:
|
|
131
|
+
type_hints = {}
|
|
132
|
+
|
|
133
|
+
# Build parameters
|
|
134
|
+
properties = {}
|
|
135
|
+
required = []
|
|
136
|
+
|
|
137
|
+
for param_name, param in sig.parameters.items():
|
|
138
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
param_type = type_hints.get(param_name, str)
|
|
142
|
+
param_schema = python_type_to_json_schema(param_type)
|
|
143
|
+
param_schema["description"] = param_descriptions.get(param_name, f"Parameter {param_name}")
|
|
144
|
+
|
|
145
|
+
properties[param_name] = param_schema
|
|
146
|
+
|
|
147
|
+
if param.default is inspect.Parameter.empty:
|
|
148
|
+
required.append(param_name)
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
"type": "function",
|
|
152
|
+
"function": {
|
|
153
|
+
"name": func.__name__,
|
|
154
|
+
"description": description,
|
|
155
|
+
"parameters": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"properties": properties,
|
|
158
|
+
"required": required,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _is_valid_tool_function(func: Callable) -> bool:
|
|
165
|
+
"""Check if a function is valid for use as a tool.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
func: Function to validate
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
True if function can be used as a tool
|
|
172
|
+
"""
|
|
173
|
+
# Must have a docstring
|
|
174
|
+
if not func.__doc__:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# Must have type hints
|
|
178
|
+
try:
|
|
179
|
+
sig = inspect.signature(func)
|
|
180
|
+
for param_name, param in sig.parameters.items():
|
|
181
|
+
# Skip *args, **kwargs
|
|
182
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
183
|
+
continue
|
|
184
|
+
# Check if parameter has annotation
|
|
185
|
+
if param.annotation is inspect.Parameter.empty:
|
|
186
|
+
return False
|
|
187
|
+
except Exception:
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def discover_tools(tools_dir: Optional[Path] = None) -> List[Callable]:
|
|
194
|
+
"""Discover custom tool functions from Python files.
|
|
195
|
+
|
|
196
|
+
Loads all .py files from the tools directory and extracts functions
|
|
197
|
+
that have proper type hints and docstrings.
|
|
198
|
+
|
|
199
|
+
Tool functions must:
|
|
200
|
+
- Have type hints for all parameters
|
|
201
|
+
- Have a docstring with description and Args section
|
|
202
|
+
- Be defined at module level (not nested)
|
|
203
|
+
- Not start with underscore (private functions ignored)
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
tools_dir: Directory to search for tool files (default: ~/.patchpal/tools/)
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of callable tool functions
|
|
210
|
+
"""
|
|
211
|
+
if tools_dir is None:
|
|
212
|
+
tools_dir = Path.home() / ".patchpal" / "tools"
|
|
213
|
+
|
|
214
|
+
if not tools_dir.exists():
|
|
215
|
+
return []
|
|
216
|
+
|
|
217
|
+
tools = []
|
|
218
|
+
loaded_modules = []
|
|
219
|
+
|
|
220
|
+
# Discover all .py files
|
|
221
|
+
for tool_file in sorted(tools_dir.glob("*.py")):
|
|
222
|
+
try:
|
|
223
|
+
# Create a unique module name to avoid conflicts
|
|
224
|
+
module_name = f"patchpal_custom_tools.{tool_file.stem}"
|
|
225
|
+
|
|
226
|
+
# Load the module
|
|
227
|
+
spec = util.spec_from_file_location(module_name, tool_file)
|
|
228
|
+
if spec and spec.loader:
|
|
229
|
+
module = util.module_from_spec(spec)
|
|
230
|
+
|
|
231
|
+
# Store reference to prevent garbage collection
|
|
232
|
+
sys.modules[module_name] = module
|
|
233
|
+
loaded_modules.append(module)
|
|
234
|
+
|
|
235
|
+
# Execute the module
|
|
236
|
+
spec.loader.exec_module(module)
|
|
237
|
+
|
|
238
|
+
# Extract valid tool functions
|
|
239
|
+
for name, obj in inspect.getmembers(module, inspect.isfunction):
|
|
240
|
+
# Skip private functions
|
|
241
|
+
if name.startswith("_"):
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# Skip functions from imports (only module-level definitions)
|
|
245
|
+
if obj.__module__ != module_name:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
# Validate tool function
|
|
249
|
+
if _is_valid_tool_function(obj):
|
|
250
|
+
tools.append(obj)
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
# Print warning but continue with other tools
|
|
254
|
+
print(
|
|
255
|
+
f"\033[1;33m⚠️ Warning: Failed to load custom tool from {tool_file.name}: {e}\033[0m"
|
|
256
|
+
)
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
return tools
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def list_custom_tools(tools_dir: Optional[Path] = None) -> List[tuple[str, str, Path]]:
|
|
263
|
+
"""List all custom tools with their descriptions.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
tools_dir: Directory to search for tool files (default: ~/.patchpal/tools/)
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List of (tool_name, description, file_path) tuples
|
|
270
|
+
"""
|
|
271
|
+
tools = discover_tools(tools_dir)
|
|
272
|
+
|
|
273
|
+
result = []
|
|
274
|
+
for tool in tools:
|
|
275
|
+
# Extract description from docstring (first line)
|
|
276
|
+
description = ""
|
|
277
|
+
if tool.__doc__:
|
|
278
|
+
description = tool.__doc__.split("\n")[0].strip()
|
|
279
|
+
|
|
280
|
+
# Get source file
|
|
281
|
+
try:
|
|
282
|
+
source_file = Path(inspect.getfile(tool))
|
|
283
|
+
except Exception:
|
|
284
|
+
source_file = Path("unknown")
|
|
285
|
+
|
|
286
|
+
result.append((tool.__name__, description, source_file))
|
|
287
|
+
|
|
288
|
+
return result
|
patchpal/tools.py
CHANGED
|
@@ -2521,8 +2521,24 @@ def web_search(query: str, max_results: int = 5) -> str:
|
|
|
2521
2521
|
max_results = min(max_results, 10)
|
|
2522
2522
|
|
|
2523
2523
|
try:
|
|
2524
|
+
# Determine SSL verification setting
|
|
2525
|
+
# Priority: PATCHPAL_VERIFY_SSL env var > SSL_CERT_FILE > REQUESTS_CA_BUNDLE > default True
|
|
2526
|
+
verify_ssl = os.getenv("PATCHPAL_VERIFY_SSL")
|
|
2527
|
+
if verify_ssl is not None:
|
|
2528
|
+
# User explicitly set PATCHPAL_VERIFY_SSL
|
|
2529
|
+
if verify_ssl.lower() in ("false", "0", "no"):
|
|
2530
|
+
verify = False
|
|
2531
|
+
elif verify_ssl.lower() in ("true", "1", "yes"):
|
|
2532
|
+
verify = True
|
|
2533
|
+
else:
|
|
2534
|
+
# Treat as path to CA bundle
|
|
2535
|
+
verify = verify_ssl
|
|
2536
|
+
else:
|
|
2537
|
+
# Use SSL_CERT_FILE or REQUESTS_CA_BUNDLE if set (for corporate environments)
|
|
2538
|
+
verify = os.getenv("SSL_CERT_FILE") or os.getenv("REQUESTS_CA_BUNDLE") or True
|
|
2539
|
+
|
|
2524
2540
|
# Perform search using DuckDuckGo
|
|
2525
|
-
with DDGS() as ddgs:
|
|
2541
|
+
with DDGS(verify=verify) as ddgs:
|
|
2526
2542
|
results = list(ddgs.text(query, max_results=max_results))
|
|
2527
2543
|
|
|
2528
2544
|
if not results:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchpal
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A lean Claude Code clone in pure Python
|
|
5
5
|
Author: PatchPal Contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -46,6 +46,18 @@ Dynamic: license-file
|
|
|
46
46
|
|
|
47
47
|
A key goal of this project is to approximate Claude Code's core functionality while remaining lean, accessible, and configurable, enabling learning, experimentation, and broad applicability across use cases.
|
|
48
48
|
|
|
49
|
+
```bash
|
|
50
|
+
$ls ./patchpal
|
|
51
|
+
__init__.py agent.py cli.py context.py permissions.py skills.py system_prompt.md tool_schema.py tools.py
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
$ pip install patchpal # install
|
|
58
|
+
$ patchpal # start
|
|
59
|
+
```
|
|
60
|
+
|
|
49
61
|
## Table of Contents
|
|
50
62
|
|
|
51
63
|
- [Installation](https://github.com/amaiya/patchpal?tab=readme-ov-file#installation)
|
|
@@ -58,12 +70,14 @@ A key goal of this project is to approximate Claude Code's core functionality wh
|
|
|
58
70
|
- [Git Operations](https://github.com/amaiya/patchpal?tab=readme-ov-file#git-operations-no-permission-required)
|
|
59
71
|
- [Web Capabilities](https://github.com/amaiya/patchpal?tab=readme-ov-file#web-capabilities-requires-permission)
|
|
60
72
|
- [Skills System](https://github.com/amaiya/patchpal?tab=readme-ov-file#skills-system)
|
|
73
|
+
- [Custom Tools](https://github.com/amaiya/patchpal?tab=readme-ov-file#custom-tools)
|
|
61
74
|
- [Model Configuration](https://github.com/amaiya/patchpal?tab=readme-ov-file#model-configuration)
|
|
62
75
|
- [Supported Models](https://github.com/amaiya/patchpal?tab=readme-ov-file#supported-models)
|
|
63
76
|
- [Using Local Models (vLLM & Ollama)](https://github.com/amaiya/patchpal?tab=readme-ov-file#using-local-models-vllm--ollama)
|
|
64
77
|
- [Air-Gapped and Offline Environments](https://github.com/amaiya/patchpal?tab=readme-ov-file#air-gapped-and-offline-environments)
|
|
65
78
|
- [Maximum Security Mode](https://github.com/amaiya/patchpal?tab=readme-ov-file#maximum-security-mode)
|
|
66
79
|
- [Usage](https://github.com/amaiya/patchpal?tab=readme-ov-file#usage)
|
|
80
|
+
- [Python API](https://github.com/amaiya/patchpal?tab=readme-ov-file#python-api)
|
|
67
81
|
- [Configuration](https://github.com/amaiya/patchpal?tab=readme-ov-file#configuration)
|
|
68
82
|
- [Example Tasks](https://github.com/amaiya/patchpal?tab=readme-ov-file#example-tasks)
|
|
69
83
|
- [Safety](https://github.com/amaiya/patchpal?tab=readme-ov-file#safety)
|
|
@@ -71,10 +85,6 @@ A key goal of this project is to approximate Claude Code's core functionality wh
|
|
|
71
85
|
- [Troubleshooting](https://github.com/amaiya/patchpal?tab=readme-ov-file#troubleshooting)
|
|
72
86
|
|
|
73
87
|
|
|
74
|
-
```bash
|
|
75
|
-
$ls ./patchpal
|
|
76
|
-
__init__.py agent.py cli.py context.py permissions.py skills.py system_prompt.md tools.py
|
|
77
|
-
```
|
|
78
88
|
|
|
79
89
|
## Installation
|
|
80
90
|
|
|
@@ -307,6 +317,197 @@ You: list skills
|
|
|
307
317
|
|
|
308
318
|
Project skills (`.patchpal/skills/`) override personal skills (`~/.patchpal/skills/`) with the same name.
|
|
309
319
|
|
|
320
|
+
### Custom Tools
|
|
321
|
+
|
|
322
|
+
Custom tools extend PatchPal's capabilities by adding new Python functions that the agent can call. Unlike skills (which are prompt-based workflows), custom tools are executable Python code that the agent invokes automatically when needed.
|
|
323
|
+
|
|
324
|
+
**Key Differences:**
|
|
325
|
+
- **Skills**: Markdown files with instructions for the agent to follow
|
|
326
|
+
- **Custom Tools**: Python functions that execute code and return results
|
|
327
|
+
|
|
328
|
+
**Installation:**
|
|
329
|
+
|
|
330
|
+
1. **Create the tools directory:**
|
|
331
|
+
```bash
|
|
332
|
+
mkdir -p ~/.patchpal/tools
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
2. **Copy the example tools (or create your own):**
|
|
336
|
+
```bash
|
|
337
|
+
# After pip install patchpal, get the example tools
|
|
338
|
+
curl -L https://github.com/amaiya/patchpal/archive/main.tar.gz | tar xz --strip=1 patchpal-main/examples
|
|
339
|
+
|
|
340
|
+
# Copy to your tools directory
|
|
341
|
+
cp examples/tools/calculator.py ~/.patchpal/tools/
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
3. **Start PatchPal - tools are loaded automatically:**
|
|
345
|
+
```bash
|
|
346
|
+
$ patchpal
|
|
347
|
+
================================================================================
|
|
348
|
+
PatchPal - Claude Code–inspired coding and automation assistant
|
|
349
|
+
================================================================================
|
|
350
|
+
|
|
351
|
+
Using model: anthropic/claude-sonnet-4-5
|
|
352
|
+
🔧 Loaded 7 custom tool(s): add, subtract, multiply, divide, calculate_percentage, fahrenheit_to_celsius, celsius_to_fahrenheit
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Creating Custom Tools:**
|
|
356
|
+
|
|
357
|
+
Custom tools are Python functions with specific requirements:
|
|
358
|
+
|
|
359
|
+
**Requirements:**
|
|
360
|
+
1. **Type hints** for all parameters
|
|
361
|
+
2. **Docstring** with description and Args section (Google-style)
|
|
362
|
+
3. **Module-level** functions (not nested inside classes)
|
|
363
|
+
4. **Return type** should typically be `str` (for LLM consumption)
|
|
364
|
+
5. Function names **cannot start with underscore** (private functions ignored)
|
|
365
|
+
|
|
366
|
+
**Example:**
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
# ~/.patchpal/tools/my_tools.py
|
|
370
|
+
|
|
371
|
+
def add(x: int, y: int) -> str:
|
|
372
|
+
"""Add two numbers together.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
x: First number
|
|
376
|
+
y: Second number
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
The sum as a string
|
|
380
|
+
"""
|
|
381
|
+
result = x + y
|
|
382
|
+
return f"{x} + {y} = {result}"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
|
|
386
|
+
"""Convert between currencies.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
amount: Amount to convert
|
|
390
|
+
from_currency: Source currency code (e.g., USD)
|
|
391
|
+
to_currency: Target currency code (e.g., EUR)
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Converted amount as a string
|
|
395
|
+
"""
|
|
396
|
+
# Your implementation here (API call, etc.)
|
|
397
|
+
# This is just a simple example
|
|
398
|
+
rates = {"USD": 1.0, "EUR": 0.85, "GBP": 0.73}
|
|
399
|
+
usd_amount = amount / rates.get(from_currency, 1.0)
|
|
400
|
+
result = usd_amount * rates.get(to_currency, 1.0)
|
|
401
|
+
return f"{amount} {from_currency} = {result:.2f} {to_currency}"
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Using Custom Tools:**
|
|
405
|
+
|
|
406
|
+
Once loaded, the agent calls your custom tools automatically:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
You: What's 15 + 27?
|
|
410
|
+
Agent: [Calls the add tool]
|
|
411
|
+
15 + 27 = 42
|
|
412
|
+
|
|
413
|
+
You: Convert 100 USD to EUR
|
|
414
|
+
Agent: [Calls convert_currency tool]
|
|
415
|
+
100 USD = 85.00 EUR
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**Tool Discovery:**
|
|
419
|
+
|
|
420
|
+
PatchPal discovers tools from `~/.patchpal/tools/*.py` at startup. All `.py` files are scanned for valid tool functions.
|
|
421
|
+
|
|
422
|
+
**What Gets Loaded:**
|
|
423
|
+
- ✅ Functions with type hints and docstrings
|
|
424
|
+
- ✅ Multiple functions per file
|
|
425
|
+
- ✅ Files can import standard libraries
|
|
426
|
+
- ❌ Functions without type hints
|
|
427
|
+
- ❌ Functions without docstrings
|
|
428
|
+
- ❌ Private functions (starting with `_`)
|
|
429
|
+
- ❌ Imported functions (must be defined in the file)
|
|
430
|
+
|
|
431
|
+
**Example Tools:**
|
|
432
|
+
|
|
433
|
+
The repository includes [example tools](https://github.com/amaiya/patchpal/tree/main/examples/tools):
|
|
434
|
+
- **calculator.py**: Basic arithmetic (add, subtract, multiply, divide), temperature conversion, percentage calculations
|
|
435
|
+
- Demonstrates different numeric types (int, float)
|
|
436
|
+
- Shows proper formatting of results for LLM consumption
|
|
437
|
+
- Examples: `add`, `subtract`, `multiply`, `divide`, `calculate_percentage`, `fahrenheit_to_celsius`
|
|
438
|
+
|
|
439
|
+
View the [examples/tools/](https://github.com/amaiya/patchpal/tree/main/examples/tools) directory for complete examples and a detailed README.
|
|
440
|
+
|
|
441
|
+
**Security Note:**
|
|
442
|
+
|
|
443
|
+
⚠️ Custom tools execute arbitrary Python code on your system. Only install tools from sources you trust.
|
|
444
|
+
|
|
445
|
+
- Tools are only loaded from `~/.patchpal/tools/` (your home directory)
|
|
446
|
+
- Project-local tools (`.patchpal/tools/`) are **not supported** for security
|
|
447
|
+
- This prevents accidental execution of untrusted code from repositories
|
|
448
|
+
|
|
449
|
+
**Advanced Features:**
|
|
450
|
+
|
|
451
|
+
**Optional Parameters:**
|
|
452
|
+
```python
|
|
453
|
+
from typing import Optional
|
|
454
|
+
|
|
455
|
+
def greet(name: str, greeting: Optional[str] = "Hello") -> str:
|
|
456
|
+
"""Greet someone.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
name: Person's name
|
|
460
|
+
greeting: Optional greeting message (default: "Hello")
|
|
461
|
+
"""
|
|
462
|
+
return f"{greeting}, {name}!"
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Complex Types:**
|
|
466
|
+
```python
|
|
467
|
+
from typing import List
|
|
468
|
+
|
|
469
|
+
def sum_numbers(numbers: List[int]) -> str:
|
|
470
|
+
"""Sum a list of numbers.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
numbers: List of integers to sum
|
|
474
|
+
"""
|
|
475
|
+
total = sum(numbers)
|
|
476
|
+
return f"Sum of {numbers} = {total}"
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
**Python API:**
|
|
480
|
+
|
|
481
|
+
Custom tools can also be used programmatically:
|
|
482
|
+
|
|
483
|
+
```python
|
|
484
|
+
from patchpal.agent import create_agent
|
|
485
|
+
|
|
486
|
+
def calculator(x: int, y: int) -> str:
|
|
487
|
+
"""Add two numbers.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
x: First number
|
|
491
|
+
y: Second number
|
|
492
|
+
"""
|
|
493
|
+
return str(x + y)
|
|
494
|
+
|
|
495
|
+
# Create agent with custom tools
|
|
496
|
+
agent = create_agent(custom_tools=[calculator])
|
|
497
|
+
response = agent.run("What's 5 + 3?")
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
See the [Python API](https://github.com/amaiya/patchpal?tab=readme-ov-file#python-api) section for more details.
|
|
501
|
+
|
|
502
|
+
**Troubleshooting:**
|
|
503
|
+
|
|
504
|
+
If tools aren't loading:
|
|
505
|
+
1. Check the file has a `.py` extension
|
|
506
|
+
2. Ensure functions have type hints for all parameters
|
|
507
|
+
3. Verify docstrings follow Google style (with Args: section)
|
|
508
|
+
4. Look for warning messages when starting PatchPal
|
|
509
|
+
5. Test the function directly in Python to check for syntax errors
|
|
510
|
+
|
|
310
511
|
## Model Configuration
|
|
311
512
|
|
|
312
513
|
PatchPal supports any LiteLLM-compatible model. You can configure the model in three ways (in order of priority):
|
|
@@ -668,6 +869,163 @@ The agent will process your request and show you the results. You can continue w
|
|
|
668
869
|
- **Interrupt Agent**: Press `Ctrl-C` during agent execution to stop the current task without exiting PatchPal
|
|
669
870
|
- **Exit**: Type `exit`, `quit`, or press `Ctrl-C` at the prompt to exit PatchPal
|
|
670
871
|
|
|
872
|
+
## Python API
|
|
873
|
+
|
|
874
|
+
PatchPal can be used programmatically from Python scripts or a REPL, giving you full agent capabilities with a simple API. **Unlike fully autonomous agent frameworks, PatchPal is designed for human-in-the-loop workflows** where users maintain control through interactive permission prompts, making it ideal for code assistance, debugging, and automation tasks that benefit from human oversight.
|
|
875
|
+
|
|
876
|
+
**Basic Usage:**
|
|
877
|
+
|
|
878
|
+
```python
|
|
879
|
+
from patchpal.agent import create_agent
|
|
880
|
+
|
|
881
|
+
# Create an agent (uses default model or PATCHPAL_MODEL env var)
|
|
882
|
+
agent = create_agent()
|
|
883
|
+
|
|
884
|
+
# Or specify a model explicitly
|
|
885
|
+
agent = create_agent(model_id="anthropic/claude-sonnet-4_5")
|
|
886
|
+
|
|
887
|
+
# Run the agent on a task
|
|
888
|
+
response = agent.run("List all Python files in this directory")
|
|
889
|
+
print(response)
|
|
890
|
+
|
|
891
|
+
# Continue the conversation (history is maintained)
|
|
892
|
+
response = agent.run("Now read the main agent file")
|
|
893
|
+
print(response)
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
**Adding Custom Tools:**
|
|
897
|
+
|
|
898
|
+
Custom tools can be used in two ways:
|
|
899
|
+
|
|
900
|
+
1. **CLI**: Place `.py` files in `~/.patchpal/tools/` (auto-discovered at startup)
|
|
901
|
+
2. **Python API**: Pass functions directly to `create_agent(custom_tools=[...])`
|
|
902
|
+
|
|
903
|
+
Both methods use the same tool schema auto-generation from Python functions with type hints and docstrings:
|
|
904
|
+
|
|
905
|
+
```python
|
|
906
|
+
from patchpal.agent import create_agent
|
|
907
|
+
|
|
908
|
+
def calculator(x: int, y: int, operation: str = "add") -> str:
|
|
909
|
+
"""Perform basic arithmetic operations.
|
|
910
|
+
|
|
911
|
+
Args:
|
|
912
|
+
x: First number
|
|
913
|
+
y: Second number
|
|
914
|
+
operation: Operation to perform (add, subtract, multiply, divide)
|
|
915
|
+
|
|
916
|
+
Returns:
|
|
917
|
+
Result as a string
|
|
918
|
+
"""
|
|
919
|
+
if operation == "add":
|
|
920
|
+
return f"{x} + {y} = {x + y}"
|
|
921
|
+
elif operation == "subtract":
|
|
922
|
+
return f"{x} - {y} = {x - y}"
|
|
923
|
+
elif operation == "multiply":
|
|
924
|
+
return f"{x} * {y} = {x * y}"
|
|
925
|
+
elif operation == "divide":
|
|
926
|
+
if y == 0:
|
|
927
|
+
return "Error: Cannot divide by zero"
|
|
928
|
+
return f"{x} / {y} = {x / y}"
|
|
929
|
+
return "Unknown operation"
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def get_weather(city: str, units: str = "celsius") -> str:
|
|
933
|
+
"""Get weather information for a city.
|
|
934
|
+
|
|
935
|
+
Args:
|
|
936
|
+
city: Name of the city
|
|
937
|
+
units: Temperature units (celsius or fahrenheit)
|
|
938
|
+
|
|
939
|
+
Returns:
|
|
940
|
+
Weather information string
|
|
941
|
+
"""
|
|
942
|
+
# Your implementation here (API call, etc.)
|
|
943
|
+
return f"Weather in {city}: 22°{units[0].upper()}, Sunny"
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
# Create agent with custom tools
|
|
947
|
+
agent = create_agent(
|
|
948
|
+
model_id="anthropic/claude-sonnet-4-5",
|
|
949
|
+
custom_tools=[calculator, get_weather]
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
# Use the agent - it will call your custom tools when appropriate
|
|
953
|
+
response = agent.run("What's 15 multiplied by 23?")
|
|
954
|
+
print(response)
|
|
955
|
+
|
|
956
|
+
response = agent.run("What's the weather in Paris?")
|
|
957
|
+
print(response)
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
**Key Points:**
|
|
961
|
+
- Custom tools are automatically converted to LLM tool schemas
|
|
962
|
+
- Functions should have type hints and Google-style docstrings
|
|
963
|
+
- The agent will call your functions when appropriate
|
|
964
|
+
- Tool execution follows the same permission system as built-in tools
|
|
965
|
+
|
|
966
|
+
**Advanced Usage:**
|
|
967
|
+
|
|
968
|
+
```python
|
|
969
|
+
from patchpal.agent import PatchPalAgent
|
|
970
|
+
|
|
971
|
+
# Create agent with custom configuration
|
|
972
|
+
agent = PatchPalAgent(model_id="anthropic/claude-sonnet-4-5")
|
|
973
|
+
|
|
974
|
+
# Set custom max iterations for complex tasks
|
|
975
|
+
response = agent.run("Refactor the entire codebase", max_iterations=200)
|
|
976
|
+
|
|
977
|
+
# Access conversation history
|
|
978
|
+
print(f"Messages in history: {len(agent.messages)}")
|
|
979
|
+
|
|
980
|
+
# Check context window usage
|
|
981
|
+
stats = agent.context_manager.get_usage_stats(agent.messages)
|
|
982
|
+
print(f"Token usage: {stats['total_tokens']:,} / {stats['context_limit']:,}")
|
|
983
|
+
print(f"Usage: {stats['usage_percent']}%")
|
|
984
|
+
|
|
985
|
+
# Manually trigger compaction if needed
|
|
986
|
+
if agent.context_manager.needs_compaction(agent.messages):
|
|
987
|
+
agent._perform_auto_compaction()
|
|
988
|
+
|
|
989
|
+
# Track API costs (cumulative token counts across session)
|
|
990
|
+
print(f"Total LLM calls: {agent.total_llm_calls}")
|
|
991
|
+
print(f"Cumulative input tokens: {agent.cumulative_input_tokens:,}")
|
|
992
|
+
print(f"Cumulative output tokens: {agent.cumulative_output_tokens:,}")
|
|
993
|
+
print(f"Total tokens: {agent.cumulative_input_tokens + agent.cumulative_output_tokens:,}")
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
**Use Cases:**
|
|
997
|
+
- **Interactive debugging**: Use in Jupyter notebooks for hands-on debugging with agent assistance
|
|
998
|
+
- **Automation scripts**: Build scripts that use the agent for complex tasks with human oversight
|
|
999
|
+
- **Custom workflows**: Integrate PatchPal into your own tools and pipelines
|
|
1000
|
+
- **Code review assistance**: Programmatic code analysis with permission controls
|
|
1001
|
+
- **Batch processing**: Process multiple tasks programmatically while maintaining control
|
|
1002
|
+
- **Testing and evaluation**: Test agent behavior with different prompts and configurations
|
|
1003
|
+
|
|
1004
|
+
**Key Features:**
|
|
1005
|
+
- **Human-in-the-loop design**: Permission prompts ensure human oversight (unlike fully autonomous frameworks)
|
|
1006
|
+
- **Stateful conversations**: Agent maintains full conversation history
|
|
1007
|
+
- **Custom tools**: Add your own Python functions (via CLI auto-discovery or API parameter) with automatic schema generation
|
|
1008
|
+
- **Automatic context management**: Auto-compaction works the same as CLI
|
|
1009
|
+
- **All built-in tools available**: File operations, git, web search, skills, etc.
|
|
1010
|
+
- **Model flexibility**: Works with any LiteLLM-compatible model
|
|
1011
|
+
- **Token tracking**: Monitor API usage and costs in real-time
|
|
1012
|
+
- **Environment variables respected**: All `PATCHPAL_*` settings apply
|
|
1013
|
+
|
|
1014
|
+
**PatchPal vs. Other Agent Frameworks:**
|
|
1015
|
+
|
|
1016
|
+
Unlike fully autonomous agent frameworks (e.g., smolagents, autogen), PatchPal is explicitly designed for **human-in-the-loop workflows**:
|
|
1017
|
+
|
|
1018
|
+
| Feature | PatchPal | Autonomous Frameworks |
|
|
1019
|
+
|---------|----------|----------------------|
|
|
1020
|
+
| **Design Philosophy** | Human oversight & control | Autonomous execution |
|
|
1021
|
+
| **Permission System** | Interactive prompts for sensitive operations | Typically no prompts |
|
|
1022
|
+
| **Primary Use Case** | Code assistance, debugging, interactive tasks | Automated workflows, batch processing |
|
|
1023
|
+
| **Safety Model** | Write boundary protection, command blocking | Varies by framework |
|
|
1024
|
+
| **Custom Tools** | Yes, with automatic schema generation | Yes (varies by framework) |
|
|
1025
|
+
| **Best For** | Developers who want AI assistance with control | Automation, research, agent benchmarks |
|
|
1026
|
+
|
|
1027
|
+
The Python API uses the same agent implementation as the CLI, so you get the complete feature set including permissions, safety guardrails, and context management.
|
|
1028
|
+
|
|
671
1029
|
## Configuration
|
|
672
1030
|
|
|
673
1031
|
PatchPal can be configured through `PATCHPAL_*` environment variables to customize behavior, security, and performance.
|
|
@@ -734,6 +1092,16 @@ export PATCHPAL_PRUNE_MINIMUM=20000 # Minimum tokens to prune (default:
|
|
|
734
1092
|
# Enable/Disable Web Access
|
|
735
1093
|
export PATCHPAL_ENABLE_WEB=false # Disable web search/fetch for air-gapped environments (default: true)
|
|
736
1094
|
|
|
1095
|
+
# SSL Certificate Verification (for web_search)
|
|
1096
|
+
export PATCHPAL_VERIFY_SSL=true # SSL verification for web searches (default: true)
|
|
1097
|
+
# Set to 'false' to disable (not recommended for production)
|
|
1098
|
+
# Or set to path of CA bundle file for corporate certificates
|
|
1099
|
+
# Auto-detects SSL_CERT_FILE and REQUESTS_CA_BUNDLE if not set
|
|
1100
|
+
# Examples:
|
|
1101
|
+
# export PATCHPAL_VERIFY_SSL=false # Disable verification
|
|
1102
|
+
# export PATCHPAL_VERIFY_SSL=/path/to/ca-bundle.crt # Custom CA bundle
|
|
1103
|
+
# (Leave unset to auto-detect from SSL_CERT_FILE/REQUESTS_CA_BUNDLE)
|
|
1104
|
+
|
|
737
1105
|
# Web Request Limits
|
|
738
1106
|
export PATCHPAL_WEB_TIMEOUT=60 # Web request timeout in seconds (default: 30)
|
|
739
1107
|
export PATCHPAL_MAX_WEB_SIZE=10485760 # Max web content size in bytes (default: 5MB)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
patchpal/__init__.py,sha256=S3dYO3L8dSQG2Eaosbu4Pbdq5eTxXLmmvxSzh-TIPiI,606
|
|
2
|
+
patchpal/agent.py,sha256=ayMkZUoohUsf5Tz4esBjOPZUvBT5n-ijOzoOp3c9LAA,59719
|
|
3
|
+
patchpal/cli.py,sha256=6Imrd4hGupIrTi9jnnfwvraNZ_Pq0VJxfo6aSjLRoCY,24131
|
|
4
|
+
patchpal/context.py,sha256=hdTUvyAXXUP47JY1Q3YJDU7noGAcHuBGlNuU272Fjp4,14831
|
|
5
|
+
patchpal/permissions.py,sha256=pVlzit2KFmCpfcbHrHhjPA0LPka04wOtaQdZCf3CCa0,10781
|
|
6
|
+
patchpal/skills.py,sha256=ESLPHkDI8DH4mnAbN8mIcbZ6Bis4vCcqS_NjlYPNCOs,3926
|
|
7
|
+
patchpal/system_prompt.md,sha256=LQzcILr41s65hk7JjaX_WzjUHBHCazVSrx_F_ErqTmA,10850
|
|
8
|
+
patchpal/tool_schema.py,sha256=dGEGYV160G9c7EnSMtnbQ_mYuoR1n6PHHE8T20BriYE,8357
|
|
9
|
+
patchpal/tools.py,sha256=YAUX2-8BBqjZEadIWlUdO-KV2-WHGazgKdMHkYRAExI,93819
|
|
10
|
+
patchpal-0.6.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
11
|
+
patchpal-0.6.0.dist-info/METADATA,sha256=hjleiaXTNaavuW0OygY1XPdbuflYxMQb0hAWw9pGWPw,57384
|
|
12
|
+
patchpal-0.6.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
+
patchpal-0.6.0.dist-info/entry_points.txt,sha256=XcuQikKu5i8Sd8AfHLuKxSE2RWByInTcQgWpP61sr48,47
|
|
14
|
+
patchpal-0.6.0.dist-info/top_level.txt,sha256=YWgv2F-_PIHCu-sF3AF8N1ut5_FbOT-VV6HB70pGSQ8,9
|
|
15
|
+
patchpal-0.6.0.dist-info/RECORD,,
|
patchpal-0.4.5.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
patchpal/__init__.py,sha256=BZKk70eNcw6BLNVrj1TWs91vUTXbRMaHOHObNttZgrM,606
|
|
2
|
-
patchpal/agent.py,sha256=uO-MA1ptt2FdpXrrbo07vVIPQj3radcIwP5izdjzmkQ,55519
|
|
3
|
-
patchpal/cli.py,sha256=6UKoMxtow6Xd643vQ89tb6podepAgU1TjGlE2p8FFzE,23352
|
|
4
|
-
patchpal/context.py,sha256=hdTUvyAXXUP47JY1Q3YJDU7noGAcHuBGlNuU272Fjp4,14831
|
|
5
|
-
patchpal/permissions.py,sha256=pVlzit2KFmCpfcbHrHhjPA0LPka04wOtaQdZCf3CCa0,10781
|
|
6
|
-
patchpal/skills.py,sha256=ESLPHkDI8DH4mnAbN8mIcbZ6Bis4vCcqS_NjlYPNCOs,3926
|
|
7
|
-
patchpal/system_prompt.md,sha256=LQzcILr41s65hk7JjaX_WzjUHBHCazVSrx_F_ErqTmA,10850
|
|
8
|
-
patchpal/tools.py,sha256=6zynI83wW6hwddIDKCYVRK8ICprj93bopp0K60PabS8,93042
|
|
9
|
-
patchpal-0.4.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
10
|
-
patchpal-0.4.5.dist-info/METADATA,sha256=9J5IPsOWx0OTPkghGpE6tF-6CnchyBJYFL_DBLr-uIA,44203
|
|
11
|
-
patchpal-0.4.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
12
|
-
patchpal-0.4.5.dist-info/entry_points.txt,sha256=XcuQikKu5i8Sd8AfHLuKxSE2RWByInTcQgWpP61sr48,47
|
|
13
|
-
patchpal-0.4.5.dist-info/top_level.txt,sha256=YWgv2F-_PIHCu-sF3AF8N1ut5_FbOT-VV6HB70pGSQ8,9
|
|
14
|
-
patchpal-0.4.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|