open-swarm 0.1.1744967296__py3-none-any.whl → 0.1.1745017234__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.
@@ -119,24 +119,23 @@ class BlueprintBase(ABC):
119
119
  Defines the core interface for blueprint initialization and execution.
120
120
  """
121
121
  enable_terminal_commands: bool = False # By default, terminal command execution is disabled
122
-
123
- @classmethod
124
- def main(cls):
125
- """
126
- Standard CLI entry point for all blueprints.
127
- Subclasses can override metadata/config_path if needed.
128
- """
129
- from swarm.extensions.blueprint.cli_handler import run_blueprint_cli
130
- from pathlib import Path
131
- swarm_version = getattr(cls, "SWARM_VERSION", "1.0.0")
132
- config_path = getattr(cls, "DEFAULT_CONFIG_PATH", Path(__file__).parent / "swarm_config.json")
133
- run_blueprint_cli(cls, swarm_version=swarm_version, default_config_path=config_path)
122
+ approval_required: bool = False
123
+ console = Console()
124
+ session_logger: 'SessionLogger' = None
134
125
 
135
126
  def display_splash_screen(self, animated: bool = False):
136
127
  """Default splash screen. Subclasses can override for custom CLI/API branding."""
137
128
  console = Console()
138
129
  console.print(f"[bold cyan]Welcome to {self.__class__.__name__}![/]", style="bold")
139
130
 
131
+ def _load_configuration(self):
132
+ """
133
+ Loads blueprint configuration. This method is a stub for compatibility with tests that patch it.
134
+ In production, configuration is loaded via _load_and_process_config.
135
+ """
136
+ # You may override this in subclasses or patch in tests
137
+ return getattr(self, '_config', {})
138
+
140
139
  def __init__(self, blueprint_id: str, config: dict = None, config_path: 'Optional[Path]' = None, enable_terminal_commands: 'Optional[bool]' = None, **kwargs):
141
140
  try:
142
141
  if not blueprint_id:
@@ -195,9 +194,14 @@ class BlueprintBase(ABC):
195
194
  if _should_debug():
196
195
  logger.warning(f"Falling back to CLI/home config due to error: {e}")
197
196
  # 1. CLI argument (not handled here, handled in cli_handler)
198
- # 2. Current working directory
199
- cwd_config = Path.cwd() / "swarm_config.json"
200
- if cwd_config.exists():
197
+ # 2. Current working directory (guard against missing CWD)
198
+ try:
199
+ cwd_config = Path.cwd() / "swarm_config.json"
200
+ except Exception as e:
201
+ cwd_config = None
202
+ if _should_debug():
203
+ logger.warning(f"Unable to determine CWD for config lookup: {e}")
204
+ if cwd_config and cwd_config.exists():
201
205
  with open(cwd_config, 'r') as f:
202
206
  self._config = json.load(f)
203
207
  # 3. XDG_CONFIG_HOME or ~/.config/swarm/swarm_config.json
@@ -344,30 +348,36 @@ class BlueprintBase(ABC):
344
348
  if not hasattr(self, '_openai_client_cache'):
345
349
  self._openai_client_cache = {}
346
350
  if profile_name in self._model_instance_cache:
351
+ logger.debug(f"Using cached Model instance for profile '{profile_name}'.")
347
352
  return self._model_instance_cache[profile_name]
353
+ logger.debug(f"Creating new Model instance for profile '{profile_name}'.")
348
354
  profile_data = self.get_llm_profile(profile_name)
349
355
  import os
350
- model_name = os.getenv("LITELLM_MODEL") or os.getenv("DEFAULT_LLM") or profile_data.get("model") or "gpt-3.5-turbo"
351
- openai_kwargs = {}
352
- if "base_url" in profile_data:
353
- openai_kwargs["base_url"] = profile_data["base_url"]
354
- if "api_key" in profile_data:
355
- openai_kwargs["api_key"] = profile_data["api_key"]
356
- from openai import AsyncOpenAI
357
- from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
358
- client_cache_key = f"{model_name}:{openai_kwargs.get('base_url','')}:key={bool(openai_kwargs.get('api_key'))}"
356
+ # --- PATCH: API mode selection ---
357
+ # Default to 'completions' mode unless 'responses' is explicitly specified in swarm_config.json for this blueprint
358
+ api_mode = profile_data.get("api_mode") or self.config.get("api_mode") or "completions"
359
+ # Allow env override for debugging if needed
360
+ api_mode = os.getenv("SWARM_LLM_API_MODE", api_mode)
361
+ model_name = os.getenv("LITELLM_MODEL") or os.getenv("DEFAULT_LLM") or profile_data.get("model")
362
+ provider = profile_data.get("provider", "openai")
363
+ client_kwargs = { "api_key": profile_data.get("api_key"), "base_url": profile_data.get("base_url") }
364
+ filtered_kwargs = {k: v for k, v in client_kwargs.items() if v is not None}
365
+ log_kwargs = {k:v for k,v in filtered_kwargs.items() if k != 'api_key'}
366
+ logger.debug(f"Creating new AsyncOpenAI client for '{profile_name}' with {log_kwargs} and api_mode={api_mode}")
367
+ client_cache_key = f"{provider}_{profile_data.get('base_url')}_{api_mode}"
359
368
  if client_cache_key not in self._openai_client_cache:
360
- try:
361
- self._openai_client_cache[client_cache_key] = AsyncOpenAI(**openai_kwargs)
362
- except Exception as e:
363
- raise ValueError(f"Failed to init client: {e}") from e
369
+ from openai import AsyncOpenAI
370
+ self._openai_client_cache[client_cache_key] = AsyncOpenAI(**filtered_kwargs)
364
371
  client = self._openai_client_cache[client_cache_key]
365
- try:
372
+ # --- PATCH: Use correct model class based on api_mode ---
373
+ if api_mode == "responses":
374
+ from agents.models.openai_responses import OpenAIResponsesModel
375
+ model_instance = OpenAIResponsesModel(model=model_name, openai_client=client)
376
+ else:
377
+ from agents.models.openai_completions import OpenAIChatCompletionsModel
366
378
  model_instance = OpenAIChatCompletionsModel(model=model_name, openai_client=client)
367
- self._model_instance_cache[profile_name] = model_instance
368
- return model_instance
369
- except Exception as e:
370
- raise ValueError(f"Failed to init LLM: {e}") from e
379
+ self._model_instance_cache[profile_name] = model_instance
380
+ return model_instance
371
381
 
372
382
  def make_agent(self, name, instructions, tools, mcp_servers=None, **kwargs):
373
383
  """Factory for creating an Agent with the correct model instance from framework config."""
@@ -382,6 +392,60 @@ class BlueprintBase(ABC):
382
392
  **kwargs
383
393
  )
384
394
 
395
+ def request_approval(self, action_type, action_summary, action_details=None):
396
+ """
397
+ Prompt user for approval before executing an action.
398
+ Returns True if approved, False if rejected, or edited action if supported.
399
+ """
400
+ try:
401
+ from swarm.core.blueprint_ux import BlueprintUX
402
+ ux = BlueprintUX(style="serious")
403
+ box = ux.box(f"Approve {action_type}?", action_summary, summary="Details:", params=action_details)
404
+ self.console.print(box)
405
+ except Exception:
406
+ print(f"Approve {action_type}?\n{action_summary}\nDetails: {action_details}")
407
+ while True:
408
+ resp = input("Approve this action? [y]es/[n]o/[e]dit/[s]kip: ").strip().lower()
409
+ if resp in ("y", "yes"): return True
410
+ if resp in ("n", "no"): return False
411
+ if resp in ("s", "skip"): return False
412
+ if resp in ("e", "edit"):
413
+ if action_details:
414
+ print("Edit not yet implemented; skipping.")
415
+ return False
416
+ else:
417
+ print("No editable content; skipping.")
418
+ return False
419
+
420
+ def execute_tool_with_approval(self, tool_func, action_type, action_summary, action_details=None, *args, **kwargs):
421
+ if getattr(self, 'approval_required', False):
422
+ approved = self.request_approval(action_type, action_summary, action_details)
423
+ if not approved:
424
+ try:
425
+ self.console.print(f"[yellow]Skipped {action_type}[/yellow]")
426
+ except Exception:
427
+ print(f"Skipped {action_type}")
428
+ return None
429
+ return tool_func(*args, **kwargs)
430
+
431
+ def start_session_logger(self, blueprint_name: str, global_instructions: str = None, project_instructions: str = None):
432
+ from swarm.core.session_logger import SessionLogger
433
+ self.session_logger = SessionLogger(blueprint_name=blueprint_name)
434
+ self.session_logger.log_instructions(global_instructions, project_instructions)
435
+
436
+ def log_message(self, role: str, content: str):
437
+ if self.session_logger:
438
+ self.session_logger.log_message(role, content)
439
+
440
+ def log_tool_call(self, tool_name: str, result: str):
441
+ if self.session_logger:
442
+ self.session_logger.log_tool_call(tool_name, result)
443
+
444
+ def close_session_logger(self):
445
+ if self.session_logger:
446
+ self.session_logger.close()
447
+ self.session_logger = None
448
+
385
449
  @abstractmethod
386
450
  async def run(self, messages: List[Dict[str, Any]], **kwargs: Any) -> AsyncGenerator[Dict[str, Any], None]:
387
451
  """
@@ -0,0 +1,75 @@
1
+ import time
2
+ import itertools
3
+
4
+ # Style presets
5
+ def get_style(style):
6
+ if style == "serious":
7
+ return {
8
+ "border_top": "\033[1;34m╔" + "═"*50 + "╗\033[0m",
9
+ "border_bottom": "\033[1;34m╚" + "═"*50 + "╝\033[0m",
10
+ "border_side": "\033[1;34m║\033[0m",
11
+ "emoji": "🛠️",
12
+ "spinner": ['Generating.', 'Generating..', 'Generating...', 'Running...'],
13
+ "fallback": 'Generating... Taking longer than expected',
14
+ }
15
+ elif style == "silly":
16
+ return {
17
+ "border_top": "\033[1;35m(ノ◕ヮ◕)ノ*:・゚✧" + "~"*40 + "✧゚・: *ヽ(◕ヮ◕ヽ)\033[0m",
18
+ "border_bottom": "\033[1;35m(づ。◕‿‿◕。)づ" + "~"*40 + "づ(。◕‿‿◕。)づ\033[0m",
19
+ "border_side": "\033[1;35m~\033[0m",
20
+ "emoji": "🦆",
21
+ "spinner": ['Quacking.', 'Quacking..', 'Quacking...', 'Flapping...'],
22
+ "fallback": 'Quacking... Taking longer than expected',
23
+ }
24
+ else:
25
+ return get_style("serious")
26
+
27
+ class BlueprintUX:
28
+ def __init__(self, style="serious"):
29
+ self.style = style
30
+ self._style_conf = get_style(style)
31
+ self._spinner_cycle = itertools.cycle(self._style_conf["spinner"])
32
+ self._spinner_start = None
33
+
34
+ def box(self, title, content, summary=None, result_count=None, params=None):
35
+ lines = []
36
+ border_top = self._style_conf["border_top"]
37
+ border_bottom = self._style_conf["border_bottom"]
38
+ border_side = self._style_conf["border_side"]
39
+ emoji = self._style_conf["emoji"]
40
+ lines.append(border_top)
41
+ lines.append(f"{border_side} {emoji} {title:<46} {border_side}")
42
+ if summary:
43
+ lines.append(f"{border_side} {summary:<48} {border_side}")
44
+ if result_count is not None:
45
+ lines.append(f"{border_side} Results: {result_count:<41} {border_side}")
46
+ if params:
47
+ lines.append(f"{border_side} Params: {params:<41} {border_side}")
48
+ lines.append(f"{border_side}{'':<50}{border_side}")
49
+ for line in content.splitlines():
50
+ lines.append(f"{border_side} {line[:48]:<48} {border_side}")
51
+ lines.append(border_bottom)
52
+ return "\n".join(lines)
53
+
54
+ def spinner(self, state_idx, taking_long=False):
55
+ if taking_long:
56
+ return self._style_conf["fallback"]
57
+ spinner_states = self._style_conf["spinner"]
58
+ return spinner_states[state_idx % len(spinner_states)]
59
+
60
+ def summary(self, op_type, result_count, params):
61
+ return f"{op_type} | Results: {result_count} | Params: {params}"
62
+
63
+ def progress(self, current, total=None):
64
+ if total:
65
+ return f"Processed {current}/{total} lines..."
66
+ return f"Processed {current} lines..."
67
+
68
+ def code_vs_semantic(self, result_type, results):
69
+ if result_type == "code":
70
+ header = "[Code Search Results]"
71
+ elif result_type == "semantic":
72
+ header = "[Semantic Search Results]"
73
+ else:
74
+ header = "[Results]"
75
+ return f"{header}\n" + "\n".join(results)
@@ -10,11 +10,7 @@ from swarm.core.server_config import load_server_config, save_server_config
10
10
  from swarm.utils.color_utils import color_text
11
11
  from swarm.settings import DEBUG
12
12
  from swarm.core.utils.logger import *
13
- from swarm.extensions.cli.utils import (
14
- prompt_user,
15
- log_and_exit,
16
- display_message
17
- )
13
+ from swarm.extensions.cli.utils.prompt_user import prompt_user
18
14
 
19
15
  # Initialize logger for this module
20
16
  logger = logging.getLogger(__name__)
@@ -56,10 +52,10 @@ def backup_configuration(config_path: str) -> None:
56
52
  try:
57
53
  shutil.copy(config_path, backup_path)
58
54
  logger.info(f"Configuration backup created at '{backup_path}'")
59
- display_message(f"Backup of configuration created at '{backup_path}'", "info")
55
+ print(f"Backup of configuration created at '{backup_path}'")
60
56
  except Exception as e:
61
57
  logger.error(f"Failed to create configuration backup: {e}")
62
- display_message(f"Failed to create backup: {e}", "error")
58
+ print(f"Failed to create backup: {e}")
63
59
  sys.exit(1)
64
60
 
65
61
  def load_config(config_path: str) -> Dict[str, Any]:
@@ -82,11 +78,11 @@ def load_config(config_path: str) -> Dict[str, Any]:
82
78
  logger.debug(f"Raw configuration loaded: {config}")
83
79
  except FileNotFoundError:
84
80
  logger.error(f"Configuration file not found at {config_path}")
85
- display_message(f"Configuration file not found at {config_path}", "error")
81
+ print(f"Configuration file not found at {config_path}")
86
82
  sys.exit(1)
87
83
  except json.JSONDecodeError as e:
88
84
  logger.error(f"Invalid JSON in configuration file {config_path}: {e}")
89
- display_message(f"Invalid JSON in configuration file {config_path}: {e}", "error")
85
+ print(f"Invalid JSON in configuration file {config_path}: {e}")
90
86
  sys.exit(1)
91
87
 
92
88
  # Resolve placeholders recursively
@@ -95,7 +91,7 @@ def load_config(config_path: str) -> Dict[str, Any]:
95
91
  logger.debug(f"Configuration after resolving placeholders: {resolved_config}")
96
92
  except Exception as e:
97
93
  logger.error(f"Failed to resolve placeholders in configuration: {e}")
98
- display_message(f"Failed to resolve placeholders in configuration: {e}", "error")
94
+ print(f"Failed to resolve placeholders in configuration: {e}")
99
95
  sys.exit(1)
100
96
 
101
97
  return resolved_config
@@ -115,10 +111,10 @@ def save_config(config_path: str, config: Dict[str, Any]) -> None:
115
111
  with open(config_path, "w") as f:
116
112
  json.dump(config, f, indent=4)
117
113
  logger.info(f"Configuration saved to '{config_path}'")
118
- display_message(f"Configuration saved to '{config_path}'", "info")
114
+ print(f"Configuration saved to '{config_path}'")
119
115
  except Exception as e:
120
116
  logger.error(f"Failed to save configuration: {e}")
121
- display_message(f"Failed to save configuration: {e}", "error")
117
+ print(f"Failed to save configuration: {e}")
122
118
  sys.exit(1)
123
119
 
124
120
  def add_llm(config_path: str) -> None:
@@ -129,20 +125,20 @@ def add_llm(config_path: str) -> None:
129
125
  config_path (str): Path to the configuration file.
130
126
  """
131
127
  config = load_config(config_path)
132
- display_message("Starting the process to add a new LLM.", "info")
128
+ print("Starting the process to add a new LLM.")
133
129
 
134
130
  while True:
135
131
  llm_name = prompt_user("Enter the name of the new LLM (or type 'done' to finish)").strip()
136
- display_message(f"User entered LLM name: {llm_name}", "info")
132
+ print(f"User entered LLM name: {llm_name}")
137
133
  if llm_name.lower() == 'done':
138
- display_message("Finished adding LLMs.", "info")
134
+ print("Finished adding LLMs.")
139
135
  break
140
136
  if not llm_name:
141
- display_message("LLM name cannot be empty.", "error")
137
+ print("LLM name cannot be empty.")
142
138
  continue
143
139
 
144
140
  if llm_name in config.get("llm", {}):
145
- display_message(f"LLM '{llm_name}' already exists.", "warning")
141
+ print(f"LLM '{llm_name}' already exists.")
146
142
  continue
147
143
 
148
144
  llm = {}
@@ -158,16 +154,16 @@ def add_llm(config_path: str) -> None:
158
154
  temperature_input = prompt_user("Enter the temperature (e.g., 0.7)").strip()
159
155
  llm["temperature"] = float(temperature_input)
160
156
  except ValueError:
161
- display_message("Invalid temperature value. Using default 0.7.", "warning")
157
+ print("Invalid temperature value. Using default 0.7.")
162
158
  llm["temperature"] = 0.7
163
159
 
164
160
  config.setdefault("llm", {})[llm_name] = llm
165
161
  logger.info(f"Added LLM '{llm_name}' to configuration.")
166
- display_message(f"LLM '{llm_name}' added.", "info")
162
+ print(f"LLM '{llm_name}' added.")
167
163
 
168
164
  backup_configuration(config_path)
169
165
  save_config(config_path, config)
170
- display_message("LLM configuration process completed.", "info")
166
+ print("LLM configuration process completed.")
171
167
 
172
168
  def remove_llm(config_path: str, llm_name: str) -> None:
173
169
  """
@@ -180,18 +176,18 @@ def remove_llm(config_path: str, llm_name: str) -> None:
180
176
  config = load_config(config_path)
181
177
 
182
178
  if llm_name not in config.get("llm", {}):
183
- display_message(f"LLM '{llm_name}' does not exist.", "error")
179
+ print(f"LLM '{llm_name}' does not exist.")
184
180
  return
185
181
 
186
182
  confirm = prompt_user(f"Are you sure you want to remove LLM '{llm_name}'? (yes/no)").strip().lower()
187
183
  if confirm not in ['yes', 'y']:
188
- display_message("Operation cancelled.", "warning")
184
+ print("Operation cancelled.")
189
185
  return
190
186
 
191
187
  del config["llm"][llm_name]
192
188
  backup_configuration(config_path)
193
189
  save_config(config_path, config)
194
- display_message(f"LLM '{llm_name}' has been removed.", "info")
190
+ print(f"LLM '{llm_name}' has been removed.")
195
191
  logger.info(f"Removed LLM '{llm_name}' from configuration.")
196
192
 
197
193
  def add_mcp_server(config_path: str) -> None:
@@ -202,20 +198,20 @@ def add_mcp_server(config_path: str) -> None:
202
198
  config_path (str): Path to the configuration file.
203
199
  """
204
200
  config = load_config(config_path)
205
- display_message("Starting the process to add a new MCP server.", "info")
201
+ print("Starting the process to add a new MCP server.")
206
202
 
207
203
  while True:
208
204
  server_name = prompt_user("Enter the name of the new MCP server (or type 'done' to finish)").strip()
209
- display_message(f"User entered MCP server name: {server_name}", "info")
205
+ print(f"User entered MCP server name: {server_name}")
210
206
  if server_name.lower() == 'done':
211
- display_message("Finished adding MCP servers.", "info")
207
+ print("Finished adding MCP servers.")
212
208
  break
213
209
  if not server_name:
214
- display_message("Server name cannot be empty.", "error")
210
+ print("Server name cannot be empty.")
215
211
  continue
216
212
 
217
213
  if server_name in config.get("mcpServers", {}):
218
- display_message(f"MCP server '{server_name}' already exists.", "warning")
214
+ print(f"MCP server '{server_name}' already exists.")
219
215
  continue
220
216
 
221
217
  server = {}
@@ -226,7 +222,7 @@ def add_mcp_server(config_path: str) -> None:
226
222
  if not isinstance(server["args"], list):
227
223
  raise ValueError
228
224
  except ValueError:
229
- display_message("Invalid arguments format. Using an empty list.", "warning")
225
+ print("Invalid arguments format. Using an empty list.")
230
226
  server["args"] = []
231
227
 
232
228
  env_vars = {}
@@ -242,11 +238,11 @@ def add_mcp_server(config_path: str) -> None:
242
238
 
243
239
  config.setdefault("mcpServers", {})[server_name] = server
244
240
  logger.info(f"Added MCP server '{server_name}' to configuration.")
245
- display_message(f"MCP server '{server_name}' added.", "info")
241
+ print(f"MCP server '{server_name}' added.")
246
242
 
247
243
  backup_configuration(config_path)
248
244
  save_config(config_path, config)
249
- display_message("MCP server configuration process completed.", "info")
245
+ print("MCP server configuration process completed.")
250
246
 
251
247
  def remove_mcp_server(config_path: str, server_name: str) -> None:
252
248
  """
@@ -259,16 +255,16 @@ def remove_mcp_server(config_path: str, server_name: str) -> None:
259
255
  config = load_config(config_path)
260
256
 
261
257
  if server_name not in config.get("mcpServers", {}):
262
- display_message(f"MCP server '{server_name}' does not exist.", "error")
258
+ print(f"MCP server '{server_name}' does not exist.")
263
259
  return
264
260
 
265
261
  confirm = prompt_user(f"Are you sure you want to remove MCP server '{server_name}'? (yes/no)").strip().lower()
266
262
  if confirm not in ['yes', 'y']:
267
- display_message("Operation cancelled.", "warning")
263
+ print("Operation cancelled.")
268
264
  return
269
265
 
270
266
  del config["mcpServers"][server_name]
271
267
  backup_configuration(config_path)
272
268
  save_config(config_path, config)
273
- display_message(f"MCP server '{server_name}' has been removed.", "info")
269
+ print(f"MCP server '{server_name}' has been removed.")
274
270
  logger.info(f"Removed MCP server '{server_name}' from configuration.")
@@ -70,10 +70,10 @@ def ansi_box(title: str, content: str, color: str = "94", emoji: str = "🔎", b
70
70
  def print_search_box(title: str, content: str, color: str = "94", emoji: str = "🔎"):
71
71
  print(ansi_box(title, content, color=color, emoji=emoji))
72
72
 
73
- def pretty_print_response(messages: List[Dict[str, Any]], use_markdown: bool = False, spinner=None) -> None:
74
- """Format and print messages, optionally rendering assistant content as markdown."""
73
+ def pretty_print_response(messages: List[Dict[str, Any]], use_markdown: bool = False, spinner=None, agent_name: str = None) -> None:
74
+ """Format and print messages, optionally rendering assistant content as markdown, and always prefixing agent responses with the agent's name."""
75
75
  # --- DEBUG PRINT ---
76
- print(f"\n[DEBUG pretty_print_response called with {len(messages)} messages, use_markdown={use_markdown}]", flush=True)
76
+ print(f"\n[DEBUG pretty_print_response called with {len(messages)} messages, use_markdown={use_markdown}, agent_name={agent_name}]", flush=True)
77
77
 
78
78
  if spinner:
79
79
  spinner.stop()
@@ -85,7 +85,7 @@ def pretty_print_response(messages: List[Dict[str, Any]], use_markdown: bool = F
85
85
  return
86
86
 
87
87
  for i, msg in enumerate(messages):
88
- # --- DEBUG PRINT ---
88
+ # --- DEBUG PRINT ---
89
89
  print(f"\n[DEBUG Processing message {i}: type={type(msg)}]", flush=True)
90
90
  if not isinstance(msg, dict):
91
91
  print(f"[DEBUG Skipping non-dict message {i}]", flush=True)
@@ -98,20 +98,20 @@ def pretty_print_response(messages: List[Dict[str, Any]], use_markdown: bool = F
98
98
  # --- DEBUG PRINT ---
99
99
  print(f"[DEBUG Message {i}: role={role}, sender={sender}, has_content={bool(msg_content)}, has_tools={bool(tool_calls)}]", flush=True)
100
100
 
101
-
102
101
  if role == "assistant":
103
- print(f"\033[94m{sender}\033[0m: ", end="", flush=True)
102
+ # Use agent_name if provided, else sender, else 'assistant'
103
+ display_name = agent_name or sender or "assistant"
104
+ # Magenta for agent output
105
+ print(f"\033[95m[{display_name}]\033[0m: ", end="", flush=True)
104
106
  if msg_content:
105
- # --- DEBUG PRINT ---
107
+ # --- DEBUG PRINT ---
106
108
  print(f"\n[DEBUG Assistant content found, printing/rendering... Rich={RICH_AVAILABLE}, Markdown={use_markdown}]", flush=True)
107
109
  if use_markdown and RICH_AVAILABLE:
108
110
  render_markdown(msg_content)
109
111
  else:
110
- # --- DEBUG PRINT ---
111
- print(f"\n[DEBUG Using standard print for content:]", flush=True)
112
- print(msg_content, flush=True) # Added flush
112
+ print(msg_content, flush=True)
113
113
  elif not tool_calls:
114
- print(flush=True) # Flush newline if no content/tools
114
+ print(flush=True)
115
115
 
116
116
  if tool_calls and isinstance(tool_calls, list):
117
117
  print(" \033[92mTool Calls:\033[0m", flush=True)
@@ -0,0 +1,42 @@
1
+ import os
2
+ import datetime
3
+ from typing import Optional, List, Dict
4
+
5
+ class SessionLogger:
6
+ def __init__(self, blueprint_name: str, log_dir: Optional[str] = None):
7
+ if log_dir is None:
8
+ base_dir = os.path.dirname(__file__)
9
+ log_dir = os.path.join(base_dir, f"../blueprints/{blueprint_name}/session_logs")
10
+ os.makedirs(log_dir, exist_ok=True)
11
+ self.log_dir = log_dir
12
+ self.session_time = datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
13
+ self.log_path = os.path.join(log_dir, f"session_{self.session_time}.md")
14
+ self._open_log()
15
+
16
+ def _open_log(self):
17
+ self.log_file = open(self.log_path, "w")
18
+ self.log_file.write(f"# Session Log\n\nStarted: {self.session_time}\n\n")
19
+ self.log_file.flush()
20
+
21
+ def log_instructions(self, global_instructions: Optional[str], project_instructions: Optional[str]):
22
+ self.log_file.write("## Instructions\n")
23
+ if global_instructions:
24
+ self.log_file.write("### Global Instructions\n" + global_instructions + "\n\n")
25
+ if project_instructions:
26
+ self.log_file.write("### Project Instructions\n" + project_instructions + "\n\n")
27
+ self.log_file.write("## Messages\n")
28
+ self.log_file.flush()
29
+
30
+ def log_message(self, role: str, content: str, agent_name: str = None):
31
+ # Log agent name if provided, else fallback to role
32
+ display_name = agent_name or role
33
+ self.log_file.write(f"- **{display_name}**: {content}\n")
34
+ self.log_file.flush()
35
+
36
+ def log_tool_call(self, tool_name: str, result: str):
37
+ self.log_file.write(f"- **assistant (tool:{tool_name})**: {result}\n")
38
+ self.log_file.flush()
39
+
40
+ def close(self):
41
+ self.log_file.write(f"\nEnded: {datetime.datetime.now().strftime('%Y-%m-%dT%H-%M-%S')}\n")
42
+ self.log_file.close()
@@ -15,3 +15,48 @@ class SlashCommandRegistry:
15
15
  return self.commands.get(command)
16
16
 
17
17
  slash_registry = SlashCommandRegistry()
18
+ # Built-in '/help' slash command
19
+ @slash_registry.register('/help')
20
+ def _help_command(blueprint=None, args=None):
21
+ """List available slash commands."""
22
+ cmds = sorted(slash_registry.commands.keys())
23
+ return "Available slash commands:\n" + "\n".join(cmds)
24
+
25
+ # Built-in '/compact' slash command
26
+ @slash_registry.register('/compact')
27
+ def _compact_command(blueprint=None, args=None):
28
+ """Placeholder for compacting conversation context."""
29
+ return "[slash command] compact summary not implemented yet."
30
+
31
+ # Built-in '/model' slash command
32
+ @slash_registry.register('/model')
33
+ def _model_command(blueprint=None, args=None):
34
+ """Show or switch the current LLM model."""
35
+ if args:
36
+ return f"[slash command] model switch not implemented. Requested: {args}"
37
+ profile = getattr(blueprint, 'llm_profile_name', None)
38
+ return f"[slash command] current LLM profile: {profile or 'unknown'}"
39
+
40
+ # Built-in '/approval' slash command
41
+ @slash_registry.register('/approval')
42
+ def _approval_command(blueprint=None, args=None):
43
+ """Toggle or display auto-approval mode."""
44
+ return "[slash command] approval mode not implemented yet."
45
+
46
+ # Built-in '/history' slash command
47
+ @slash_registry.register('/history')
48
+ def _history_command(blueprint=None, args=None):
49
+ """Display session history of commands and files."""
50
+ return "[slash command] history not implemented yet."
51
+
52
+ # Built-in '/clear' slash command
53
+ @slash_registry.register('/clear')
54
+ def _clear_command(blueprint=None, args=None):
55
+ """Clear the screen and current context."""
56
+ return "[slash command] context cleared."
57
+
58
+ # Built-in '/clearhistory' slash command
59
+ @slash_registry.register('/clearhistory')
60
+ def _clearhistory_command(blueprint=None, args=None):
61
+ """Clear the command history."""
62
+ return "[slash command] command history cleared."