patchpal 0.22.7__tar.gz → 0.23.0__tar.gz
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-0.22.7/patchpal.egg-info → patchpal-0.23.0}/PKG-INFO +13 -1
- {patchpal-0.22.7 → patchpal-0.23.0}/README.md +12 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/__init__.py +1 -1
- patchpal-0.23.0/patchpal/agent/bedrock_profile_utils.py +226 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/agent/function_calling.py +123 -23
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/cli/autopilot.py +6 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/cli/interactive.py +9 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/code_analysis.py +6 -2
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/common.py +34 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/definitions.py +8 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/find_tool.py +48 -15
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/repo_map.py +14 -3
- {patchpal-0.22.7 → patchpal-0.23.0/patchpal.egg-info}/PKG-INFO +13 -1
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal.egg-info/SOURCES.txt +1 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/LICENSE +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/MANIFEST.in +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/agent/__init__.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/agent/react.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/cli/__init__.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/cli/mcp.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/cli/sandbox.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/cli/streaming.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/config.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/context.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/permissions.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/prompts/react_prompt.md +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/prompts/system_prompt.md +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/skills.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/__init__.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/audit.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/file_reading.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/file_writing.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/grep_tool.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/image_handler.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/mcp.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/shell_tools.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/todo_tools.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/tool_schema.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/user_interaction.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal/tools/web_tools.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal.egg-info/dependency_links.txt +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal.egg-info/entry_points.txt +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal.egg-info/requires.txt +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/patchpal.egg-info/top_level.txt +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/pyproject.toml +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/setup.cfg +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_agent.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_cli.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_config_dynamic.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_context.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_custom_tools.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_enabled_tools.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_find_tool.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_guardrails.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_image_blocking.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_maximum_security.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_mcp_config.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_memory.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_operational_safety.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_optional_tools.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_permissions.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_react.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_reasoning_content.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_repo_map.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_simplified_prompt.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_skills.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_streaming.py +0 -0
- {patchpal-0.22.7 → patchpal-0.23.0}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchpal
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.0
|
|
4
4
|
Summary: An agentic coding and automation assistant, supporting both local and cloud LLMs
|
|
5
5
|
Author: PatchPal Contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -177,6 +177,18 @@ While originally designed for software development, PatchPal is also a general-p
|
|
|
177
177
|
2. PatchPal includes a [unique guardrails system](https://amaiya.github.io/patchpal/safety/) that is better suited to privacy-conscious use cases involving sensitive data.
|
|
178
178
|
3. We needed an agent harness that seamlessly works with [both local and cloud models](https://amaiya.github.io/patchpal/models/overview/#supported-models), including AWS GovCloud Bedrock models.
|
|
179
179
|
|
|
180
|
+
> On Windows Subsystem for Linux (WSL), why is it stalling intermittently at "Thinking..."?
|
|
181
|
+
|
|
182
|
+
This is a [known issue](https://github.com/microsoft/WSL/issues/6264#issuecomment-762154193) with WSL2.
|
|
183
|
+
|
|
184
|
+
Try examining and then lowering the `mtu`:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
$ cat /sys/class/net/eth1/mtu
|
|
188
|
+
1427
|
|
189
|
+
|
|
190
|
+
$ sudo ip link set eth1 mtu 1400
|
|
191
|
+
```
|
|
180
192
|
|
|
181
193
|
## Documentation
|
|
182
194
|
|
|
@@ -129,6 +129,18 @@ While originally designed for software development, PatchPal is also a general-p
|
|
|
129
129
|
2. PatchPal includes a [unique guardrails system](https://amaiya.github.io/patchpal/safety/) that is better suited to privacy-conscious use cases involving sensitive data.
|
|
130
130
|
3. We needed an agent harness that seamlessly works with [both local and cloud models](https://amaiya.github.io/patchpal/models/overview/#supported-models), including AWS GovCloud Bedrock models.
|
|
131
131
|
|
|
132
|
+
> On Windows Subsystem for Linux (WSL), why is it stalling intermittently at "Thinking..."?
|
|
133
|
+
|
|
134
|
+
This is a [known issue](https://github.com/microsoft/WSL/issues/6264#issuecomment-762154193) with WSL2.
|
|
135
|
+
|
|
136
|
+
Try examining and then lowering the `mtu`:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
$ cat /sys/class/net/eth1/mtu
|
|
140
|
+
1427
|
|
141
|
+
|
|
142
|
+
$ sudo ip link set eth1 mtu 1400
|
|
143
|
+
```
|
|
132
144
|
|
|
133
145
|
## Documentation
|
|
134
146
|
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Utilities for AWS Bedrock application inference profiles.
|
|
2
|
+
|
|
3
|
+
Application inference profiles (tagged profiles) don't include model names in their ARNs,
|
|
4
|
+
making it impossible to statically determine model capabilities or pricing. This module
|
|
5
|
+
provides functions to detect the underlying model and its capabilities at runtime.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import litellm
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _extract_model_from_arn(arn: str) -> str | None:
|
|
12
|
+
"""Try to extract underlying model info from an inference profile ARN using AWS API.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
arn: The inference profile ARN
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Model name if found, None otherwise
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
import boto3
|
|
24
|
+
|
|
25
|
+
# Extract region from ARN (arn:aws-us-gov:bedrock:us-gov-east-1:...)
|
|
26
|
+
parts = arn.split(":")
|
|
27
|
+
if len(parts) >= 4:
|
|
28
|
+
region = parts[3]
|
|
29
|
+
else:
|
|
30
|
+
region = os.getenv("AWS_REGION_NAME") or os.getenv("AWS_REGION") or "us-east-1"
|
|
31
|
+
|
|
32
|
+
# Create bedrock client
|
|
33
|
+
bedrock = boto3.client("bedrock", region_name=region)
|
|
34
|
+
|
|
35
|
+
# Get inference profile details
|
|
36
|
+
response = bedrock.get_inference_profile(inferenceProfileIdentifier=arn)
|
|
37
|
+
|
|
38
|
+
# Extract model info from response
|
|
39
|
+
if "models" in response and response["models"]:
|
|
40
|
+
# Get first model from the profile
|
|
41
|
+
first_model = response["models"][0]
|
|
42
|
+
if "modelArn" in first_model:
|
|
43
|
+
# Extract model ID from ARN
|
|
44
|
+
# e.g., arn:aws-us-gov:bedrock:us-gov-west-1::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0
|
|
45
|
+
model_arn = first_model["modelArn"]
|
|
46
|
+
|
|
47
|
+
# Try multiple extraction methods
|
|
48
|
+
if "foundation-model" in model_arn:
|
|
49
|
+
# Split on the foundation-model part
|
|
50
|
+
parts = model_arn.split("foundation-model/")
|
|
51
|
+
if len(parts) > 1:
|
|
52
|
+
return parts[1]
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
except Exception:
|
|
56
|
+
# API call failed or boto3 not available
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def detect_model_capabilities(
|
|
61
|
+
model_id: str, litellm_kwargs: dict = None
|
|
62
|
+
) -> tuple[bool, str | None]:
|
|
63
|
+
"""Detect prompt caching support and underlying model name for application inference profiles.
|
|
64
|
+
|
|
65
|
+
This is useful for application inference profiles (tagged profiles) where the
|
|
66
|
+
underlying model name is not in the ARN, making it impossible to statically
|
|
67
|
+
determine model capabilities or pricing.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
model_id: Full LiteLLM model identifier (e.g., bedrock/converse/arn:...)
|
|
71
|
+
litellm_kwargs: Optional kwargs to pass to litellm.completion
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Tuple of (caching_supported: bool, model_name: str | None)
|
|
75
|
+
- caching_supported: True if prompt caching works with tools
|
|
76
|
+
- model_name: Detected model name from response metadata (e.g., "claude-3-5-sonnet-20241022")
|
|
77
|
+
"""
|
|
78
|
+
if litellm_kwargs is None:
|
|
79
|
+
litellm_kwargs = {}
|
|
80
|
+
|
|
81
|
+
# Create a minimal test message with cache markers
|
|
82
|
+
test_messages = [
|
|
83
|
+
{
|
|
84
|
+
"role": "user",
|
|
85
|
+
"content": [
|
|
86
|
+
{
|
|
87
|
+
"type": "text",
|
|
88
|
+
"text": "What is the capital of France?",
|
|
89
|
+
"cache_control": {"type": "ephemeral"},
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
# Include a minimal tool to match real usage (some models only support caching without tools)
|
|
96
|
+
test_tools = [
|
|
97
|
+
{
|
|
98
|
+
"type": "function",
|
|
99
|
+
"function": {
|
|
100
|
+
"name": "get_info",
|
|
101
|
+
"description": "Get information",
|
|
102
|
+
"parameters": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"properties": {"query": {"type": "string", "description": "The query"}},
|
|
105
|
+
"required": ["query"],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
caching_supported = False
|
|
112
|
+
detected_model = None
|
|
113
|
+
|
|
114
|
+
# For ARNs, try to get model info from AWS API first
|
|
115
|
+
if "arn:aws" in model_id and "inference-profile" in model_id:
|
|
116
|
+
# Extract just the ARN (remove bedrock/converse/ prefix if present)
|
|
117
|
+
arn = model_id.replace("bedrock/converse/", "").replace("bedrock/", "")
|
|
118
|
+
detected_model = _extract_model_from_arn(arn)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
# Try with Anthropic-style cache_control AND tools (matches real usage)
|
|
122
|
+
response = litellm.completion(
|
|
123
|
+
model=model_id,
|
|
124
|
+
messages=test_messages,
|
|
125
|
+
tools=test_tools,
|
|
126
|
+
tool_choice="auto", # Include tool_choice like the real agent
|
|
127
|
+
max_tokens=10,
|
|
128
|
+
**litellm_kwargs,
|
|
129
|
+
)
|
|
130
|
+
# If we got here without error, caching is supported
|
|
131
|
+
caching_supported = True
|
|
132
|
+
|
|
133
|
+
# Only try to extract model from response if we didn't get it from AWS API
|
|
134
|
+
if not detected_model:
|
|
135
|
+
# Try to extract model name from response metadata
|
|
136
|
+
# Bedrock responses include model info in various places
|
|
137
|
+
if hasattr(response, "_hidden_params") and response._hidden_params:
|
|
138
|
+
# LiteLLM stores raw response data here
|
|
139
|
+
hidden = response._hidden_params
|
|
140
|
+
|
|
141
|
+
# Check optional_params which may contain raw boto3 response
|
|
142
|
+
if "optional_params" in hidden and isinstance(hidden["optional_params"], dict):
|
|
143
|
+
optional = hidden["optional_params"]
|
|
144
|
+
# Bedrock converse API may include model info in the response
|
|
145
|
+
if "model" in optional:
|
|
146
|
+
detected_model = optional["model"]
|
|
147
|
+
elif "modelId" in optional:
|
|
148
|
+
detected_model = optional["modelId"]
|
|
149
|
+
|
|
150
|
+
# Check standard fields
|
|
151
|
+
if not detected_model and "model_id" in hidden and hidden["model_id"]:
|
|
152
|
+
detected_model = hidden["model_id"]
|
|
153
|
+
elif not detected_model and "model" in hidden and hidden["model"]:
|
|
154
|
+
detected_model = hidden["model"]
|
|
155
|
+
|
|
156
|
+
# Check response metadata
|
|
157
|
+
if not detected_model and hasattr(response, "model"):
|
|
158
|
+
model_val = response.model
|
|
159
|
+
# Skip if it's just the ARN we passed in
|
|
160
|
+
if model_val and "application-inference-profile" not in model_val:
|
|
161
|
+
detected_model = model_val
|
|
162
|
+
|
|
163
|
+
# Try to extract from response choices/usage if available
|
|
164
|
+
if not detected_model and hasattr(response, "usage"):
|
|
165
|
+
usage = response.usage
|
|
166
|
+
if hasattr(usage, "model") and usage.model:
|
|
167
|
+
detected_model = usage.model
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
error_msg = str(e).lower()
|
|
171
|
+
# Check for caching-specific errors
|
|
172
|
+
if any(
|
|
173
|
+
phrase in error_msg
|
|
174
|
+
for phrase in [
|
|
175
|
+
"prompt caching",
|
|
176
|
+
"cache_control",
|
|
177
|
+
"cachepoint",
|
|
178
|
+
"unsupported model",
|
|
179
|
+
"did not allow prompt caching",
|
|
180
|
+
]
|
|
181
|
+
):
|
|
182
|
+
# Caching not supported, but still try to detect model without caching
|
|
183
|
+
caching_supported = False
|
|
184
|
+
|
|
185
|
+
# Only retry if we don't already have model from AWS API
|
|
186
|
+
if not detected_model:
|
|
187
|
+
try:
|
|
188
|
+
# Retry without cache markers to detect model
|
|
189
|
+
simple_messages = [{"role": "user", "content": "Hi"}]
|
|
190
|
+
response = litellm.completion(
|
|
191
|
+
model=model_id,
|
|
192
|
+
messages=simple_messages,
|
|
193
|
+
tools=test_tools,
|
|
194
|
+
max_tokens=5,
|
|
195
|
+
**litellm_kwargs,
|
|
196
|
+
)
|
|
197
|
+
# Try to extract model from response
|
|
198
|
+
if hasattr(response, "_hidden_params") and response._hidden_params:
|
|
199
|
+
hidden = response._hidden_params
|
|
200
|
+
if "model_id" in hidden:
|
|
201
|
+
detected_model = hidden["model_id"]
|
|
202
|
+
elif "model" in hidden:
|
|
203
|
+
detected_model = hidden["model"]
|
|
204
|
+
if not detected_model and hasattr(response, "model"):
|
|
205
|
+
detected_model = response.model
|
|
206
|
+
except Exception:
|
|
207
|
+
pass # Could not detect model
|
|
208
|
+
else:
|
|
209
|
+
# Different error (auth, network, etc.)
|
|
210
|
+
caching_supported = False
|
|
211
|
+
|
|
212
|
+
return caching_supported, detected_model
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_prompt_caching_support(model_id: str, litellm_kwargs: dict = None) -> bool:
|
|
216
|
+
"""Test if a model supports prompt caching (backward compatibility wrapper).
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
model_id: Full LiteLLM model identifier (e.g., bedrock/converse/arn:...)
|
|
220
|
+
litellm_kwargs: Optional kwargs to pass to litellm.completion
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
True if prompt caching is supported, False otherwise
|
|
224
|
+
"""
|
|
225
|
+
caching_supported, _ = detect_model_capabilities(model_id, litellm_kwargs)
|
|
226
|
+
return caching_supported
|
|
@@ -28,14 +28,36 @@ LLM_TIMEOUT = config.LLM_TIMEOUT
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _is_bedrock_arn(model_id: str) -> bool:
|
|
31
|
-
"""Check if a model ID is a Bedrock ARN.
|
|
31
|
+
"""Check if a model ID is a Bedrock ARN.
|
|
32
|
+
|
|
33
|
+
Supports all Bedrock inference profile ARN formats:
|
|
34
|
+
- arn:aws:bedrock:region:account:inference-profile/profile-id
|
|
35
|
+
- arn:aws-us-gov:bedrock:region:account:inference-profile/profile-id
|
|
36
|
+
- arn:aws:bedrock:region:account:application-inference-profile/app-id
|
|
37
|
+
- arn:aws-us-gov:bedrock:region:account:application-inference-profile/app-id
|
|
38
|
+
"""
|
|
32
39
|
return (
|
|
33
40
|
model_id.startswith("arn:aws")
|
|
34
41
|
and ":bedrock:" in model_id
|
|
35
|
-
and "
|
|
42
|
+
and "inference-profile/" in model_id
|
|
36
43
|
)
|
|
37
44
|
|
|
38
45
|
|
|
46
|
+
def _is_application_inference_profile(model_id: str) -> bool:
|
|
47
|
+
"""Check if a model ID is a Bedrock application inference profile (tagged profile).
|
|
48
|
+
|
|
49
|
+
Application inference profiles are tagged profiles that don't include the underlying
|
|
50
|
+
model name in the ARN, making it impossible to statically determine model capabilities.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
model_id: Model identifier (may or may not have bedrock/ prefix)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if this is an application inference profile ARN
|
|
57
|
+
"""
|
|
58
|
+
return ":application-inference-profile/" in model_id
|
|
59
|
+
|
|
60
|
+
|
|
39
61
|
def _normalize_bedrock_model_id(model_id: str) -> str:
|
|
40
62
|
"""Normalize Bedrock model ID to ensure it has the bedrock/ prefix.
|
|
41
63
|
|
|
@@ -51,7 +73,11 @@ def _normalize_bedrock_model_id(model_id: str) -> str:
|
|
|
51
73
|
|
|
52
74
|
# If it looks like a Bedrock ARN, add the prefix
|
|
53
75
|
if _is_bedrock_arn(model_id):
|
|
54
|
-
|
|
76
|
+
# Application inference profiles require the converse API
|
|
77
|
+
if ":application-inference-profile/" in model_id:
|
|
78
|
+
return f"bedrock/converse/{model_id}"
|
|
79
|
+
else:
|
|
80
|
+
return f"bedrock/{model_id}"
|
|
55
81
|
|
|
56
82
|
# If it's a standard Bedrock model ID (e.g., anthropic.claude-v2)
|
|
57
83
|
# Check if it looks like a Bedrock model format
|
|
@@ -274,6 +300,10 @@ def _supports_prompt_caching(model_id: str) -> bool:
|
|
|
274
300
|
# Bedrock Nova models support caching
|
|
275
301
|
if model_id.startswith("bedrock/") and "amazon.nova" in model_id.lower():
|
|
276
302
|
return True
|
|
303
|
+
# Bedrock ARNs (all types): enable caching and let Bedrock handle it
|
|
304
|
+
# If the underlying model doesn't support caching, Bedrock will ignore the markers
|
|
305
|
+
if model_id.startswith("bedrock/") and "inference-profile/" in model_id:
|
|
306
|
+
return True
|
|
277
307
|
return False
|
|
278
308
|
|
|
279
309
|
|
|
@@ -301,12 +331,14 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
|
|
|
301
331
|
|
|
302
332
|
# Determine cache marker format based on provider
|
|
303
333
|
# Anthropic models (direct or via Bedrock) use cache_control
|
|
304
|
-
#
|
|
305
|
-
|
|
306
|
-
|
|
334
|
+
# Nova models use cachePoint
|
|
335
|
+
# For Bedrock ARNs without model name, default to cache_control (most common)
|
|
336
|
+
if model_id.startswith("bedrock/") and "amazon.nova" in model_id.lower():
|
|
337
|
+
# Nova models explicitly use cachePoint
|
|
307
338
|
cache_marker = {"cachePoint": {"type": "default"}}
|
|
308
339
|
else:
|
|
309
340
|
# Anthropic models (direct or via Bedrock) use cache_control
|
|
341
|
+
# Also default for Bedrock ARNs (most use Anthropic/Claude)
|
|
310
342
|
cache_marker = {"cache_control": {"type": "ephemeral"}}
|
|
311
343
|
|
|
312
344
|
# Count existing cache markers across all messages
|
|
@@ -509,6 +541,49 @@ class PatchPalAgent:
|
|
|
509
541
|
if litellm_kwargs:
|
|
510
542
|
self.litellm_kwargs.update(litellm_kwargs)
|
|
511
543
|
|
|
544
|
+
# Detect capabilities for application inference profiles
|
|
545
|
+
# These ARNs don't include model names, so we test with a minimal request
|
|
546
|
+
self.prompt_caching_supported = None # None = untested, True/False after test
|
|
547
|
+
self.detected_model_name = None # Store detected model for cost tracking
|
|
548
|
+
if _is_application_inference_profile(self.model_id):
|
|
549
|
+
# Test capabilities with a minimal request
|
|
550
|
+
try:
|
|
551
|
+
from patchpal.agent.bedrock_profile_utils import detect_model_capabilities
|
|
552
|
+
|
|
553
|
+
print("\033[2mℹ️ Detecting model capabilities...\033[0m", flush=True)
|
|
554
|
+
self.prompt_caching_supported, self.detected_model_name = detect_model_capabilities(
|
|
555
|
+
self.model_id, self.litellm_kwargs
|
|
556
|
+
)
|
|
557
|
+
if self.prompt_caching_supported:
|
|
558
|
+
print("\033[2m✓ Prompt caching is supported\033[0m", flush=True)
|
|
559
|
+
else:
|
|
560
|
+
print("\033[2m✗ Prompt caching is not supported\033[0m", flush=True)
|
|
561
|
+
|
|
562
|
+
if self.detected_model_name:
|
|
563
|
+
print(f"\033[2m✓ Detected model: {self.detected_model_name}\033[0m", flush=True)
|
|
564
|
+
|
|
565
|
+
# Update context limit based on detected model
|
|
566
|
+
try:
|
|
567
|
+
model_info = litellm.get_model_info(f"bedrock/{self.detected_model_name}")
|
|
568
|
+
max_input = model_info.get("max_input_tokens")
|
|
569
|
+
if max_input and isinstance(max_input, (int, float)) and max_input > 0:
|
|
570
|
+
self.context_manager.context_limit = int(max_input)
|
|
571
|
+
except Exception:
|
|
572
|
+
pass # Keep default limit
|
|
573
|
+
else:
|
|
574
|
+
print(
|
|
575
|
+
"\033[2m⚠ Could not detect underlying model name (cost tracking may be inaccurate)\033[0m",
|
|
576
|
+
flush=True,
|
|
577
|
+
)
|
|
578
|
+
except Exception:
|
|
579
|
+
# If test fails, assume caching not supported and model unknown
|
|
580
|
+
self.prompt_caching_supported = False
|
|
581
|
+
self.detected_model_name = None
|
|
582
|
+
elif _supports_prompt_caching(self.model_id):
|
|
583
|
+
self.prompt_caching_supported = True
|
|
584
|
+
else:
|
|
585
|
+
self.prompt_caching_supported = False
|
|
586
|
+
|
|
512
587
|
# Load MEMORY.md if it exists and has non-template content
|
|
513
588
|
self._load_project_memory()
|
|
514
589
|
|
|
@@ -847,7 +922,17 @@ It's currently empty (just the template). The file is automatically loaded at se
|
|
|
847
922
|
float: The calculated cost in dollars
|
|
848
923
|
"""
|
|
849
924
|
try:
|
|
850
|
-
|
|
925
|
+
# For application inference profiles, use detected model name for pricing
|
|
926
|
+
model_for_pricing = self.model_id
|
|
927
|
+
if _is_application_inference_profile(self.model_id) and self.detected_model_name:
|
|
928
|
+
# Map detected model name to a pricing model
|
|
929
|
+
# e.g., "anthropic.claude-3-5-sonnet-20241022-v2:0" -> "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0"
|
|
930
|
+
if not self.detected_model_name.startswith("bedrock/"):
|
|
931
|
+
model_for_pricing = f"bedrock/{self.detected_model_name}"
|
|
932
|
+
else:
|
|
933
|
+
model_for_pricing = self.detected_model_name
|
|
934
|
+
|
|
935
|
+
model_info = litellm.get_model_info(model_for_pricing)
|
|
851
936
|
input_cost_per_token = model_info.get("input_cost_per_token", 0)
|
|
852
937
|
output_cost_per_token = model_info.get("output_cost_per_token", 0)
|
|
853
938
|
|
|
@@ -881,19 +966,26 @@ It's currently empty (just the template). The file is automatically loaded at se
|
|
|
881
966
|
cost += cache_read_tokens * input_cost_per_token * 0.1
|
|
882
967
|
|
|
883
968
|
# Handle OpenAI cache pricing (prompt_tokens_details.cached_tokens)
|
|
969
|
+
# IMPORTANT: For Bedrock, LiteLLM populates prompt_tokens_details.cached_tokens
|
|
970
|
+
# with cache_read_input_tokens for compatibility, but we already handled those above.
|
|
971
|
+
# Only process this field if we're NOT using Bedrock-style cache fields.
|
|
884
972
|
openai_cached_tokens = 0
|
|
885
|
-
if
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
#
|
|
896
|
-
|
|
973
|
+
if not (cache_creation_tokens or cache_read_tokens): # Only for non-Bedrock models
|
|
974
|
+
if (
|
|
975
|
+
hasattr(usage, "prompt_tokens_details")
|
|
976
|
+
and usage.prompt_tokens_details is not None
|
|
977
|
+
):
|
|
978
|
+
prompt_details = usage.prompt_tokens_details
|
|
979
|
+
if hasattr(prompt_details, "cached_tokens") and prompt_details.cached_tokens:
|
|
980
|
+
# Ensure cached_tokens is a number, not a mock or None
|
|
981
|
+
if isinstance(prompt_details.cached_tokens, (int, float)):
|
|
982
|
+
openai_cached_tokens = prompt_details.cached_tokens
|
|
983
|
+
# Use cached_input_cost_per_token if available, otherwise fallback to 0.5x multiplier
|
|
984
|
+
if cached_input_cost_per_token > 0:
|
|
985
|
+
cost += openai_cached_tokens * cached_input_cost_per_token
|
|
986
|
+
else:
|
|
987
|
+
# Fallback: OpenAI cached tokens typically cost 50% of regular input
|
|
988
|
+
cost += openai_cached_tokens * input_cost_per_token * 0.5
|
|
897
989
|
|
|
898
990
|
# Regular input tokens (excluding all cache tokens)
|
|
899
991
|
regular_input = (
|
|
@@ -1048,8 +1140,10 @@ It's currently empty (just the template). The file is automatically loaded at se
|
|
|
1048
1140
|
# Filter images if BLOCK_IMAGES is enabled (for non-vision models or user preference)
|
|
1049
1141
|
messages = self.image_handler.filter_images_if_blocked(messages)
|
|
1050
1142
|
|
|
1051
|
-
# Apply prompt caching for supported models
|
|
1052
|
-
|
|
1143
|
+
# Apply prompt caching for supported models
|
|
1144
|
+
# Check instance variable for dynamically-tested models (application inference profiles)
|
|
1145
|
+
if self.prompt_caching_supported:
|
|
1146
|
+
messages = _apply_prompt_caching(messages, self.model_id)
|
|
1053
1147
|
|
|
1054
1148
|
# Use LiteLLM for all providers
|
|
1055
1149
|
try:
|
|
@@ -1260,6 +1354,7 @@ It's currently empty (just the template). The file is automatically loaded at se
|
|
|
1260
1354
|
)
|
|
1261
1355
|
elif tool_name == "get_repo_map":
|
|
1262
1356
|
max_files = tool_args.get("max_files", 100)
|
|
1357
|
+
max_depth = tool_args.get("max_depth")
|
|
1263
1358
|
patterns = ""
|
|
1264
1359
|
if tool_args.get("include_patterns"):
|
|
1265
1360
|
patterns = (
|
|
@@ -1269,8 +1364,9 @@ It's currently empty (just the template). The file is automatically loaded at se
|
|
|
1269
1364
|
patterns = (
|
|
1270
1365
|
f" (exclude: {', '.join(tool_args['exclude_patterns'])})"
|
|
1271
1366
|
)
|
|
1367
|
+
depth_info = f", depth≤{max_depth}" if max_depth is not None else ""
|
|
1272
1368
|
print(
|
|
1273
|
-
f"\033[2m🗺️ Generating repository map (max {max_files} files{patterns})...\033[0m",
|
|
1369
|
+
f"\033[2m🗺️ Generating repository map (max {max_files} files{depth_info}{patterns})...\033[0m",
|
|
1274
1370
|
flush=True,
|
|
1275
1371
|
)
|
|
1276
1372
|
elif tool_name == "get_file_info":
|
|
@@ -1295,8 +1391,12 @@ It's currently empty (just the template). The file is automatically loaded at se
|
|
|
1295
1391
|
)
|
|
1296
1392
|
elif tool_name == "find":
|
|
1297
1393
|
pattern_desc = tool_args.get("pattern", "*")
|
|
1394
|
+
max_depth = tool_args.get("max_depth")
|
|
1395
|
+
depth_info = (
|
|
1396
|
+
f" (depth≤{max_depth})" if max_depth is not None else ""
|
|
1397
|
+
)
|
|
1298
1398
|
print(
|
|
1299
|
-
f"\033[2m📂 Finding files: {pattern_desc}\033[0m",
|
|
1399
|
+
f"\033[2m📂 Finding files: {pattern_desc}{depth_info}\033[0m",
|
|
1300
1400
|
flush=True,
|
|
1301
1401
|
)
|
|
1302
1402
|
elif tool_name == "list_skills":
|
|
@@ -125,6 +125,12 @@ def autopilot_loop(
|
|
|
125
125
|
# The agent's conversation history accumulates, so it can see all previous work
|
|
126
126
|
response = agent.run(prompt, max_iterations=100)
|
|
127
127
|
|
|
128
|
+
# Reset operation counter after each conversation turn
|
|
129
|
+
# This allows long autopilot sessions without hitting the global limit
|
|
130
|
+
from patchpal.tools.common import reset_operation_counter
|
|
131
|
+
|
|
132
|
+
reset_operation_counter()
|
|
133
|
+
|
|
128
134
|
# Log agent response to audit log
|
|
129
135
|
try:
|
|
130
136
|
from patchpal.tools.audit import log_agent_response
|
|
@@ -15,6 +15,7 @@ from rich.markdown import Markdown
|
|
|
15
15
|
from patchpal.agent import create_agent, create_react_agent
|
|
16
16
|
from patchpal.config import config
|
|
17
17
|
from patchpal.tools import audit_logger, set_require_permission_for_all
|
|
18
|
+
from patchpal.tools.common import reset_operation_counter
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _sanitize_for_logging(text: str) -> str:
|
|
@@ -1567,6 +1568,10 @@ Supported models: Any LiteLLM-supported model
|
|
|
1567
1568
|
|
|
1568
1569
|
result = agent.run(prompt, max_iterations=max_iterations)
|
|
1569
1570
|
|
|
1571
|
+
# Reset operation counter after each conversation turn
|
|
1572
|
+
# This allows long sessions without hitting the global limit
|
|
1573
|
+
reset_operation_counter()
|
|
1574
|
+
|
|
1570
1575
|
print("\n" + "=" * 80)
|
|
1571
1576
|
print("\033[1;32mAgent:\033[0m")
|
|
1572
1577
|
print("=" * 80)
|
|
@@ -1595,6 +1600,10 @@ Supported models: Any LiteLLM-supported model
|
|
|
1595
1600
|
|
|
1596
1601
|
result = agent.run(user_input, max_iterations=max_iterations)
|
|
1597
1602
|
|
|
1603
|
+
# Reset operation counter after each conversation turn
|
|
1604
|
+
# This allows long sessions without hitting the global limit
|
|
1605
|
+
reset_operation_counter()
|
|
1606
|
+
|
|
1598
1607
|
# Log agent response to audit log with hash-chaining
|
|
1599
1608
|
try:
|
|
1600
1609
|
from patchpal.tools.audit import log_agent_response
|
|
@@ -76,7 +76,7 @@ CLASS_NODE_TYPES = {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def code_structure(path: str, max_symbols: int = 50) -> str:
|
|
79
|
+
def code_structure(path: str, max_symbols: int = 50, _internal_call: bool = False) -> str:
|
|
80
80
|
"""
|
|
81
81
|
Analyze code structure using tree-sitter AST parsing.
|
|
82
82
|
|
|
@@ -92,6 +92,7 @@ def code_structure(path: str, max_symbols: int = 50) -> str:
|
|
|
92
92
|
Args:
|
|
93
93
|
path: File path to analyze (relative or absolute)
|
|
94
94
|
max_symbols: Maximum number of symbols to show (default: 50)
|
|
95
|
+
_internal_call: Internal flag - don't count operation if called from get_repo_map
|
|
95
96
|
|
|
96
97
|
Returns:
|
|
97
98
|
Formatted code structure overview
|
|
@@ -107,7 +108,10 @@ def code_structure(path: str, max_symbols: int = 50) -> str:
|
|
|
107
108
|
|
|
108
109
|
Use read_lines('patchpal/tools.py', start, end) to read specific sections.
|
|
109
110
|
"""
|
|
110
|
-
|
|
111
|
+
# Only count operation if not an internal call from get_repo_map
|
|
112
|
+
# This prevents exceeding operation limits when scanning large repos
|
|
113
|
+
if not _internal_call:
|
|
114
|
+
_operation_limiter.check_limit(f"code_structure({path})")
|
|
111
115
|
|
|
112
116
|
if not TREE_SITTER_AVAILABLE:
|
|
113
117
|
return (
|
|
@@ -48,6 +48,40 @@ except ImportError:
|
|
|
48
48
|
|
|
49
49
|
REPO_ROOT = Path(".").resolve()
|
|
50
50
|
|
|
51
|
+
|
|
52
|
+
def depth_limited_walk(root_dir: Path, max_depth: int):
|
|
53
|
+
"""Walk directory tree up to max_depth without traversing deeper.
|
|
54
|
+
|
|
55
|
+
This is a shared utility for tools that need depth-limited traversal
|
|
56
|
+
to avoid performance issues in large codebases.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
root_dir: Root directory to start traversal
|
|
60
|
+
max_depth: Maximum depth to traverse (0 = only root_dir level)
|
|
61
|
+
|
|
62
|
+
Yields:
|
|
63
|
+
Path objects found within depth limit (both files and directories)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def _walk(current_dir: Path, current_depth: int):
|
|
67
|
+
"""Recursively walk directories up to max_depth."""
|
|
68
|
+
if current_depth > max_depth:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
for item in current_dir.iterdir():
|
|
73
|
+
yield item
|
|
74
|
+
# Only recurse if we haven't reached max depth and it's a directory
|
|
75
|
+
if item.is_dir() and not any(part.startswith(".") for part in item.parts):
|
|
76
|
+
if current_depth < max_depth: # Check before recursing
|
|
77
|
+
yield from _walk(item, current_depth + 1)
|
|
78
|
+
except (PermissionError, OSError):
|
|
79
|
+
# Skip directories we can't read
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
yield from _walk(root_dir, 0)
|
|
83
|
+
|
|
84
|
+
|
|
51
85
|
# Import config for centralized environment variable access
|
|
52
86
|
from patchpal.config import config # noqa: E402
|
|
53
87
|
|
|
@@ -139,6 +139,10 @@ Tip: Read README first for context when exploring repositories.""",
|
|
|
139
139
|
"items": {"type": "string"},
|
|
140
140
|
"description": "Files to prioritize in the output (e.g., files mentioned in conversation). These appear first in the map.",
|
|
141
141
|
},
|
|
142
|
+
"max_depth": {
|
|
143
|
+
"type": "integer",
|
|
144
|
+
"description": "Maximum directory depth to traverse (default: None for unlimited). Example: max_depth=3 traverses up to 3 levels deep from repository root. Useful for large codebases to limit scope.",
|
|
145
|
+
},
|
|
142
146
|
},
|
|
143
147
|
"required": [],
|
|
144
148
|
},
|
|
@@ -457,6 +461,10 @@ Tip: Read README first for context when exploring repositories.""",
|
|
|
457
461
|
"type": "string",
|
|
458
462
|
"description": "Directory to search in (default: repository root). Can be relative to repo root or absolute.",
|
|
459
463
|
},
|
|
464
|
+
"max_depth": {
|
|
465
|
+
"type": "integer",
|
|
466
|
+
"description": "Maximum directory depth to traverse (default: None for unlimited). Example: max_depth=2 searches up to 2 levels deep from the search directory. Useful for limiting scope in large codebases.",
|
|
467
|
+
},
|
|
460
468
|
},
|
|
461
469
|
"required": [],
|
|
462
470
|
},
|
|
@@ -12,6 +12,7 @@ from typing import Optional
|
|
|
12
12
|
from patchpal.tools.common import (
|
|
13
13
|
REPO_ROOT,
|
|
14
14
|
_operation_limiter,
|
|
15
|
+
depth_limited_walk,
|
|
15
16
|
require_permission_for_read,
|
|
16
17
|
)
|
|
17
18
|
|
|
@@ -19,14 +20,38 @@ MAX_RESULTS = 100
|
|
|
19
20
|
MAX_OUTPUT_BYTES = 50 * 1024
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def _matches_glob_pattern(file_path: Path, search_dir: Path, pattern: str) -> bool:
|
|
24
|
+
"""Check if a file matches a glob pattern.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
file_path: Absolute path to the file
|
|
28
|
+
search_dir: Base search directory
|
|
29
|
+
pattern: Glob pattern (e.g., '*.py', '**/*.js', 'src/*.txt')
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if file matches the pattern
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
rel_path = file_path.relative_to(search_dir)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
# Match against relative path for patterns with directory structure
|
|
40
|
+
if "**" in pattern or "/" in pattern or "\\" in pattern:
|
|
41
|
+
return rel_path.match(pattern)
|
|
42
|
+
else:
|
|
43
|
+
# Match against just the filename for simple patterns
|
|
44
|
+
return Path(file_path.name).match(pattern)
|
|
45
|
+
|
|
46
|
+
|
|
22
47
|
@require_permission_for_read(
|
|
23
48
|
"find",
|
|
24
|
-
get_description=lambda pattern="**/*", path=None: (
|
|
49
|
+
get_description=lambda pattern="**/*", path=None, max_depth=None: (
|
|
25
50
|
f" Search for files matching '{pattern}'" + (f" in {path}" if path else "")
|
|
26
51
|
),
|
|
27
|
-
get_pattern=lambda pattern="**/*", path=None: path,
|
|
52
|
+
get_pattern=lambda pattern="**/*", path=None, max_depth=None: path,
|
|
28
53
|
)
|
|
29
|
-
def find(pattern: str = "**/*", path: Optional[str] = None) -> str:
|
|
54
|
+
def find(pattern: str = "**/*", path: Optional[str] = None, max_depth: Optional[int] = None) -> str:
|
|
30
55
|
"""Search for files by glob pattern.
|
|
31
56
|
|
|
32
57
|
Returns matching file paths relative to the search directory, sorted by
|
|
@@ -36,6 +61,8 @@ def find(pattern: str = "**/*", path: Optional[str] = None) -> str:
|
|
|
36
61
|
pattern: Glob pattern to match files (default: "**/*" for all files).
|
|
37
62
|
Examples: '*.py', '**/*.json', 'src/**/*.spec.ts'
|
|
38
63
|
path: Directory to search in (default: repository root). Can be relative to repo root or absolute.
|
|
64
|
+
max_depth: Maximum directory depth to traverse (default: None for unlimited).
|
|
65
|
+
Example: max_depth=2 searches up to 2 levels deep from the search directory.
|
|
39
66
|
|
|
40
67
|
Returns:
|
|
41
68
|
Newline-separated list of matching file paths, sorted by modification time
|
|
@@ -59,21 +86,27 @@ def find(pattern: str = "**/*", path: Optional[str] = None) -> str:
|
|
|
59
86
|
else:
|
|
60
87
|
search_dir = REPO_ROOT
|
|
61
88
|
|
|
62
|
-
#
|
|
63
|
-
if
|
|
64
|
-
#
|
|
65
|
-
|
|
89
|
+
# Collect candidate files
|
|
90
|
+
if max_depth is not None:
|
|
91
|
+
# Depth-limited: walk tree and filter by pattern
|
|
92
|
+
all_files = [p for p in depth_limited_walk(search_dir, max_depth) if p.is_file()]
|
|
93
|
+
matches = [f for f in all_files if _matches_glob_pattern(f, search_dir, pattern)]
|
|
66
94
|
else:
|
|
67
|
-
# Check if pattern
|
|
68
|
-
if "
|
|
69
|
-
#
|
|
95
|
+
# Check if pattern requires recursive search
|
|
96
|
+
if "**" in pattern:
|
|
97
|
+
# Recursive glob
|
|
70
98
|
matches = list(search_dir.glob(pattern))
|
|
71
99
|
else:
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
# Check if pattern contains path separators
|
|
101
|
+
if "/" in pattern or "\\" in pattern:
|
|
102
|
+
# Pattern includes directory structure
|
|
103
|
+
matches = list(search_dir.glob(pattern))
|
|
104
|
+
else:
|
|
105
|
+
# Simple filename pattern - search recursively
|
|
106
|
+
matches = list(search_dir.glob(f"**/{pattern}"))
|
|
107
|
+
|
|
108
|
+
# Filter to only files
|
|
109
|
+
matches = [p for p in matches if p.is_file()]
|
|
77
110
|
|
|
78
111
|
# Load gitignore patterns if .gitignore exists
|
|
79
112
|
gitignore_patterns = _load_gitignore_patterns(REPO_ROOT)
|
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Dict, List, Optional, Tuple
|
|
13
13
|
|
|
14
14
|
from patchpal.tools.code_analysis import LANGUAGE_MAP, code_structure
|
|
15
|
-
from patchpal.tools.common import REPO_ROOT, _operation_limiter
|
|
15
|
+
from patchpal.tools.common import REPO_ROOT, _operation_limiter, depth_limited_walk
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class RepoMapCache:
|
|
@@ -80,6 +80,7 @@ def get_repo_map(
|
|
|
80
80
|
include_patterns: Optional[List[str]] = None,
|
|
81
81
|
exclude_patterns: Optional[List[str]] = None,
|
|
82
82
|
focus_files: Optional[List[str]] = None,
|
|
83
|
+
max_depth: Optional[int] = None,
|
|
83
84
|
) -> str:
|
|
84
85
|
"""Generate a compact repository map showing code structure across all files.
|
|
85
86
|
|
|
@@ -95,6 +96,8 @@ def get_repo_map(
|
|
|
95
96
|
include_patterns: Glob patterns to include (e.g., ['*.py', '*.js'])
|
|
96
97
|
exclude_patterns: Glob patterns to exclude (e.g., ['*test*', '*_pb2.py'])
|
|
97
98
|
focus_files: Files mentioned in conversation (prioritized in output)
|
|
99
|
+
max_depth: Maximum directory depth to traverse (default: None for unlimited).
|
|
100
|
+
Example: max_depth=3 traverses up to 3 levels deep from repository root.
|
|
98
101
|
|
|
99
102
|
Returns:
|
|
100
103
|
Formatted repository map with file structures
|
|
@@ -130,7 +133,13 @@ def get_repo_map(
|
|
|
130
133
|
file_structures: Dict[str, str] = {}
|
|
131
134
|
skipped_count = 0
|
|
132
135
|
|
|
133
|
-
|
|
136
|
+
# Use depth-limited traversal if max_depth is specified
|
|
137
|
+
if max_depth is not None:
|
|
138
|
+
paths_to_check = depth_limited_walk(REPO_ROOT, max_depth)
|
|
139
|
+
else:
|
|
140
|
+
paths_to_check = REPO_ROOT.rglob("*")
|
|
141
|
+
|
|
142
|
+
for path in paths_to_check:
|
|
134
143
|
# Skip directories, hidden files, and non-code files
|
|
135
144
|
if not path.is_file():
|
|
136
145
|
continue
|
|
@@ -162,8 +171,10 @@ def get_repo_map(
|
|
|
162
171
|
|
|
163
172
|
if structure is None:
|
|
164
173
|
# Generate structure
|
|
174
|
+
# Pass _internal_call=True so code_structure doesn't count as an operation
|
|
175
|
+
# This prevents repo_map from using thousands of operations in large repos
|
|
165
176
|
try:
|
|
166
|
-
structure = code_structure(str(rel_path), max_symbols=20)
|
|
177
|
+
structure = code_structure(str(rel_path), max_symbols=20, _internal_call=True)
|
|
167
178
|
if structure and not structure.startswith("❌"):
|
|
168
179
|
# Extract just the essential parts (remove hints and verbose info)
|
|
169
180
|
lines = structure.split("\n")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchpal
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.0
|
|
4
4
|
Summary: An agentic coding and automation assistant, supporting both local and cloud LLMs
|
|
5
5
|
Author: PatchPal Contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -177,6 +177,18 @@ While originally designed for software development, PatchPal is also a general-p
|
|
|
177
177
|
2. PatchPal includes a [unique guardrails system](https://amaiya.github.io/patchpal/safety/) that is better suited to privacy-conscious use cases involving sensitive data.
|
|
178
178
|
3. We needed an agent harness that seamlessly works with [both local and cloud models](https://amaiya.github.io/patchpal/models/overview/#supported-models), including AWS GovCloud Bedrock models.
|
|
179
179
|
|
|
180
|
+
> On Windows Subsystem for Linux (WSL), why is it stalling intermittently at "Thinking..."?
|
|
181
|
+
|
|
182
|
+
This is a [known issue](https://github.com/microsoft/WSL/issues/6264#issuecomment-762154193) with WSL2.
|
|
183
|
+
|
|
184
|
+
Try examining and then lowering the `mtu`:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
$ cat /sys/class/net/eth1/mtu
|
|
188
|
+
1427
|
|
189
|
+
|
|
190
|
+
$ sudo ip link set eth1 mtu 1400
|
|
191
|
+
```
|
|
180
192
|
|
|
181
193
|
## Documentation
|
|
182
194
|
|
|
@@ -14,6 +14,7 @@ patchpal.egg-info/entry_points.txt
|
|
|
14
14
|
patchpal.egg-info/requires.txt
|
|
15
15
|
patchpal.egg-info/top_level.txt
|
|
16
16
|
patchpal/agent/__init__.py
|
|
17
|
+
patchpal/agent/bedrock_profile_utils.py
|
|
17
18
|
patchpal/agent/function_calling.py
|
|
18
19
|
patchpal/agent/react.py
|
|
19
20
|
patchpal/cli/__init__.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|