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.
Files changed (65) hide show
  1. code_puppy/agent.py +3 -3
  2. code_puppy/agents/agent_creator_agent.py +0 -3
  3. code_puppy/agents/agent_qa_kitten.py +203 -0
  4. code_puppy/agents/base_agent.py +9 -0
  5. code_puppy/command_line/command_handler.py +68 -28
  6. code_puppy/command_line/mcp/add_command.py +1 -1
  7. code_puppy/command_line/mcp/base.py +1 -1
  8. code_puppy/command_line/mcp/install_command.py +1 -1
  9. code_puppy/command_line/mcp/list_command.py +1 -1
  10. code_puppy/command_line/mcp/search_command.py +1 -1
  11. code_puppy/command_line/mcp/start_all_command.py +1 -1
  12. code_puppy/command_line/mcp/status_command.py +2 -2
  13. code_puppy/command_line/mcp/stop_all_command.py +1 -1
  14. code_puppy/command_line/mcp/utils.py +1 -1
  15. code_puppy/command_line/mcp/wizard_utils.py +2 -2
  16. code_puppy/config.py +142 -12
  17. code_puppy/http_utils.py +50 -24
  18. code_puppy/{mcp → mcp_}/config_wizard.py +1 -1
  19. code_puppy/{mcp → mcp_}/examples/retry_example.py +1 -1
  20. code_puppy/{mcp → mcp_}/managed_server.py +1 -1
  21. code_puppy/{mcp → mcp_}/server_registry_catalog.py +1 -3
  22. code_puppy/message_history_processor.py +1 -61
  23. code_puppy/state_management.py +4 -2
  24. code_puppy/tools/__init__.py +103 -6
  25. code_puppy/tools/browser/__init__.py +0 -0
  26. code_puppy/tools/browser/browser_control.py +293 -0
  27. code_puppy/tools/browser/browser_interactions.py +552 -0
  28. code_puppy/tools/browser/browser_locators.py +642 -0
  29. code_puppy/tools/browser/browser_navigation.py +251 -0
  30. code_puppy/tools/browser/browser_screenshot.py +242 -0
  31. code_puppy/tools/browser/browser_scripts.py +478 -0
  32. code_puppy/tools/browser/browser_workflows.py +196 -0
  33. code_puppy/tools/browser/camoufox_manager.py +194 -0
  34. code_puppy/tools/browser/vqa_agent.py +66 -0
  35. code_puppy/tools/browser_control.py +293 -0
  36. code_puppy/tools/browser_interactions.py +552 -0
  37. code_puppy/tools/browser_locators.py +642 -0
  38. code_puppy/tools/browser_navigation.py +251 -0
  39. code_puppy/tools/browser_screenshot.py +278 -0
  40. code_puppy/tools/browser_scripts.py +478 -0
  41. code_puppy/tools/browser_workflows.py +215 -0
  42. code_puppy/tools/camoufox_manager.py +150 -0
  43. code_puppy/tools/command_runner.py +12 -7
  44. code_puppy/tools/file_operations.py +7 -7
  45. code_puppy/tui/components/custom_widgets.py +1 -1
  46. code_puppy/tui/screens/mcp_install_wizard.py +8 -8
  47. {code_puppy-0.0.171.dist-info → code_puppy-0.0.172.dist-info}/METADATA +3 -1
  48. {code_puppy-0.0.171.dist-info → code_puppy-0.0.172.dist-info}/RECORD +65 -46
  49. /code_puppy/{mcp → mcp_}/__init__.py +0 -0
  50. /code_puppy/{mcp → mcp_}/async_lifecycle.py +0 -0
  51. /code_puppy/{mcp → mcp_}/blocking_startup.py +0 -0
  52. /code_puppy/{mcp → mcp_}/captured_stdio_server.py +0 -0
  53. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  54. /code_puppy/{mcp → mcp_}/dashboard.py +0 -0
  55. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  56. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  57. /code_puppy/{mcp → mcp_}/manager.py +0 -0
  58. /code_puppy/{mcp → mcp_}/registry.py +0 -0
  59. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  60. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  61. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  62. {code_puppy-0.0.171.data → code_puppy-0.0.172.data}/data/code_puppy/models.json +0 -0
  63. {code_puppy-0.0.171.dist-info → code_puppy-0.0.172.dist-info}/WHEEL +0 -0
  64. {code_puppy-0.0.171.dist-info → code_puppy-0.0.172.dist-info}/entry_points.txt +0 -0
  65. {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
- else:
187
- # If models_config is empty, fall back to gpt-5
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 retry_if_exception_type, stop_after_attempt, wait_exponential
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(f"HTTP retry: Retrying request due to status code {response.status_code}")
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) and e.response.status_code in retry_status_codes,
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(f"HTTP retry: Retrying request due to status code {response.status_code}")
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) and e.response.status_code in retry_status_codes,
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(f"HTTP retry: Retrying request due to status code {response.status_code}")
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) and e.response.status_code in retry_status_codes,
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, verify=verify, headers=headers or {}, timeout=timeout
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(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
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(verify=verify, headers=headers or {}, timeout=timeout)
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.mcp.manager import ServerConfig, get_mcp_manager
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.mcp.retry_manager import get_retry_manager, retry_mcp_call # noqa: E402
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.mcp.blocking_startup import BlockingMCPServerStdio
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
- "headers": {
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
@@ -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 "|".join(attributes)
143
+ result = "|".join(attributes)
144
+ return result
143
145
 
144
146
 
145
147
  def hash_message(message: Any) -> int:
@@ -1,20 +1,71 @@
1
1
  from code_puppy.messaging import emit_warning
2
- from code_puppy.tools.agent_tools import (
3
- register_list_agents,
4
- register_invoke_agent,
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 register_edit_file, register_delete_file
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