Henri Bonamy commited on
Commit
258ab19
·
1 Parent(s): 675af1a

color for output, tool output truncation, plan printing

Browse files
agent/core/agent_loop.py CHANGED
@@ -365,7 +365,7 @@ async def submission_loop(
365
 
366
  # Create session with tool router
367
  session = Session(event_queue, config=config, tool_router=tool_router)
368
- print("🤖 Agent loop started")
369
 
370
  # Main processing loop
371
  async with tool_router:
 
365
 
366
  # Create session with tool router
367
  session = Session(event_queue, config=config, tool_router=tool_router)
368
+ print("Agent loop started")
369
 
370
  # Main processing loop
371
  async with tool_router:
agent/main.py CHANGED
@@ -16,6 +16,16 @@ from agent.config import load_config
16
  from agent.core.agent_loop import submission_loop
17
  from agent.core.session import OpType
18
  from agent.core.tools import ToolRouter
 
 
 
 
 
 
 
 
 
 
19
 
20
  litellm.drop_params = True
21
 
@@ -24,9 +34,9 @@ if lmnr_api_key:
24
  try:
25
  Laminar.initialize(project_api_key=lmnr_api_key)
26
  litellm.callbacks = [LaminarLiteLLMCallback()]
27
- print("Laminar initialized")
28
  except Exception as e:
29
- print(f"⚠️ Failed to initialize Laminar: {e}")
30
 
31
 
32
  @dataclass
@@ -53,6 +63,7 @@ async def event_listener(
53
  ) -> None:
54
  """Background task that listens for events and displays them"""
55
  submission_id = [1000] # Use list to make it mutable in closure
 
56
 
57
  while True:
58
  try:
@@ -60,27 +71,32 @@ async def event_listener(
60
 
61
  # Display event
62
  if event.event_type == "ready":
63
- print("Agent ready")
64
  ready_event.set()
65
  elif event.event_type == "assistant_message":
66
  content = event.data.get("content", "") if event.data else ""
67
  if content:
68
- print(f"\n🤖 Assistant: {content}")
69
  elif event.event_type == "tool_call":
70
  tool_name = event.data.get("tool", "") if event.data else ""
71
  arguments = event.data.get("arguments", {}) if event.data else {}
72
  if tool_name:
73
- print(
74
- f"🔧 Calling tool: {tool_name} with arguments: {json.dumps(arguments)[:100]}..."
75
- )
76
  elif event.event_type == "tool_output":
77
  output = event.data.get("output", "") if event.data else ""
78
  success = event.data.get("success", False) if event.data else False
79
- status = "✅" if success else "❌"
80
  if output:
81
- print(f"{status} Tool output: {output}")
 
 
82
  elif event.event_type == "turn_complete":
83
- print("✅ Turn complete\n")
 
 
 
 
84
  turn_complete_event.set()
85
  elif event.event_type == "error":
86
  error = (
@@ -88,30 +104,26 @@ async def event_listener(
88
  if event.data
89
  else "Unknown error"
90
  )
91
- print(f"❌ Error: {error}")
92
  turn_complete_event.set()
93
  elif event.event_type == "shutdown":
94
- print("🛑 Agent shutdown")
95
  break
96
  elif event.event_type == "processing":
97
- print("Processing...", flush=True)
98
  elif event.event_type == "compacted":
99
  old_tokens = event.data.get("old_tokens", 0) if event.data else 0
100
  new_tokens = event.data.get("new_tokens", 0) if event.data else 0
101
- print(f"📦 Compacted context: {old_tokens} → {new_tokens} tokens")
102
  elif event.event_type == "approval_required":
103
  # Display job details and prompt for approval
104
  tool_name = event.data.get("tool", "") if event.data else ""
105
  arguments = event.data.get("arguments", {}) if event.data else {}
106
 
107
- print("\n" + "=" * 60)
108
- print("⚠️ JOB EXECUTION APPROVAL REQUIRED")
109
- print("=" * 60)
110
-
111
  operation = arguments.get("operation", "")
112
  args = arguments.get("args", {})
113
 
114
- print(f"Operation: {operation}")
115
 
116
  if operation == "uv":
117
  script = args.get("script", "")
@@ -135,9 +147,10 @@ async def event_listener(
135
  if secrets:
136
  print(f"Secrets: {', '.join(secrets)}")
137
 
138
- print("=" * 60)
139
-
140
  # Get user decision
 
 
 
141
  loop = asyncio.get_event_loop()
142
  response = await loop.run_in_executor(
143
  None,
@@ -161,13 +174,13 @@ async def event_listener(
161
  ),
162
  )
163
  await submission_queue.put(approval_submission)
164
- print("=" * 60 + "\n")
165
  # Silently ignore other events
166
 
167
  except asyncio.CancelledError:
168
  break
169
  except Exception as e:
170
- print(f"⚠️ Event listener error: {e}")
171
 
172
 
173
  async def get_user_input() -> str:
@@ -178,9 +191,13 @@ async def get_user_input() -> str:
178
 
179
  async def main():
180
  """Interactive chat with the agent"""
181
- print("=" * 60)
182
- print("🤖 Interactive Agent Chat")
183
- print("=" * 60)
 
 
 
 
184
  print("Type your messages below. Type 'exit', 'quit', or '/quit' to end.\n")
185
 
186
  # Create queues for communication
@@ -215,7 +232,7 @@ async def main():
215
  )
216
 
217
  # Wait for agent to initialize
218
- print("Initializing agent...")
219
  await ready_event.wait()
220
 
221
  submission_id = 0
@@ -253,7 +270,7 @@ async def main():
253
  await submission_queue.put(submission)
254
 
255
  except KeyboardInterrupt:
256
- print("\n\n⚠️ Interrupted by user")
257
 
258
  # Shutdown
259
  print("\n🛑 Shutting down agent...")
 
16
  from agent.core.agent_loop import submission_loop
17
  from agent.core.session import OpType
18
  from agent.core.tools import ToolRouter
19
+ from agent.utils.terminal_display import (
20
+ format_error,
21
+ format_header,
22
+ format_plan_display,
23
+ format_separator,
24
+ format_success,
25
+ format_tool_call,
26
+ format_tool_output,
27
+ format_turn_complete,
28
+ )
29
 
30
  litellm.drop_params = True
31
 
 
34
  try:
35
  Laminar.initialize(project_api_key=lmnr_api_key)
36
  litellm.callbacks = [LaminarLiteLLMCallback()]
37
+ print("Laminar initialized")
38
  except Exception as e:
39
+ print(f"Failed to initialize Laminar: {e}")
40
 
41
 
42
  @dataclass
 
63
  ) -> None:
64
  """Background task that listens for events and displays them"""
65
  submission_id = [1000] # Use list to make it mutable in closure
66
+ last_tool_name = [None] # Track last tool called
67
 
68
  while True:
69
  try:
 
71
 
72
  # Display event
73
  if event.event_type == "ready":
74
+ print(format_success("Agent ready"))
75
  ready_event.set()
76
  elif event.event_type == "assistant_message":
77
  content = event.data.get("content", "") if event.data else ""
78
  if content:
79
+ print(f"\nAssistant: {content}")
80
  elif event.event_type == "tool_call":
81
  tool_name = event.data.get("tool", "") if event.data else ""
82
  arguments = event.data.get("arguments", {}) if event.data else {}
83
  if tool_name:
84
+ last_tool_name[0] = tool_name # Store for tool_output event
85
+ args_str = json.dumps(arguments)[:100] + "..."
86
+ print(format_tool_call(tool_name, args_str))
87
  elif event.event_type == "tool_output":
88
  output = event.data.get("output", "") if event.data else ""
89
  success = event.data.get("success", False) if event.data else False
 
90
  if output:
91
+ # Don't truncate plan_tool output, truncate everything else
92
+ should_truncate = last_tool_name[0] != "plan_tool"
93
+ print(format_tool_output(output, success, truncate=should_truncate))
94
  elif event.event_type == "turn_complete":
95
+ print(format_turn_complete())
96
+ # Display plan after turn complete
97
+ plan_display = format_plan_display()
98
+ if plan_display:
99
+ print(plan_display)
100
  turn_complete_event.set()
101
  elif event.event_type == "error":
102
  error = (
 
104
  if event.data
105
  else "Unknown error"
106
  )
107
+ print(format_error(error))
108
  turn_complete_event.set()
109
  elif event.event_type == "shutdown":
110
+ print("Agent shutdown")
111
  break
112
  elif event.event_type == "processing":
113
+ print("Processing...", flush=True)
114
  elif event.event_type == "compacted":
115
  old_tokens = event.data.get("old_tokens", 0) if event.data else 0
116
  new_tokens = event.data.get("new_tokens", 0) if event.data else 0
117
+ print(f"Compacted context: {old_tokens} → {new_tokens} tokens")
118
  elif event.event_type == "approval_required":
119
  # Display job details and prompt for approval
120
  tool_name = event.data.get("tool", "") if event.data else ""
121
  arguments = event.data.get("arguments", {}) if event.data else {}
122
 
 
 
 
 
123
  operation = arguments.get("operation", "")
124
  args = arguments.get("args", {})
125
 
126
+ print(f"\nOperation: {operation}")
127
 
128
  if operation == "uv":
129
  script = args.get("script", "")
 
147
  if secrets:
148
  print(f"Secrets: {', '.join(secrets)}")
149
 
 
 
150
  # Get user decision
151
+ print("\n" + format_separator())
152
+ print(format_header("JOB EXECUTION APPROVAL REQUIRED"))
153
+ print(format_separator())
154
  loop = asyncio.get_event_loop()
155
  response = await loop.run_in_executor(
156
  None,
 
174
  ),
175
  )
176
  await submission_queue.put(approval_submission)
177
+ print(format_separator() + "\n")
178
  # Silently ignore other events
179
 
180
  except asyncio.CancelledError:
181
  break
182
  except Exception as e:
183
+ print(f"Event listener error: {e}")
184
 
185
 
186
  async def get_user_input() -> str:
 
191
 
192
  async def main():
193
  """Interactive chat with the agent"""
194
+ from agent.utils.terminal_display import Colors
195
+
196
+ # Clear screen
197
+ os.system('clear' if os.name != 'nt' else 'cls')
198
+
199
+ print(format_separator())
200
+ print(f"{Colors.YELLOW}{Colors.BOLD}\U0001F917 Hugging Face Agent{Colors.RESET}")
201
  print("Type your messages below. Type 'exit', 'quit', or '/quit' to end.\n")
202
 
203
  # Create queues for communication
 
232
  )
233
 
234
  # Wait for agent to initialize
235
+ print("Initializing agent...")
236
  await ready_event.wait()
237
 
238
  submission_id = 0
 
270
  await submission_queue.put(submission)
271
 
272
  except KeyboardInterrupt:
273
+ print("\n\nInterrupted by user")
274
 
275
  # Shutdown
276
  print("\n🛑 Shutting down agent...")
agent/tools/plan_tool.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Any, Dict, List
3
+ from .types import ToolResult
4
+ from agent.utils.terminal_display import format_plan_tool_output
5
+
6
+
7
+ # In-memory storage for the current plan (raw structure from agent)
8
+ _current_plan: List[Dict[str, str]] = []
9
+
10
+
11
+ class PlanTool:
12
+ """Tool for managing a list of todos with status tracking."""
13
+
14
+ def __init__(self):
15
+ pass
16
+
17
+ async def execute(self, params: Dict[str, Any]) -> ToolResult:
18
+ """
19
+ Execute the WritePlan operation.
20
+
21
+ Args:
22
+ params: Dictionary containing:
23
+ - todos: List of todo items, each with id, content, and status
24
+
25
+ Returns:
26
+ ToolResult with formatted output
27
+ """
28
+ global _current_plan
29
+
30
+ todos = params.get("todos", [])
31
+
32
+ # Validate todos structure
33
+ for todo in todos:
34
+ if not isinstance(todo, dict):
35
+ return {
36
+ "formatted": "Error: Each todo must be an object",
37
+ "isError": True,
38
+ }
39
+
40
+ required_fields = ["id", "content", "status"]
41
+ for field in required_fields:
42
+ if field not in todo:
43
+ return {
44
+ "formatted": f"Error: Todo missing required field '{field}'",
45
+ "isError": True,
46
+ }
47
+
48
+ # Validate status
49
+ valid_statuses = ["pending", "in_progress", "completed"]
50
+ if todo["status"] not in valid_statuses:
51
+ return {
52
+ "formatted": f"Error: Invalid status '{todo['status']}'. Must be one of: {', '.join(valid_statuses)}",
53
+ "isError": True,
54
+ }
55
+
56
+ # Store the raw todos structure in memory
57
+ _current_plan = todos
58
+
59
+ # Format only for display using terminal_display utility
60
+ formatted_output = format_plan_tool_output(todos)
61
+
62
+ return {
63
+ "formatted": formatted_output,
64
+ "totalResults": len(todos),
65
+ "isError": False,
66
+ }
67
+
68
+
69
+ def get_current_plan() -> List[Dict[str, str]]:
70
+ """Get the current plan (raw structure)."""
71
+ return _current_plan
72
+
73
+
74
+ # Tool specification
75
+ PLAN_TOOL_SPEC = {
76
+ "name": "plan_tool",
77
+ "description": "Manage a plan with a list of todos. Each call replaces the entire plan with the provided todos list.",
78
+ "parameters": {
79
+ "type": "object",
80
+ "properties": {
81
+ "todos": {
82
+ "type": "array",
83
+ "description": "List of todo items",
84
+ "items": {
85
+ "type": "object",
86
+ "properties": {
87
+ "id": {
88
+ "type": "string",
89
+ "description": "Unique identifier for the todo"
90
+ },
91
+ "content": {
92
+ "type": "string",
93
+ "description": "Description of the todo task"
94
+ },
95
+ "status": {
96
+ "type": "string",
97
+ "enum": ["pending", "in_progress", "completed"],
98
+ "description": "Current status of the todo"
99
+ }
100
+ },
101
+ "required": ["id", "content", "status"]
102
+ }
103
+ }
104
+ },
105
+ "required": ["todos"]
106
+ }
107
+ }
108
+
109
+
110
+ async def plan_tool_handler(arguments: Dict[str, Any]) -> tuple[str, bool]:
111
+ tool = PlanTool()
112
+ result = await tool.execute(arguments)
113
+ return result["formatted"], not result.get("isError", False)
agent/utils/terminal_display.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Terminal display utilities with colors and formatting
3
+ """
4
+
5
+
6
+ # ANSI color codes
7
+ class Colors:
8
+ RED = "\033[91m"
9
+ GREEN = "\033[92m"
10
+ YELLOW = "\033[93m"
11
+ BLUE = "\033[94m"
12
+ MAGENTA = "\033[95m"
13
+ CYAN = "\033[96m"
14
+ BOLD = "\033[1m"
15
+ UNDERLINE = "\033[4m"
16
+ RESET = "\033[0m"
17
+
18
+
19
+ def truncate_to_lines(text: str, max_lines: int = 6) -> str:
20
+ """Truncate text to max_lines, adding '...' if truncated"""
21
+ lines = text.split('\n')
22
+ if len(lines) <= max_lines:
23
+ return text
24
+ return '\n'.join(lines[:max_lines]) + f'\n{Colors.CYAN}... ({len(lines) - max_lines} more lines){Colors.RESET}'
25
+
26
+
27
+ def format_header(text: str, emoji: str = "") -> str:
28
+ """Format a header with bold"""
29
+ full_text = f"{emoji} {text}" if emoji else text
30
+ return f"{Colors.BOLD}{full_text}{Colors.RESET}"
31
+
32
+
33
+ def format_plan_display() -> str:
34
+ """Format the current plan for display (no colors, full visibility)"""
35
+ from agent.tools.plan_tool import get_current_plan
36
+
37
+ plan = get_current_plan()
38
+ if not plan:
39
+ return ""
40
+
41
+ lines = ["\n" + "=" * 60]
42
+ lines.append("CURRENT PLAN")
43
+ lines.append("=" * 60 + "\n")
44
+
45
+ # Group by status
46
+ completed = [t for t in plan if t["status"] == "completed"]
47
+ in_progress = [t for t in plan if t["status"] == "in_progress"]
48
+ pending = [t for t in plan if t["status"] == "pending"]
49
+
50
+ if completed:
51
+ lines.append("Completed:")
52
+ for todo in completed:
53
+ lines.append(f" [x] {todo['id']}. {todo['content']}")
54
+ lines.append("")
55
+
56
+ if in_progress:
57
+ lines.append("In Progress:")
58
+ for todo in in_progress:
59
+ lines.append(f" [~] {todo['id']}. {todo['content']}")
60
+ lines.append("")
61
+
62
+ if pending:
63
+ lines.append("Pending:")
64
+ for todo in pending:
65
+ lines.append(f" [ ] {todo['id']}. {todo['content']}")
66
+ lines.append("")
67
+
68
+ lines.append(f"Total: {len(plan)} todos ({len(completed)} completed, {len(in_progress)} in progress, {len(pending)} pending)")
69
+ lines.append("=" * 60 + "\n")
70
+
71
+ return '\n'.join(lines)
72
+
73
+
74
+ def format_error(message: str) -> str:
75
+ """Format an error message in red"""
76
+ return f"{Colors.RED}ERROR: {message}{Colors.RESET}"
77
+
78
+
79
+ def format_success(message: str, emoji: str = "") -> str:
80
+ """Format a success message in green"""
81
+ prefix = f"{emoji} " if emoji else ""
82
+ return f"{Colors.GREEN}{prefix}{message}{Colors.RESET}"
83
+
84
+
85
+ def format_tool_call(tool_name: str, arguments: str) -> str:
86
+ """Format a tool call message"""
87
+ return f"{Colors.YELLOW}Calling tool: {Colors.BOLD}{tool_name}{Colors.RESET}{Colors.YELLOW} with arguments: {arguments}{Colors.RESET}"
88
+
89
+
90
+ def format_tool_output(output: str, success: bool, truncate: bool = True) -> str:
91
+ """Format tool output with color and optional truncation"""
92
+ if truncate:
93
+ output = truncate_to_lines(output, max_lines=6)
94
+
95
+ if success:
96
+ return f"{Colors.YELLOW}Tool output:{Colors.RESET}\n{output}"
97
+ else:
98
+ return f"{Colors.RED}Tool output:{Colors.RESET}\n{output}"
99
+
100
+
101
+ def format_turn_complete() -> str:
102
+ """Format turn complete message in green with hugging face emoji"""
103
+ return f"{Colors.GREEN}{Colors.BOLD}\U0001F917 Turn complete{Colors.RESET}\n"
104
+
105
+
106
+ def format_separator(char: str = "=", length: int = 60) -> str:
107
+ """Format a separator line"""
108
+ return char * length
109
+
110
+
111
+ def format_plan_tool_output(todos: list) -> str:
112
+ """Format the plan tool output (no colors, full visibility)"""
113
+ if not todos:
114
+ return "Plan is empty."
115
+
116
+ lines = ["Plan updated successfully", ""]
117
+
118
+ # Group by status
119
+ completed = [t for t in todos if t["status"] == "completed"]
120
+ in_progress = [t for t in todos if t["status"] == "in_progress"]
121
+ pending = [t for t in todos if t["status"] == "pending"]
122
+
123
+ if completed:
124
+ lines.append("Completed:")
125
+ for todo in completed:
126
+ lines.append(f" [x] {todo['id']}. {todo['content']}")
127
+ lines.append("")
128
+
129
+ if in_progress:
130
+ lines.append("In Progress:")
131
+ for todo in in_progress:
132
+ lines.append(f" [~] {todo['id']}. {todo['content']}")
133
+ lines.append("")
134
+
135
+ if pending:
136
+ lines.append("Pending:")
137
+ for todo in pending:
138
+ lines.append(f" [ ] {todo['id']}. {todo['content']}")
139
+ lines.append("")
140
+
141
+ lines.append(f"Total: {len(todos)} todos ({len(completed)} completed, {len(in_progress)} in progress, {len(pending)} pending)")
142
+
143
+ return "\n".join(lines)