code-puppy 0.0.171__py3-none-any.whl → 0.0.172__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.
- code_puppy/agent.py +3 -3
- code_puppy/agents/agent_creator_agent.py +0 -3
- code_puppy/agents/agent_qa_kitten.py +203 -0
- code_puppy/agents/base_agent.py +9 -0
- code_puppy/command_line/command_handler.py +68 -28
- code_puppy/command_line/mcp/add_command.py +1 -1
- code_puppy/command_line/mcp/base.py +1 -1
- code_puppy/command_line/mcp/install_command.py +1 -1
- code_puppy/command_line/mcp/list_command.py +1 -1
- code_puppy/command_line/mcp/search_command.py +1 -1
- code_puppy/command_line/mcp/start_all_command.py +1 -1
- code_puppy/command_line/mcp/status_command.py +2 -2
- code_puppy/command_line/mcp/stop_all_command.py +1 -1
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +2 -2
- code_puppy/config.py +142 -12
- code_puppy/http_utils.py +50 -24
- code_puppy/{mcp → mcp_}/config_wizard.py +1 -1
- code_puppy/{mcp → mcp_}/examples/retry_example.py +1 -1
- code_puppy/{mcp → mcp_}/managed_server.py +1 -1
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +1 -3
- code_puppy/message_history_processor.py +1 -61
- code_puppy/state_management.py +4 -2
- code_puppy/tools/__init__.py +103 -6
- code_puppy/tools/browser/__init__.py +0 -0
- code_puppy/tools/browser/browser_control.py +293 -0
- code_puppy/tools/browser/browser_interactions.py +552 -0
- code_puppy/tools/browser/browser_locators.py +642 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +242 -0
- code_puppy/tools/browser/browser_scripts.py +478 -0
- code_puppy/tools/browser/browser_workflows.py +196 -0
- code_puppy/tools/browser/camoufox_manager.py +194 -0
- code_puppy/tools/browser/vqa_agent.py +66 -0
- code_puppy/tools/browser_control.py +293 -0
- code_puppy/tools/browser_interactions.py +552 -0
- code_puppy/tools/browser_locators.py +642 -0
- code_puppy/tools/browser_navigation.py +251 -0
- code_puppy/tools/browser_screenshot.py +278 -0
- code_puppy/tools/browser_scripts.py +478 -0
- code_puppy/tools/browser_workflows.py +215 -0
- code_puppy/tools/camoufox_manager.py +150 -0
- code_puppy/tools/command_runner.py +12 -7
- code_puppy/tools/file_operations.py +7 -7
- code_puppy/tui/components/custom_widgets.py +1 -1
- code_puppy/tui/screens/mcp_install_wizard.py +8 -8
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.172.dist-info}/METADATA +3 -1
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.172.dist-info}/RECORD +65 -46
- /code_puppy/{mcp → mcp_}/__init__.py +0 -0
- /code_puppy/{mcp → mcp_}/async_lifecycle.py +0 -0
- /code_puppy/{mcp → mcp_}/blocking_startup.py +0 -0
- /code_puppy/{mcp → mcp_}/captured_stdio_server.py +0 -0
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/dashboard.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/manager.py +0 -0
- /code_puppy/{mcp → mcp_}/registry.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.171.data → code_puppy-0.0.172.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.172.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.172.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.171.dist-info → code_puppy-0.0.172.dist-info}/licenses/LICENSE +0 -0
code_puppy/config.py
CHANGED
|
@@ -14,6 +14,12 @@ AGENTS_DIR = os.path.join(CONFIG_DIR, "agents")
|
|
|
14
14
|
DEFAULT_SECTION = "puppy"
|
|
15
15
|
REQUIRED_KEYS = ["puppy_name", "owner_name"]
|
|
16
16
|
|
|
17
|
+
# Cache containers for model validation and defaults
|
|
18
|
+
_model_validation_cache = {}
|
|
19
|
+
_default_model_cache = None
|
|
20
|
+
_default_vision_model_cache = None
|
|
21
|
+
_default_vqa_model_cache = None
|
|
22
|
+
|
|
17
23
|
|
|
18
24
|
def ensure_config_exists():
|
|
19
25
|
"""
|
|
@@ -109,6 +115,7 @@ def get_config_keys():
|
|
|
109
115
|
default_keys = [
|
|
110
116
|
"yolo_mode",
|
|
111
117
|
"model",
|
|
118
|
+
"vqa_model_name",
|
|
112
119
|
"compaction_strategy",
|
|
113
120
|
"protected_token_count",
|
|
114
121
|
"compaction_threshold",
|
|
@@ -156,9 +163,6 @@ def load_mcp_server_configs():
|
|
|
156
163
|
return {}
|
|
157
164
|
|
|
158
165
|
|
|
159
|
-
# Cache for model validation to prevent hitting ModelFactory on every call
|
|
160
|
-
_model_validation_cache = {}
|
|
161
|
-
_default_model_cache = None
|
|
162
166
|
|
|
163
167
|
|
|
164
168
|
def _default_model_from_models_json():
|
|
@@ -169,30 +173,107 @@ def _default_model_from_models_json():
|
|
|
169
173
|
"""
|
|
170
174
|
global _default_model_cache
|
|
171
175
|
|
|
172
|
-
# Return cached default if we have one
|
|
173
176
|
if _default_model_cache is not None:
|
|
174
177
|
return _default_model_cache
|
|
175
178
|
|
|
176
179
|
try:
|
|
177
|
-
# Local import to avoid potential circular dependency on module import
|
|
178
180
|
from code_puppy.model_factory import ModelFactory
|
|
179
181
|
|
|
180
182
|
models_config = ModelFactory.load_config()
|
|
181
183
|
if models_config:
|
|
182
|
-
# Get the first key from the models config
|
|
183
184
|
first_key = next(iter(models_config))
|
|
184
185
|
_default_model_cache = first_key
|
|
185
186
|
return first_key
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
_default_model_cache = "gpt-5"
|
|
189
|
-
return "gpt-5"
|
|
187
|
+
_default_model_cache = "gpt-5"
|
|
188
|
+
return "gpt-5"
|
|
190
189
|
except Exception:
|
|
191
|
-
# Any problem (network, file missing, empty dict, etc.) => fall back to gpt-5
|
|
192
190
|
_default_model_cache = "gpt-5"
|
|
193
191
|
return "gpt-5"
|
|
194
192
|
|
|
195
193
|
|
|
194
|
+
def _default_vision_model_from_models_json() -> str:
|
|
195
|
+
"""Select a default vision-capable model from models.json with caching."""
|
|
196
|
+
global _default_vision_model_cache
|
|
197
|
+
|
|
198
|
+
if _default_vision_model_cache is not None:
|
|
199
|
+
return _default_vision_model_cache
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
from code_puppy.model_factory import ModelFactory
|
|
203
|
+
|
|
204
|
+
models_config = ModelFactory.load_config()
|
|
205
|
+
if models_config:
|
|
206
|
+
# Prefer explicitly tagged vision models
|
|
207
|
+
for name, config in models_config.items():
|
|
208
|
+
if config.get("supports_vision"):
|
|
209
|
+
_default_vision_model_cache = name
|
|
210
|
+
return name
|
|
211
|
+
|
|
212
|
+
# Fallback heuristic: common multimodal models
|
|
213
|
+
preferred_candidates = (
|
|
214
|
+
"gpt-4.1",
|
|
215
|
+
"gpt-4.1-mini",
|
|
216
|
+
"gpt-4.1-nano",
|
|
217
|
+
"claude-4-0-sonnet",
|
|
218
|
+
"gemini-2.5-flash-preview-05-20",
|
|
219
|
+
)
|
|
220
|
+
for candidate in preferred_candidates:
|
|
221
|
+
if candidate in models_config:
|
|
222
|
+
_default_vision_model_cache = candidate
|
|
223
|
+
return candidate
|
|
224
|
+
|
|
225
|
+
# Last resort: use the general default model
|
|
226
|
+
_default_vision_model_cache = _default_model_from_models_json()
|
|
227
|
+
return _default_vision_model_cache
|
|
228
|
+
|
|
229
|
+
_default_vision_model_cache = "gpt-4.1"
|
|
230
|
+
return "gpt-4.1"
|
|
231
|
+
except Exception:
|
|
232
|
+
_default_vision_model_cache = "gpt-4.1"
|
|
233
|
+
return "gpt-4.1"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _default_vqa_model_from_models_json() -> str:
|
|
237
|
+
"""Select a default VQA-capable model, preferring vision-ready options."""
|
|
238
|
+
global _default_vqa_model_cache
|
|
239
|
+
|
|
240
|
+
if _default_vqa_model_cache is not None:
|
|
241
|
+
return _default_vqa_model_cache
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
from code_puppy.model_factory import ModelFactory
|
|
245
|
+
|
|
246
|
+
models_config = ModelFactory.load_config()
|
|
247
|
+
if models_config:
|
|
248
|
+
# Allow explicit VQA hints if present
|
|
249
|
+
for name, config in models_config.items():
|
|
250
|
+
if config.get("supports_vqa"):
|
|
251
|
+
_default_vqa_model_cache = name
|
|
252
|
+
return name
|
|
253
|
+
|
|
254
|
+
# Reuse multimodal heuristics before falling back to generic default
|
|
255
|
+
preferred_candidates = (
|
|
256
|
+
"gpt-4.1",
|
|
257
|
+
"gpt-4.1-mini",
|
|
258
|
+
"claude-4-0-sonnet",
|
|
259
|
+
"gemini-2.5-flash-preview-05-20",
|
|
260
|
+
"gpt-4.1-nano",
|
|
261
|
+
)
|
|
262
|
+
for candidate in preferred_candidates:
|
|
263
|
+
if candidate in models_config:
|
|
264
|
+
_default_vqa_model_cache = candidate
|
|
265
|
+
return candidate
|
|
266
|
+
|
|
267
|
+
_default_vqa_model_cache = _default_model_from_models_json()
|
|
268
|
+
return _default_vqa_model_cache
|
|
269
|
+
|
|
270
|
+
_default_vqa_model_cache = "gpt-4.1"
|
|
271
|
+
return "gpt-4.1"
|
|
272
|
+
except Exception:
|
|
273
|
+
_default_vqa_model_cache = "gpt-4.1"
|
|
274
|
+
return "gpt-4.1"
|
|
275
|
+
|
|
276
|
+
|
|
196
277
|
def _validate_model_exists(model_name: str) -> bool:
|
|
197
278
|
"""Check if a model exists in models.json with caching to avoid redundant calls."""
|
|
198
279
|
global _model_validation_cache
|
|
@@ -218,9 +299,11 @@ def _validate_model_exists(model_name: str) -> bool:
|
|
|
218
299
|
|
|
219
300
|
def clear_model_cache():
|
|
220
301
|
"""Clear the model validation cache. Call this when models.json changes."""
|
|
221
|
-
global _model_validation_cache, _default_model_cache
|
|
302
|
+
global _model_validation_cache, _default_model_cache, _default_vision_model_cache, _default_vqa_model_cache
|
|
222
303
|
_model_validation_cache.clear()
|
|
223
304
|
_default_model_cache = None
|
|
305
|
+
_default_vision_model_cache = None
|
|
306
|
+
_default_vqa_model_cache = None
|
|
224
307
|
|
|
225
308
|
|
|
226
309
|
def get_model_name():
|
|
@@ -258,6 +341,20 @@ def set_model_name(model: str):
|
|
|
258
341
|
clear_model_cache()
|
|
259
342
|
|
|
260
343
|
|
|
344
|
+
def get_vqa_model_name() -> str:
|
|
345
|
+
"""Return the configured VQA model, falling back to an inferred default."""
|
|
346
|
+
stored_model = get_value("vqa_model_name")
|
|
347
|
+
if stored_model and _validate_model_exists(stored_model):
|
|
348
|
+
return stored_model
|
|
349
|
+
return _default_vqa_model_from_models_json()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def set_vqa_model_name(model: str):
|
|
353
|
+
"""Persist the configured VQA model name and refresh caches."""
|
|
354
|
+
set_config_value("vqa_model_name", model or "")
|
|
355
|
+
clear_model_cache()
|
|
356
|
+
|
|
357
|
+
|
|
261
358
|
def get_puppy_token():
|
|
262
359
|
"""Returns the puppy_token from config, or None if not set."""
|
|
263
360
|
return get_value("puppy_token")
|
|
@@ -493,3 +590,36 @@ def save_command_to_history(command: str):
|
|
|
493
590
|
f"❌ An unexpected error occurred while saving command history: {str(e)}"
|
|
494
591
|
)
|
|
495
592
|
direct_console.print(f"[bold red]{error_msg}[/bold red]")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def get_agent_pinned_model(agent_name: str) -> str:
|
|
596
|
+
"""Get the pinned model for a specific agent.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
agent_name: Name of the agent to get the pinned model for.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Pinned model name, or None if no model is pinned for this agent.
|
|
603
|
+
"""
|
|
604
|
+
return get_value(f"agent_model_{agent_name}")
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def set_agent_pinned_model(agent_name: str, model_name: str):
|
|
608
|
+
"""Set the pinned model for a specific agent.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
agent_name: Name of the agent to pin the model for.
|
|
612
|
+
model_name: Model name to pin to this agent.
|
|
613
|
+
"""
|
|
614
|
+
set_config_value(f"agent_model_{agent_name}", model_name)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def clear_agent_pinned_model(agent_name: str):
|
|
618
|
+
"""Clear the pinned model for a specific agent.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
agent_name: Name of the agent to clear the pinned model for.
|
|
622
|
+
"""
|
|
623
|
+
# We can't easily delete keys from configparser, so set to empty string
|
|
624
|
+
# which will be treated as None by get_agent_pinned_model
|
|
625
|
+
set_config_value(f"agent_model_{agent_name}", "")
|
code_puppy/http_utils.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import Dict, Optional, Union
|
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
import requests
|
|
13
|
-
from tenacity import
|
|
13
|
+
from tenacity import stop_after_attempt, wait_exponential
|
|
14
14
|
|
|
15
15
|
try:
|
|
16
16
|
from pydantic_ai.retries import (
|
|
@@ -57,26 +57,32 @@ def create_client(
|
|
|
57
57
|
|
|
58
58
|
# If retry components are available, create a client with retry transport
|
|
59
59
|
if TenacityTransport and RetryConfig and wait_retry_after:
|
|
60
|
+
|
|
60
61
|
def should_retry_status(response):
|
|
61
62
|
"""Raise exceptions for retryable HTTP status codes."""
|
|
62
63
|
if response.status_code in retry_status_codes:
|
|
63
|
-
emit_info(
|
|
64
|
+
emit_info(
|
|
65
|
+
f"HTTP retry: Retrying request due to status code {response.status_code}"
|
|
66
|
+
)
|
|
64
67
|
response.raise_for_status()
|
|
65
68
|
|
|
66
69
|
transport = TenacityTransport(
|
|
67
70
|
config=RetryConfig(
|
|
68
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
71
|
+
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
72
|
+
and e.response.status_code in retry_status_codes,
|
|
69
73
|
wait=wait_retry_after(
|
|
70
74
|
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
71
|
-
max_wait=300
|
|
75
|
+
max_wait=300,
|
|
72
76
|
),
|
|
73
77
|
stop=stop_after_attempt(10),
|
|
74
|
-
reraise=True
|
|
78
|
+
reraise=True,
|
|
75
79
|
),
|
|
76
|
-
validate_response=should_retry_status
|
|
80
|
+
validate_response=should_retry_status,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return httpx.Client(
|
|
84
|
+
transport=transport, verify=verify, headers=headers or {}, timeout=timeout
|
|
77
85
|
)
|
|
78
|
-
|
|
79
|
-
return httpx.Client(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
|
|
80
86
|
else:
|
|
81
87
|
# Fallback to regular client if retry components are not available
|
|
82
88
|
return httpx.Client(verify=verify, headers=headers or {}, timeout=timeout)
|
|
@@ -93,26 +99,32 @@ def create_async_client(
|
|
|
93
99
|
|
|
94
100
|
# If retry components are available, create a client with retry transport
|
|
95
101
|
if AsyncTenacityTransport and RetryConfig and wait_retry_after:
|
|
102
|
+
|
|
96
103
|
def should_retry_status(response):
|
|
97
104
|
"""Raise exceptions for retryable HTTP status codes."""
|
|
98
105
|
if response.status_code in retry_status_codes:
|
|
99
|
-
emit_info(
|
|
106
|
+
emit_info(
|
|
107
|
+
f"HTTP retry: Retrying request due to status code {response.status_code}"
|
|
108
|
+
)
|
|
100
109
|
response.raise_for_status()
|
|
101
110
|
|
|
102
111
|
transport = AsyncTenacityTransport(
|
|
103
112
|
config=RetryConfig(
|
|
104
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
113
|
+
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
114
|
+
and e.response.status_code in retry_status_codes,
|
|
105
115
|
wait=wait_retry_after(
|
|
106
116
|
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
107
|
-
max_wait=300
|
|
117
|
+
max_wait=300,
|
|
108
118
|
),
|
|
109
119
|
stop=stop_after_attempt(10),
|
|
110
|
-
reraise=True
|
|
120
|
+
reraise=True,
|
|
111
121
|
),
|
|
112
|
-
validate_response=should_retry_status
|
|
122
|
+
validate_response=should_retry_status,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return httpx.AsyncClient(
|
|
126
|
+
transport=transport, verify=verify, headers=headers or {}, timeout=timeout
|
|
113
127
|
)
|
|
114
|
-
|
|
115
|
-
return httpx.AsyncClient(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
|
|
116
128
|
else:
|
|
117
129
|
# Fallback to regular client if retry components are not available
|
|
118
130
|
return httpx.AsyncClient(verify=verify, headers=headers or {}, timeout=timeout)
|
|
@@ -169,32 +181,44 @@ def create_reopenable_async_client(
|
|
|
169
181
|
|
|
170
182
|
# If retry components are available, create a client with retry transport
|
|
171
183
|
if AsyncTenacityTransport and RetryConfig and wait_retry_after:
|
|
184
|
+
|
|
172
185
|
def should_retry_status(response):
|
|
173
186
|
"""Raise exceptions for retryable HTTP status codes."""
|
|
174
187
|
if response.status_code in retry_status_codes:
|
|
175
|
-
emit_info(
|
|
188
|
+
emit_info(
|
|
189
|
+
f"HTTP retry: Retrying request due to status code {response.status_code}"
|
|
190
|
+
)
|
|
176
191
|
response.raise_for_status()
|
|
177
192
|
|
|
178
193
|
transport = AsyncTenacityTransport(
|
|
179
194
|
config=RetryConfig(
|
|
180
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
195
|
+
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
196
|
+
and e.response.status_code in retry_status_codes,
|
|
181
197
|
wait=wait_retry_after(
|
|
182
198
|
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
183
|
-
max_wait=300
|
|
199
|
+
max_wait=300,
|
|
184
200
|
),
|
|
185
201
|
stop=stop_after_attempt(10),
|
|
186
|
-
reraise=True
|
|
202
|
+
reraise=True,
|
|
187
203
|
),
|
|
188
|
-
validate_response=should_retry_status
|
|
204
|
+
validate_response=should_retry_status,
|
|
189
205
|
)
|
|
190
|
-
|
|
206
|
+
|
|
191
207
|
if ReopenableAsyncClient is not None:
|
|
192
208
|
return ReopenableAsyncClient(
|
|
193
|
-
transport=transport,
|
|
209
|
+
transport=transport,
|
|
210
|
+
verify=verify,
|
|
211
|
+
headers=headers or {},
|
|
212
|
+
timeout=timeout,
|
|
194
213
|
)
|
|
195
214
|
else:
|
|
196
215
|
# Fallback to regular AsyncClient if ReopenableAsyncClient is not available
|
|
197
|
-
return httpx.AsyncClient(
|
|
216
|
+
return httpx.AsyncClient(
|
|
217
|
+
transport=transport,
|
|
218
|
+
verify=verify,
|
|
219
|
+
headers=headers or {},
|
|
220
|
+
timeout=timeout,
|
|
221
|
+
)
|
|
198
222
|
else:
|
|
199
223
|
# Fallback to regular clients if retry components are not available
|
|
200
224
|
if ReopenableAsyncClient is not None:
|
|
@@ -203,7 +227,9 @@ def create_reopenable_async_client(
|
|
|
203
227
|
)
|
|
204
228
|
else:
|
|
205
229
|
# Fallback to regular AsyncClient if ReopenableAsyncClient is not available
|
|
206
|
-
return httpx.AsyncClient(
|
|
230
|
+
return httpx.AsyncClient(
|
|
231
|
+
verify=verify, headers=headers or {}, timeout=timeout
|
|
232
|
+
)
|
|
207
233
|
|
|
208
234
|
|
|
209
235
|
def is_cert_bundle_available() -> bool:
|
|
@@ -11,7 +11,7 @@ from urllib.parse import urlparse
|
|
|
11
11
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
|
|
14
|
-
from code_puppy.
|
|
14
|
+
from code_puppy.mcp_.manager import ServerConfig, get_mcp_manager
|
|
15
15
|
from code_puppy.messaging import (
|
|
16
16
|
emit_error,
|
|
17
17
|
emit_info,
|
|
@@ -17,7 +17,7 @@ from typing import Any
|
|
|
17
17
|
project_root = Path(__file__).parents[3]
|
|
18
18
|
sys.path.insert(0, str(project_root))
|
|
19
19
|
|
|
20
|
-
from code_puppy.
|
|
20
|
+
from code_puppy.mcp_.retry_manager import get_retry_manager, retry_mcp_call # noqa: E402
|
|
21
21
|
|
|
22
22
|
logger = logging.getLogger(__name__)
|
|
23
23
|
|
|
@@ -24,7 +24,7 @@ from pydantic_ai.mcp import (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
from code_puppy.http_utils import create_async_client
|
|
27
|
-
from code_puppy.
|
|
27
|
+
from code_puppy.mcp_.blocking_startup import BlockingMCPServerStdio
|
|
28
28
|
from code_puppy.messaging import emit_info
|
|
29
29
|
|
|
30
30
|
# Configure logging
|
|
@@ -794,9 +794,7 @@ MCP_SERVER_REGISTRY: List[MCPServerTemplate] = [
|
|
|
794
794
|
type="http",
|
|
795
795
|
config={
|
|
796
796
|
"url": "https://mcp.context7.com/mcp",
|
|
797
|
-
|
|
798
|
-
"Authorization": "Bearer $CONTEXT7_API_KEY"
|
|
799
|
-
}
|
|
797
|
+
"headers": {"Authorization": "Bearer $CONTEXT7_API_KEY"},
|
|
800
798
|
},
|
|
801
799
|
verified=True,
|
|
802
800
|
popular=True,
|
|
@@ -192,64 +192,6 @@ def split_messages_for_protected_summarization(
|
|
|
192
192
|
return messages_to_summarize, protected_messages
|
|
193
193
|
|
|
194
194
|
|
|
195
|
-
def deduplicate_tool_returns(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
196
|
-
"""
|
|
197
|
-
Remove duplicate tool returns while preserving the first occurrence for each tool_call_id.
|
|
198
|
-
|
|
199
|
-
This function identifies tool-return parts that share the same tool_call_id and
|
|
200
|
-
removes duplicates, keeping only the first return for each id. This prevents
|
|
201
|
-
conversation corruption from duplicate tool_result blocks.
|
|
202
|
-
"""
|
|
203
|
-
if not messages:
|
|
204
|
-
return messages
|
|
205
|
-
|
|
206
|
-
seen_tool_returns: Set[str] = set()
|
|
207
|
-
deduplicated: List[ModelMessage] = []
|
|
208
|
-
removed_count = 0
|
|
209
|
-
|
|
210
|
-
for msg in messages:
|
|
211
|
-
if not hasattr(msg, "parts") or not msg.parts:
|
|
212
|
-
deduplicated.append(msg)
|
|
213
|
-
continue
|
|
214
|
-
|
|
215
|
-
filtered_parts = []
|
|
216
|
-
msg_had_duplicates = False
|
|
217
|
-
|
|
218
|
-
for part in msg.parts:
|
|
219
|
-
tool_call_id = getattr(part, "tool_call_id", None)
|
|
220
|
-
if tool_call_id and _is_tool_return_part(part):
|
|
221
|
-
if tool_call_id in seen_tool_returns:
|
|
222
|
-
msg_had_duplicates = True
|
|
223
|
-
removed_count += 1
|
|
224
|
-
continue
|
|
225
|
-
seen_tool_returns.add(tool_call_id)
|
|
226
|
-
filtered_parts.append(part)
|
|
227
|
-
|
|
228
|
-
if not filtered_parts:
|
|
229
|
-
continue
|
|
230
|
-
|
|
231
|
-
if msg_had_duplicates:
|
|
232
|
-
new_msg = type(msg)(parts=filtered_parts)
|
|
233
|
-
for attr_name in dir(msg):
|
|
234
|
-
if (
|
|
235
|
-
not attr_name.startswith("_")
|
|
236
|
-
and attr_name != "parts"
|
|
237
|
-
and hasattr(msg, attr_name)
|
|
238
|
-
):
|
|
239
|
-
try:
|
|
240
|
-
setattr(new_msg, attr_name, getattr(msg, attr_name))
|
|
241
|
-
except (AttributeError, TypeError):
|
|
242
|
-
pass
|
|
243
|
-
deduplicated.append(new_msg)
|
|
244
|
-
else:
|
|
245
|
-
deduplicated.append(msg)
|
|
246
|
-
|
|
247
|
-
if removed_count > 0:
|
|
248
|
-
emit_warning(f"Removed {removed_count} duplicate tool-return part(s)")
|
|
249
|
-
|
|
250
|
-
return deduplicated
|
|
251
|
-
|
|
252
|
-
|
|
253
195
|
def summarize_messages(
|
|
254
196
|
messages: List[ModelMessage], with_protection: bool = True
|
|
255
197
|
) -> Tuple[List[ModelMessage], List[ModelMessage]]:
|
|
@@ -412,9 +354,7 @@ def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMess
|
|
|
412
354
|
|
|
413
355
|
|
|
414
356
|
def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
415
|
-
cleaned_history = prune_interrupted_tool_calls(
|
|
416
|
-
deduplicate_tool_returns(messages)
|
|
417
|
-
)
|
|
357
|
+
cleaned_history = prune_interrupted_tool_calls(messages)
|
|
418
358
|
|
|
419
359
|
total_current_tokens = sum(
|
|
420
360
|
estimate_tokens_for_message(msg) for msg in cleaned_history
|
code_puppy/state_management.py
CHANGED
|
@@ -4,6 +4,8 @@ from typing import Any, List, Set
|
|
|
4
4
|
|
|
5
5
|
import pydantic
|
|
6
6
|
|
|
7
|
+
from code_puppy.messaging import emit_info
|
|
8
|
+
|
|
7
9
|
_tui_mode: bool = False
|
|
8
10
|
_tui_app_instance: Any = None
|
|
9
11
|
|
|
@@ -138,8 +140,8 @@ def _stringify_part(part: Any) -> str:
|
|
|
138
140
|
attributes.append(f"content={json.dumps(content, sort_keys=True)}")
|
|
139
141
|
else:
|
|
140
142
|
attributes.append(f"content={repr(content)}")
|
|
141
|
-
|
|
142
|
-
return
|
|
143
|
+
result = "|".join(attributes)
|
|
144
|
+
return result
|
|
143
145
|
|
|
144
146
|
|
|
145
147
|
def hash_message(message: Any) -> int:
|
code_puppy/tools/__init__.py
CHANGED
|
@@ -1,20 +1,71 @@
|
|
|
1
1
|
from code_puppy.messaging import emit_warning
|
|
2
|
-
from code_puppy.tools.agent_tools import
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
from code_puppy.tools.agent_tools import register_invoke_agent, register_list_agents
|
|
3
|
+
|
|
4
|
+
# Browser automation tools
|
|
5
|
+
from code_puppy.tools.browser.browser_control import (
|
|
6
|
+
register_close_browser,
|
|
7
|
+
register_create_new_page,
|
|
8
|
+
register_get_browser_status,
|
|
9
|
+
register_initialize_browser,
|
|
10
|
+
register_list_pages,
|
|
11
|
+
)
|
|
12
|
+
from code_puppy.tools.browser.browser_interactions import (
|
|
13
|
+
register_browser_check,
|
|
14
|
+
register_browser_uncheck,
|
|
15
|
+
register_click_element,
|
|
16
|
+
register_double_click_element,
|
|
17
|
+
register_get_element_text,
|
|
18
|
+
register_get_element_value,
|
|
19
|
+
register_hover_element,
|
|
20
|
+
register_select_option,
|
|
21
|
+
register_set_element_text,
|
|
22
|
+
)
|
|
23
|
+
from code_puppy.tools.browser.browser_locators import (
|
|
24
|
+
register_find_buttons,
|
|
25
|
+
register_find_by_label,
|
|
26
|
+
register_find_by_placeholder,
|
|
27
|
+
register_find_by_role,
|
|
28
|
+
register_find_by_test_id,
|
|
29
|
+
register_find_by_text,
|
|
30
|
+
register_find_links,
|
|
31
|
+
register_run_xpath_query,
|
|
32
|
+
)
|
|
33
|
+
from code_puppy.tools.browser.browser_navigation import (
|
|
34
|
+
register_browser_go_back,
|
|
35
|
+
register_browser_go_forward,
|
|
36
|
+
register_get_page_info,
|
|
37
|
+
register_navigate_to_url,
|
|
38
|
+
register_reload_page,
|
|
39
|
+
register_wait_for_load_state,
|
|
40
|
+
)
|
|
41
|
+
from code_puppy.tools.browser.browser_screenshot import (
|
|
42
|
+
register_take_screenshot_and_analyze,
|
|
43
|
+
)
|
|
44
|
+
from code_puppy.tools.browser.browser_scripts import (
|
|
45
|
+
register_browser_clear_highlights,
|
|
46
|
+
register_browser_highlight_element,
|
|
47
|
+
register_execute_javascript,
|
|
48
|
+
register_scroll_page,
|
|
49
|
+
register_scroll_to_element,
|
|
50
|
+
register_set_viewport_size,
|
|
51
|
+
register_wait_for_element,
|
|
52
|
+
)
|
|
53
|
+
from code_puppy.tools.browser.browser_workflows import (
|
|
54
|
+
register_list_workflows,
|
|
55
|
+
register_read_workflow,
|
|
56
|
+
register_save_workflow,
|
|
5
57
|
)
|
|
6
58
|
from code_puppy.tools.command_runner import (
|
|
7
59
|
register_agent_run_shell_command,
|
|
8
60
|
register_agent_share_your_reasoning,
|
|
9
61
|
)
|
|
10
|
-
from code_puppy.tools.file_modifications import
|
|
62
|
+
from code_puppy.tools.file_modifications import register_delete_file, register_edit_file
|
|
11
63
|
from code_puppy.tools.file_operations import (
|
|
64
|
+
register_grep,
|
|
12
65
|
register_list_files,
|
|
13
66
|
register_read_file,
|
|
14
|
-
register_grep,
|
|
15
67
|
)
|
|
16
68
|
|
|
17
|
-
|
|
18
69
|
# Map of tool names to their individual registration functions
|
|
19
70
|
TOOL_REGISTRY = {
|
|
20
71
|
# Agent Tools
|
|
@@ -30,6 +81,52 @@ TOOL_REGISTRY = {
|
|
|
30
81
|
# Command Runner
|
|
31
82
|
"agent_run_shell_command": register_agent_run_shell_command,
|
|
32
83
|
"agent_share_your_reasoning": register_agent_share_your_reasoning,
|
|
84
|
+
# Browser Control
|
|
85
|
+
"browser_initialize": register_initialize_browser,
|
|
86
|
+
"browser_close": register_close_browser,
|
|
87
|
+
"browser_status": register_get_browser_status,
|
|
88
|
+
"browser_new_page": register_create_new_page,
|
|
89
|
+
"browser_list_pages": register_list_pages,
|
|
90
|
+
# Browser Navigation
|
|
91
|
+
"browser_navigate": register_navigate_to_url,
|
|
92
|
+
"browser_get_page_info": register_get_page_info,
|
|
93
|
+
"browser_go_back": register_browser_go_back,
|
|
94
|
+
"browser_go_forward": register_browser_go_forward,
|
|
95
|
+
"browser_reload": register_reload_page,
|
|
96
|
+
"browser_wait_for_load": register_wait_for_load_state,
|
|
97
|
+
# Browser Element Discovery
|
|
98
|
+
"browser_find_by_role": register_find_by_role,
|
|
99
|
+
"browser_find_by_text": register_find_by_text,
|
|
100
|
+
"browser_find_by_label": register_find_by_label,
|
|
101
|
+
"browser_find_by_placeholder": register_find_by_placeholder,
|
|
102
|
+
"browser_find_by_test_id": register_find_by_test_id,
|
|
103
|
+
"browser_xpath_query": register_run_xpath_query,
|
|
104
|
+
"browser_find_buttons": register_find_buttons,
|
|
105
|
+
"browser_find_links": register_find_links,
|
|
106
|
+
# Browser Element Interactions
|
|
107
|
+
"browser_click": register_click_element,
|
|
108
|
+
"browser_double_click": register_double_click_element,
|
|
109
|
+
"browser_hover": register_hover_element,
|
|
110
|
+
"browser_set_text": register_set_element_text,
|
|
111
|
+
"browser_get_text": register_get_element_text,
|
|
112
|
+
"browser_get_value": register_get_element_value,
|
|
113
|
+
"browser_select_option": register_select_option,
|
|
114
|
+
"browser_check": register_browser_check,
|
|
115
|
+
"browser_uncheck": register_browser_uncheck,
|
|
116
|
+
# Browser Scripts and Advanced Features
|
|
117
|
+
"browser_execute_js": register_execute_javascript,
|
|
118
|
+
"browser_scroll": register_scroll_page,
|
|
119
|
+
"browser_scroll_to_element": register_scroll_to_element,
|
|
120
|
+
"browser_set_viewport": register_set_viewport_size,
|
|
121
|
+
"browser_wait_for_element": register_wait_for_element,
|
|
122
|
+
"browser_highlight_element": register_browser_highlight_element,
|
|
123
|
+
"browser_clear_highlights": register_browser_clear_highlights,
|
|
124
|
+
# Browser Screenshots and VQA
|
|
125
|
+
"browser_screenshot_analyze": register_take_screenshot_and_analyze,
|
|
126
|
+
# Browser Workflows
|
|
127
|
+
"browser_save_workflow": register_save_workflow,
|
|
128
|
+
"browser_list_workflows": register_list_workflows,
|
|
129
|
+
"browser_read_workflow": register_read_workflow,
|
|
33
130
|
}
|
|
34
131
|
|
|
35
132
|
|
|
File without changes
|