code-puppy 0.0.171__py3-none-any.whl → 0.0.173__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 (70) hide show
  1. code_puppy/agent.py +8 -8
  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 +398 -2
  5. code_puppy/command_line/command_handler.py +68 -28
  6. code_puppy/command_line/mcp/add_command.py +2 -2
  7. code_puppy/command_line/mcp/base.py +1 -1
  8. code_puppy/command_line/mcp/install_command.py +2 -2
  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 +141 -12
  17. code_puppy/http_utils.py +50 -24
  18. code_puppy/main.py +2 -1
  19. code_puppy/{mcp → mcp_}/config_wizard.py +1 -1
  20. code_puppy/{mcp → mcp_}/examples/retry_example.py +1 -1
  21. code_puppy/{mcp → mcp_}/managed_server.py +1 -1
  22. code_puppy/{mcp → mcp_}/server_registry_catalog.py +1 -3
  23. code_puppy/message_history_processor.py +83 -221
  24. code_puppy/messaging/message_queue.py +4 -4
  25. code_puppy/state_management.py +1 -100
  26. code_puppy/tools/__init__.py +103 -6
  27. code_puppy/tools/browser/__init__.py +0 -0
  28. code_puppy/tools/browser/browser_control.py +293 -0
  29. code_puppy/tools/browser/browser_interactions.py +552 -0
  30. code_puppy/tools/browser/browser_locators.py +642 -0
  31. code_puppy/tools/browser/browser_navigation.py +251 -0
  32. code_puppy/tools/browser/browser_screenshot.py +242 -0
  33. code_puppy/tools/browser/browser_scripts.py +478 -0
  34. code_puppy/tools/browser/browser_workflows.py +196 -0
  35. code_puppy/tools/browser/camoufox_manager.py +194 -0
  36. code_puppy/tools/browser/vqa_agent.py +66 -0
  37. code_puppy/tools/browser_control.py +293 -0
  38. code_puppy/tools/browser_interactions.py +552 -0
  39. code_puppy/tools/browser_locators.py +642 -0
  40. code_puppy/tools/browser_navigation.py +251 -0
  41. code_puppy/tools/browser_screenshot.py +278 -0
  42. code_puppy/tools/browser_scripts.py +478 -0
  43. code_puppy/tools/browser_workflows.py +215 -0
  44. code_puppy/tools/camoufox_manager.py +150 -0
  45. code_puppy/tools/command_runner.py +13 -8
  46. code_puppy/tools/file_operations.py +7 -7
  47. code_puppy/tui/app.py +1 -1
  48. code_puppy/tui/components/custom_widgets.py +1 -1
  49. code_puppy/tui/screens/mcp_install_wizard.py +8 -8
  50. code_puppy/tui_state.py +55 -0
  51. {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/METADATA +3 -1
  52. code_puppy-0.0.173.dist-info/RECORD +132 -0
  53. code_puppy-0.0.171.dist-info/RECORD +0 -112
  54. /code_puppy/{mcp → mcp_}/__init__.py +0 -0
  55. /code_puppy/{mcp → mcp_}/async_lifecycle.py +0 -0
  56. /code_puppy/{mcp → mcp_}/blocking_startup.py +0 -0
  57. /code_puppy/{mcp → mcp_}/captured_stdio_server.py +0 -0
  58. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  59. /code_puppy/{mcp → mcp_}/dashboard.py +0 -0
  60. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  61. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  62. /code_puppy/{mcp → mcp_}/manager.py +0 -0
  63. /code_puppy/{mcp → mcp_}/registry.py +0 -0
  64. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  65. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  66. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  67. {code_puppy-0.0.171.data → code_puppy-0.0.173.data}/data/code_puppy/models.json +0 -0
  68. {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/WHEEL +0 -0
  69. {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/entry_points.txt +0 -0
  70. {code_puppy-0.0.171.dist-info → code_puppy-0.0.173.dist-info}/licenses/LICENSE +0 -0
@@ -81,7 +81,9 @@ def get_commands_help():
81
81
  )
82
82
  help_lines.append(
83
83
  Text("/truncate", style="cyan")
84
- + Text(" <N> Truncate message history to N most recent messages (keeping system message)")
84
+ + Text(
85
+ " <N> Truncate message history to N most recent messages (keeping system message)"
86
+ )
85
87
  )
86
88
  help_lines.append(
87
89
  Text("/<unknown>", style="cyan")
@@ -409,23 +411,33 @@ def handle_command(command: str):
409
411
 
410
412
  if command.startswith("/pin_model"):
411
413
  # Handle agent model pinning
414
+ import json
415
+
412
416
  from code_puppy.agents.json_agent import discover_json_agents
413
417
  from code_puppy.command_line.model_picker_completion import load_model_names
414
- import json
415
418
 
416
419
  tokens = command.split()
417
420
 
418
421
  if len(tokens) != 3:
419
422
  emit_warning("Usage: /pin_model <agent-name> <model-name>")
420
423
 
421
- # Show available models and JSON agents
424
+ # Show available models and agents
422
425
  available_models = load_model_names()
423
426
  json_agents = discover_json_agents()
424
427
 
428
+ # Get built-in agents
429
+ from code_puppy.agents.agent_manager import get_agent_descriptions
430
+ builtin_agents = get_agent_descriptions()
431
+
425
432
  emit_info("Available models:")
426
433
  for model in available_models:
427
434
  emit_info(f" [cyan]{model}[/cyan]")
428
435
 
436
+ if builtin_agents:
437
+ emit_info("\nAvailable built-in agents:")
438
+ for agent_name, description in builtin_agents.items():
439
+ emit_info(f" [cyan]{agent_name}[/cyan] - {description}")
440
+
429
441
  if json_agents:
430
442
  emit_info("\nAvailable JSON agents:")
431
443
  for agent_name, agent_path in json_agents.items():
@@ -442,31 +454,51 @@ def handle_command(command: str):
442
454
  emit_warning(f"Available models: {', '.join(available_models)}")
443
455
  return True
444
456
 
445
- # Check that we're modifying a JSON agent (not a built-in Python agent)
457
+ # Check if this is a JSON agent or a built-in Python agent
446
458
  json_agents = discover_json_agents()
447
- if agent_name not in json_agents:
448
- emit_error(f"JSON agent '{agent_name}' not found")
449
459
 
450
- # Show available JSON agents
460
+ # Get list of available built-in agents
461
+ from code_puppy.agents.agent_manager import get_agent_descriptions
462
+ builtin_agents = get_agent_descriptions()
463
+
464
+ is_json_agent = agent_name in json_agents
465
+ is_builtin_agent = agent_name in builtin_agents
466
+
467
+ if not is_json_agent and not is_builtin_agent:
468
+ emit_error(f"Agent '{agent_name}' not found")
469
+
470
+ # Show available agents
471
+ if builtin_agents:
472
+ emit_info("Available built-in agents:")
473
+ for name, desc in builtin_agents.items():
474
+ emit_info(f" [cyan]{name}[/cyan] - {desc}")
475
+
451
476
  if json_agents:
452
- emit_info("Available JSON agents:")
477
+ emit_info("\nAvailable JSON agents:")
453
478
  for name, path in json_agents.items():
454
479
  emit_info(f" [cyan]{name}[/cyan] ({path})")
455
480
  return True
456
481
 
457
- agent_file_path = json_agents[agent_name]
458
-
459
- # Load, modify, and save the agent configuration
482
+ # Handle different agent types
460
483
  try:
461
- with open(agent_file_path, "r", encoding="utf-8") as f:
462
- agent_config = json.load(f)
484
+ if is_json_agent:
485
+ # Handle JSON agent - modify the JSON file
486
+ agent_file_path = json_agents[agent_name]
487
+
488
+ with open(agent_file_path, "r", encoding="utf-8") as f:
489
+ agent_config = json.load(f)
463
490
 
464
- # Set the model
465
- agent_config["model"] = model_name
491
+ # Set the model
492
+ agent_config["model"] = model_name
466
493
 
467
- # Save the updated configuration
468
- with open(agent_file_path, "w", encoding="utf-8") as f:
469
- json.dump(agent_config, f, indent=2, ensure_ascii=False)
494
+ # Save the updated configuration
495
+ with open(agent_file_path, "w", encoding="utf-8") as f:
496
+ json.dump(agent_config, f, indent=2, ensure_ascii=False)
497
+
498
+ else:
499
+ # Handle built-in Python agent - store in config
500
+ from code_puppy.config import set_agent_pinned_model
501
+ set_agent_pinned_model(agent_name, model_name)
470
502
 
471
503
  emit_success(f"Model '{model_name}' pinned to agent '{agent_name}'")
472
504
 
@@ -622,9 +654,11 @@ def handle_command(command: str):
622
654
  if command.startswith("/truncate"):
623
655
  tokens = command.split()
624
656
  if len(tokens) != 2:
625
- emit_error("Usage: /truncate <N> (where N is the number of messages to keep)")
657
+ emit_error(
658
+ "Usage: /truncate <N> (where N is the number of messages to keep)"
659
+ )
626
660
  return True
627
-
661
+
628
662
  try:
629
663
  n = int(tokens[1])
630
664
  if n < 1:
@@ -633,23 +667,29 @@ def handle_command(command: str):
633
667
  except ValueError:
634
668
  emit_error("N must be a valid integer")
635
669
  return True
636
-
670
+
637
671
  from code_puppy.state_management import get_message_history, set_message_history
638
-
672
+
639
673
  history = get_message_history()
640
674
  if not history:
641
675
  emit_warning("No history to truncate yet. Ask me something first!")
642
676
  return True
643
-
677
+
644
678
  if len(history) <= n:
645
- emit_info(f"History already has {len(history)} messages, which is <= {n}. Nothing to truncate.")
679
+ emit_info(
680
+ f"History already has {len(history)} messages, which is <= {n}. Nothing to truncate."
681
+ )
646
682
  return True
647
-
683
+
648
684
  # Always keep the first message (system message) and then keep the N-1 most recent messages
649
- truncated_history = [history[0]] + history[-(n-1):] if n > 1 else [history[0]]
650
-
685
+ truncated_history = (
686
+ [history[0]] + history[-(n - 1) :] if n > 1 else [history[0]]
687
+ )
688
+
651
689
  set_message_history(truncated_history)
652
- emit_success(f"Truncated message history from {len(history)} to {len(truncated_history)} messages (keeping system message and {n-1} most recent)")
690
+ emit_success(
691
+ f"Truncated message history from {len(history)} to {len(truncated_history)} messages (keeping system message and {n - 1} most recent)"
692
+ )
653
693
  return True
654
694
 
655
695
  if command in ("/exit", "/quit"):
@@ -8,7 +8,7 @@ import os
8
8
  from typing import List, Optional
9
9
 
10
10
  from code_puppy.messaging import emit_info
11
- from code_puppy.state_management import is_tui_mode
11
+ from code_puppy.tui_state import is_tui_mode
12
12
 
13
13
  from .base import MCPCommandBase
14
14
  from .wizard_utils import run_interactive_install_wizard
@@ -130,7 +130,7 @@ class AddCommand(MCPCommandBase):
130
130
  """
131
131
  try:
132
132
  from code_puppy.config import MCP_SERVERS_FILE
133
- from code_puppy.mcp.managed_server import ServerConfig
133
+ from code_puppy.mcp_.managed_server import ServerConfig
134
134
 
135
135
  # Extract required fields
136
136
  name = config_dict.pop("name")
@@ -8,7 +8,7 @@ import logging
8
8
 
9
9
  from rich.console import Console
10
10
 
11
- from code_puppy.mcp.manager import get_mcp_manager
11
+ from code_puppy.mcp_.manager import get_mcp_manager
12
12
 
13
13
  # Configure logging
14
14
  logger = logging.getLogger(__name__)
@@ -6,7 +6,7 @@ import logging
6
6
  from typing import List, Optional
7
7
 
8
8
  from code_puppy.messaging import emit_info
9
- from code_puppy.state_management import is_tui_mode
9
+ from code_puppy.tui_state import is_tui_mode
10
10
 
11
11
  from .base import MCPCommandBase
12
12
  from .wizard_utils import run_interactive_install_wizard
@@ -76,7 +76,7 @@ class InstallCommand(MCPCommandBase):
76
76
  def _install_from_catalog(self, server_name_or_id: str, group_id: str) -> bool:
77
77
  """Install a server directly from the catalog by name or ID."""
78
78
  try:
79
- from code_puppy.mcp.server_registry_catalog import catalog
79
+ from code_puppy.mcp_.server_registry_catalog import catalog
80
80
  from code_puppy.messaging import emit_prompt
81
81
 
82
82
  from .utils import find_server_id_by_name
@@ -8,7 +8,7 @@ from typing import List, Optional
8
8
  from rich.table import Table
9
9
  from rich.text import Text
10
10
 
11
- from code_puppy.mcp.managed_server import ServerState
11
+ from code_puppy.mcp_.managed_server import ServerState
12
12
  from code_puppy.messaging import emit_info
13
13
 
14
14
  from .base import MCPCommandBase
@@ -34,7 +34,7 @@ class SearchCommand(MCPCommandBase):
34
34
  group_id = self.generate_group_id()
35
35
 
36
36
  try:
37
- from code_puppy.mcp.server_registry_catalog import catalog
37
+ from code_puppy.mcp_.server_registry_catalog import catalog
38
38
 
39
39
  if not args:
40
40
  # Show popular servers if no query
@@ -6,7 +6,7 @@ import logging
6
6
  import time
7
7
  from typing import List, Optional
8
8
 
9
- from code_puppy.mcp.managed_server import ServerState
9
+ from code_puppy.mcp_.managed_server import ServerState
10
10
  from code_puppy.messaging import emit_info
11
11
 
12
12
  from .base import MCPCommandBase
@@ -8,7 +8,7 @@ from typing import List, Optional
8
8
 
9
9
  from rich.panel import Panel
10
10
 
11
- from code_puppy.mcp.managed_server import ServerState
11
+ from code_puppy.mcp_.managed_server import ServerState
12
12
  from code_puppy.messaging import emit_info
13
13
 
14
14
  from .base import MCPCommandBase
@@ -117,7 +117,7 @@ class StatusCommand(MCPCommandBase):
117
117
 
118
118
  # Check async lifecycle manager status if available
119
119
  try:
120
- from code_puppy.mcp.async_lifecycle import get_lifecycle_manager
120
+ from code_puppy.mcp_.async_lifecycle import get_lifecycle_manager
121
121
 
122
122
  lifecycle_mgr = get_lifecycle_manager()
123
123
  if lifecycle_mgr.is_running(server_id):
@@ -6,7 +6,7 @@ import logging
6
6
  import time
7
7
  from typing import List, Optional
8
8
 
9
- from code_puppy.mcp.managed_server import ServerState
9
+ from code_puppy.mcp_.managed_server import ServerState
10
10
  from code_puppy.messaging import emit_info
11
11
 
12
12
  from .base import MCPCommandBase
@@ -8,7 +8,7 @@ from typing import Optional
8
8
 
9
9
  from rich.text import Text
10
10
 
11
- from code_puppy.mcp.managed_server import ServerState
11
+ from code_puppy.mcp_.managed_server import ServerState
12
12
 
13
13
 
14
14
  def format_state_indicator(state: ServerState) -> Text:
@@ -118,7 +118,7 @@ def interactive_server_selection(group_id: str):
118
118
  # This is a simplified version - the full implementation would have
119
119
  # category browsing, search, etc. For now, we'll just show popular servers
120
120
  try:
121
- from code_puppy.mcp.server_registry_catalog import catalog
121
+ from code_puppy.mcp_.server_registry_catalog import catalog
122
122
 
123
123
  servers = catalog.get_popular(10)
124
124
  if not servers:
@@ -256,7 +256,7 @@ def install_server_from_catalog(
256
256
  import os
257
257
 
258
258
  from code_puppy.config import MCP_SERVERS_FILE
259
- from code_puppy.mcp.managed_server import ServerConfig
259
+ from code_puppy.mcp_.managed_server import ServerConfig
260
260
 
261
261
  # Set environment variables in the current environment
262
262
  for var, value in env_vars.items():
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
  """
@@ -156,9 +162,6 @@ def load_mcp_server_configs():
156
162
  return {}
157
163
 
158
164
 
159
- # Cache for model validation to prevent hitting ModelFactory on every call
160
- _model_validation_cache = {}
161
- _default_model_cache = None
162
165
 
163
166
 
164
167
  def _default_model_from_models_json():
@@ -169,30 +172,107 @@ def _default_model_from_models_json():
169
172
  """
170
173
  global _default_model_cache
171
174
 
172
- # Return cached default if we have one
173
175
  if _default_model_cache is not None:
174
176
  return _default_model_cache
175
177
 
176
178
  try:
177
- # Local import to avoid potential circular dependency on module import
178
179
  from code_puppy.model_factory import ModelFactory
179
180
 
180
181
  models_config = ModelFactory.load_config()
181
182
  if models_config:
182
- # Get the first key from the models config
183
183
  first_key = next(iter(models_config))
184
184
  _default_model_cache = first_key
185
185
  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"
186
+ _default_model_cache = "gpt-5"
187
+ return "gpt-5"
190
188
  except Exception:
191
- # Any problem (network, file missing, empty dict, etc.) => fall back to gpt-5
192
189
  _default_model_cache = "gpt-5"
193
190
  return "gpt-5"
194
191
 
195
192
 
193
+ def _default_vision_model_from_models_json() -> str:
194
+ """Select a default vision-capable model from models.json with caching."""
195
+ global _default_vision_model_cache
196
+
197
+ if _default_vision_model_cache is not None:
198
+ return _default_vision_model_cache
199
+
200
+ try:
201
+ from code_puppy.model_factory import ModelFactory
202
+
203
+ models_config = ModelFactory.load_config()
204
+ if models_config:
205
+ # Prefer explicitly tagged vision models
206
+ for name, config in models_config.items():
207
+ if config.get("supports_vision"):
208
+ _default_vision_model_cache = name
209
+ return name
210
+
211
+ # Fallback heuristic: common multimodal models
212
+ preferred_candidates = (
213
+ "gpt-4.1",
214
+ "gpt-4.1-mini",
215
+ "gpt-4.1-nano",
216
+ "claude-4-0-sonnet",
217
+ "gemini-2.5-flash-preview-05-20",
218
+ )
219
+ for candidate in preferred_candidates:
220
+ if candidate in models_config:
221
+ _default_vision_model_cache = candidate
222
+ return candidate
223
+
224
+ # Last resort: use the general default model
225
+ _default_vision_model_cache = _default_model_from_models_json()
226
+ return _default_vision_model_cache
227
+
228
+ _default_vision_model_cache = "gpt-4.1"
229
+ return "gpt-4.1"
230
+ except Exception:
231
+ _default_vision_model_cache = "gpt-4.1"
232
+ return "gpt-4.1"
233
+
234
+
235
+ def _default_vqa_model_from_models_json() -> str:
236
+ """Select a default VQA-capable model, preferring vision-ready options."""
237
+ global _default_vqa_model_cache
238
+
239
+ if _default_vqa_model_cache is not None:
240
+ return _default_vqa_model_cache
241
+
242
+ try:
243
+ from code_puppy.model_factory import ModelFactory
244
+
245
+ models_config = ModelFactory.load_config()
246
+ if models_config:
247
+ # Allow explicit VQA hints if present
248
+ for name, config in models_config.items():
249
+ if config.get("supports_vqa"):
250
+ _default_vqa_model_cache = name
251
+ return name
252
+
253
+ # Reuse multimodal heuristics before falling back to generic default
254
+ preferred_candidates = (
255
+ "gpt-4.1",
256
+ "gpt-4.1-mini",
257
+ "claude-4-0-sonnet",
258
+ "gemini-2.5-flash-preview-05-20",
259
+ "gpt-4.1-nano",
260
+ )
261
+ for candidate in preferred_candidates:
262
+ if candidate in models_config:
263
+ _default_vqa_model_cache = candidate
264
+ return candidate
265
+
266
+ _default_vqa_model_cache = _default_model_from_models_json()
267
+ return _default_vqa_model_cache
268
+
269
+ _default_vqa_model_cache = "gpt-4.1"
270
+ return "gpt-4.1"
271
+ except Exception:
272
+ _default_vqa_model_cache = "gpt-4.1"
273
+ return "gpt-4.1"
274
+
275
+
196
276
  def _validate_model_exists(model_name: str) -> bool:
197
277
  """Check if a model exists in models.json with caching to avoid redundant calls."""
198
278
  global _model_validation_cache
@@ -218,9 +298,11 @@ def _validate_model_exists(model_name: str) -> bool:
218
298
 
219
299
  def clear_model_cache():
220
300
  """Clear the model validation cache. Call this when models.json changes."""
221
- global _model_validation_cache, _default_model_cache
301
+ global _model_validation_cache, _default_model_cache, _default_vision_model_cache, _default_vqa_model_cache
222
302
  _model_validation_cache.clear()
223
303
  _default_model_cache = None
304
+ _default_vision_model_cache = None
305
+ _default_vqa_model_cache = None
224
306
 
225
307
 
226
308
  def get_model_name():
@@ -258,6 +340,20 @@ def set_model_name(model: str):
258
340
  clear_model_cache()
259
341
 
260
342
 
343
+ def get_vqa_model_name() -> str:
344
+ """Return the configured VQA model, falling back to an inferred default."""
345
+ stored_model = get_value("vqa_model_name")
346
+ if stored_model and _validate_model_exists(stored_model):
347
+ return stored_model
348
+ return _default_vqa_model_from_models_json()
349
+
350
+
351
+ def set_vqa_model_name(model: str):
352
+ """Persist the configured VQA model name and refresh caches."""
353
+ set_config_value("vqa_model_name", model or "")
354
+ clear_model_cache()
355
+
356
+
261
357
  def get_puppy_token():
262
358
  """Returns the puppy_token from config, or None if not set."""
263
359
  return get_value("puppy_token")
@@ -493,3 +589,36 @@ def save_command_to_history(command: str):
493
589
  f"❌ An unexpected error occurred while saving command history: {str(e)}"
494
590
  )
495
591
  direct_console.print(f"[bold red]{error_msg}[/bold red]")
592
+
593
+
594
+ def get_agent_pinned_model(agent_name: str) -> str:
595
+ """Get the pinned model for a specific agent.
596
+
597
+ Args:
598
+ agent_name: Name of the agent to get the pinned model for.
599
+
600
+ Returns:
601
+ Pinned model name, or None if no model is pinned for this agent.
602
+ """
603
+ return get_value(f"agent_model_{agent_name}")
604
+
605
+
606
+ def set_agent_pinned_model(agent_name: str, model_name: str):
607
+ """Set the pinned model for a specific agent.
608
+
609
+ Args:
610
+ agent_name: Name of the agent to pin the model for.
611
+ model_name: Model name to pin to this agent.
612
+ """
613
+ set_config_value(f"agent_model_{agent_name}", model_name)
614
+
615
+
616
+ def clear_agent_pinned_model(agent_name: str):
617
+ """Clear the pinned model for a specific agent.
618
+
619
+ Args:
620
+ agent_name: Name of the agent to clear the pinned model for.
621
+ """
622
+ # We can't easily delete keys from configparser, so set to empty string
623
+ # which will be treated as None by get_agent_pinned_model
624
+ 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:
code_puppy/main.py CHANGED
@@ -29,7 +29,8 @@ from code_puppy.message_history_processor import (
29
29
  message_history_accumulator,
30
30
  prune_interrupted_tool_calls,
31
31
  )
32
- from code_puppy.state_management import is_tui_mode, set_message_history, set_tui_mode
32
+ from code_puppy.state_management import set_message_history
33
+ from code_puppy.tui_state import is_tui_mode, set_tui_mode
33
34
  from code_puppy.tools.common import console
34
35
  from code_puppy.version_checker import default_version_mismatch_behavior
35
36