Aksel Joonas Reedi commited on
Commit
1dfd328
·
2 Parent(s): 7291bab83238f6

Merge branch 'main' into explore-tool

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/core/tools.py CHANGED
@@ -15,6 +15,7 @@ from mcp.types import EmbeddedResource, ImageContent, TextContent
15
  from agent.config import MCPServerConfig
16
  from agent.tools.jobs_tool import HF_JOBS_TOOL_SPEC, hf_jobs_handler
17
  from agent.tools.search_docs_tool import SEARCH_DOCS_TOOL_SPEC, search_docs_handler
 
18
 
19
  # Suppress aiohttp deprecation warning
20
  warnings.filterwarnings(
@@ -188,7 +189,7 @@ class ToolRouter:
188
  def create_builtin_tools() -> list[ToolSpec]:
189
  """Create built-in tool specifications"""
190
  print(
191
- f"Creating built-in tools: {HF_JOBS_TOOL_SPEC['name']}, {SEARCH_DOCS_TOOL_SPEC['name']}"
192
  )
193
  return [
194
  ToolSpec(
@@ -203,4 +204,10 @@ def create_builtin_tools() -> list[ToolSpec]:
203
  parameters=SEARCH_DOCS_TOOL_SPEC["parameters"],
204
  handler=search_docs_handler,
205
  ),
 
 
 
 
 
 
206
  ]
 
15
  from agent.config import MCPServerConfig
16
  from agent.tools.jobs_tool import HF_JOBS_TOOL_SPEC, hf_jobs_handler
17
  from agent.tools.search_docs_tool import SEARCH_DOCS_TOOL_SPEC, search_docs_handler
18
+ from agent.tools.plan_tool import PLAN_TOOL_SPEC, plan_tool_handler
19
 
20
  # Suppress aiohttp deprecation warning
21
  warnings.filterwarnings(
 
189
  def create_builtin_tools() -> list[ToolSpec]:
190
  """Create built-in tool specifications"""
191
  print(
192
+ f"Creating built-in tools: {HF_JOBS_TOOL_SPEC['name']}, {SEARCH_DOCS_TOOL_SPEC['name']}, {PLAN_TOOL_SPEC['name']}"
193
  )
194
  return [
195
  ToolSpec(
 
204
  parameters=SEARCH_DOCS_TOOL_SPEC["parameters"],
205
  handler=search_docs_handler,
206
  ),
207
+ ToolSpec(
208
+ ame=PLAN_TOOL_SPEC["name"],
209
+ description=PLAN_TOOL_SPEC["description"],
210
+ parameters=PLAN_TOOL_SPEC["parameters"],
211
+ handler=plan_tool_handler,
212
+ )
213
  ]
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,10 +191,26 @@ 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
187
  submission_queue = asyncio.Queue()
@@ -215,7 +244,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 +282,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("\U0001F917 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
+
200
+ banner = r"""
201
+ _ _ _ _____ _ _
202
+ | | | |_ _ __ _ __ _(_)_ __ __ _ | ___|_ _ ___ ___ / \ __ _ ___ _ __ | |_
203
+ | |_| | | | |/ _` |/ _` | | '_ \ / _` | | |_ / _` |/ __/ _ \ / _ \ / _` |/ _ \ '_ \| __|
204
+ | _ | |_| | (_| | (_| | | | | | (_| | | _| (_| | (_| __/ / ___ \ (_| | __/ | | | |_
205
+ |_| |_|\__,_|\__, |\__, |_|_| |_|\__, | |_| \__,_|\___\___| /_/ \_\__, |\___|_| |_|\__|
206
+ |___/ |___/ |___/ |___/
207
+ """
208
+
209
+
210
+ print(format_separator())
211
+ print(f"{Colors.YELLOW} {banner}{Colors.RESET}")
212
  print("Type your messages below. Type 'exit', 'quit', or '/quit' to end.\n")
213
+ print(format_separator())
214
 
215
  # Create queues for communication
216
  submission_queue = asyncio.Queue()
 
244
  )
245
 
246
  # Wait for agent to initialize
247
+ print("Initializing agent...")
248
  await ready_event.wait()
249
 
250
  submission_id = 0
 
282
  await submission_queue.put(submission)
283
 
284
  except KeyboardInterrupt:
285
+ print("\n\nInterrupted by user")
286
 
287
  # Shutdown
288
  print("\n🛑 Shutting down agent...")
agent/prompts/system_prompt.yaml CHANGED
@@ -1,5 +1,22 @@
1
  system_prompt: |
2
  You are HF Agent, a powerful AI assistant for Machine Learning Engineering, particularly training Large Language Models. You have access to {{ num_tools }} tools for interacting with Hugging Face Hub and performing ML tasks.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  # Available Tools
5
 
@@ -9,23 +26,8 @@ system_prompt: |
9
  - Spaces: Use and discover ML applications
10
  - Jobs: Manage compute jobs for training and inference
11
  - Image Generation: Generate and transform images
12
-
13
- # Agency
14
-
15
- You take initiative when the user asks you to do something, maintaining an appropriate balance between:
16
-
17
- 1. Doing the right thing when asked, including taking actions and follow-up actions
18
- 2. Not surprising the user with actions you take without asking
19
- 3. Not adding unnecessary explanations after completing tasks
20
-
21
- # Task Approach
22
-
23
- For ML engineering tasks:
24
- 1. Use all available tools to complete the task
25
- 2. Search for relevant models, datasets, and documentation on Hugging Face Hub
26
- 3. Leverage existing resources before creating new ones
27
- 4. Invoke multiple independent tools simultaneously for efficiency
28
-
29
  # Examples
30
 
31
  <example>
@@ -83,12 +85,16 @@ system_prompt: |
83
  # Conventions
84
 
85
  - Always search Hugging Face Hub for existing resources before suggesting custom implementations
 
 
 
86
  - When referencing models, datasets, or papers, include direct links from search results
87
  - Never assume a library is available - check documentation first
88
  - Before processing any dataset: inspect its actual structure first using the mcp__hf-mcp-server__hub_repo_details tool. Never assume column names: verify them beforehand.
89
  - Follow ML best practices: proper train/val/test splits, reproducibility, evaluation metrics
90
- - For training tasks, consider compute requirements and suggest appropriate hardware
91
- - Never expose or log API keys, tokens, or secrets
 
92
 
93
  # Communication Style
94
 
 
1
  system_prompt: |
2
  You are HF Agent, a powerful AI assistant for Machine Learning Engineering, particularly training Large Language Models. You have access to {{ num_tools }} tools for interacting with Hugging Face Hub and performing ML tasks.
3
+
4
+ # Task Approach
5
+
6
+ 1. Always formulate a plan. Pass the todos to the PlanTool. Update the plan as progress is made.
7
+ 2. Search for relevant models, datasets, and documentation on Hugging Face Hub.
8
+ 3. Use all available tools to complete the task. Leverage existing resources before creating new ones.
9
+ 4. Invoke multiple independent tools simultaneously for efficiency
10
+
11
+ # Autonomy / Subordinate trade-off.
12
+
13
+ Your main goal is to achieve what the user asked. For this:
14
+ 1. Take action, follow-up, launch jobs. Ask for as little action from the user as possible. Do not ask them to do things you could do via a script.
15
+
16
+ However !! :
17
+ 1. Don't surprise the user with costly, irreversible, or strange actions without asking.
18
+ 2. Don't be shy to ask questions if needed.
19
+ 3. Don't be overly talkative, explaining everything after a task ended.
20
 
21
  # Available Tools
22
 
 
26
  - Spaces: Use and discover ML applications
27
  - Jobs: Manage compute jobs for training and inference
28
  - Image Generation: Generate and transform images
29
+ - Planning : a planning/to-do tool.
30
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # Examples
32
 
33
  <example>
 
85
  # Conventions
86
 
87
  - Always search Hugging Face Hub for existing resources before suggesting custom implementations
88
+ - Keep in mind that a space is a repo, so you can create a space directly by uploading files that way. Repos should also be used to store files permanently : post-execution, files from jobs are not available.
89
+ - To run jobs, you must always pass the whole content of the file to execute. No files are available on server. Your local files and distant files are entirely seperate scopes.
90
+ - To access, create, or modify private Hub assets (spaces, private models, datasets, collections), pass `secrets: {% raw %}{{ "HF_TOKEN": "$HF_TOKEN" }}{% endraw %}` along with the jobs parameters. This is important. Without it, you will encounter authentification issues. Do not assume the user is connected on the jobs' server.
91
  - When referencing models, datasets, or papers, include direct links from search results
92
  - Never assume a library is available - check documentation first
93
  - Before processing any dataset: inspect its actual structure first using the mcp__hf-mcp-server__hub_repo_details tool. Never assume column names: verify them beforehand.
94
  - Follow ML best practices: proper train/val/test splits, reproducibility, evaluation metrics
95
+ - Unless absolutely necessary, don't ask user for action. This does not apply to follow-up questions you have.
96
+ - For training tasks, consider compute requirements and choose appropriate hardware.
97
+ - Never expose or log API keys, tokens, or secrets. Do not assume keys or secrets are available. Only Hugging Face private resources are available.
98
 
99
  # Communication Style
100
 
agent/tools/jobs_tool.py CHANGED
@@ -66,20 +66,21 @@ UV_DEFAULT_IMAGE = "ghcr.io/astral-sh/uv:python3.12-bookworm"
66
 
67
  def _substitute_hf_token(params: Dict[str, Any] | None) -> Dict[str, Any] | None:
68
  """
69
- Substitute $HF_TOKEN with actual token value from environment.
70
 
71
  Args:
72
- params: Dictionary that may contain "$HF_TOKEN" in values
73
 
74
  Returns:
75
- Dictionary with $HF_TOKEN substituted
76
  """
 
77
  if params is None:
78
  return None
79
 
80
  result = {}
81
  for key, value in params.items():
82
- if value == "$HF_TOKEN":
83
  result[key] = os.environ.get("HF_TOKEN", "")
84
  else:
85
  result[key] = value
@@ -354,7 +355,8 @@ Call this tool with:
354
  "operation": "uv",
355
  "args": {{
356
  "script": "import random\\nprint(42 + random.randint(1, 5))",
357
- "dependencies" : ["torch", "huggingface_hub"]
 
358
  }}
359
  }}
360
  ```
@@ -384,7 +386,7 @@ Call this tool with:
384
 
385
  - Jobs default to non-detached mode (stream logs until completion). Set `detach: true` to return immediately.
386
  - Prefer array commands to avoid shell parsing surprises
387
- - To access private Hub assets (spaces, private models, datasets, collections), pass `secrets: {{ "HF_TOKEN": "$HF_TOKEN" }}`
388
  - Before calling a job, think about dependencies (they must be specified), which hardware flavor to run on (choose simplest for task), and whether to include secrets.
389
  """
390
  return {"formatted": usage_text, "totalResults": 1, "resultsShared": 1}
@@ -448,7 +450,7 @@ To inspect, call this tool with `{{"operation": "inspect", "args": {{"job_id": "
448
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
449
 
450
  # Not detached - wait for completion and stream logs
451
- print(f"Job started: {job.id}")
452
  print("Streaming logs...\n---\n")
453
 
454
  final_status, all_logs = await self._wait_for_job_completion(
@@ -521,7 +523,7 @@ To check logs, call this tool with `{{"operation": "logs", "args": {{"job_id": "
521
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
522
 
523
  # Not detached - wait for completion and stream logs
524
- print(f"UV Job started: {job.id}")
525
  print("Streaming logs...\n---\n")
526
 
527
  final_status, all_logs = await self._wait_for_job_completion(
 
66
 
67
  def _substitute_hf_token(params: Dict[str, Any] | None) -> Dict[str, Any] | None:
68
  """
69
+ Substitute HF_TOKEN key with actual token value from environment.
70
 
71
  Args:
72
+ params: Dictionary that may contain "HF_TOKEN" as a key
73
 
74
  Returns:
75
+ Dictionary with HF_TOKEN value substituted from environment
76
  """
77
+ print("DEBUG !! : ", params)
78
  if params is None:
79
  return None
80
 
81
  result = {}
82
  for key, value in params.items():
83
+ if key == "HF_TOKEN":
84
  result[key] = os.environ.get("HF_TOKEN", "")
85
  else:
86
  result[key] = value
 
355
  "operation": "uv",
356
  "args": {{
357
  "script": "import random\\nprint(42 + random.randint(1, 5))",
358
+ "dependencies": ["torch", "huggingface_hub"],
359
+ "secrets": {{"HF_TOKEN": "$HF_TOKEN"}}
360
  }}
361
  }}
362
  ```
 
386
 
387
  - Jobs default to non-detached mode (stream logs until completion). Set `detach: true` to return immediately.
388
  - Prefer array commands to avoid shell parsing surprises
389
+ - To access, create, or modify private Hub assets (spaces, private models, datasets, collections), pass `secrets: {{ "HF_TOKEN": "$HF_TOKEN" }}`. This is important. Without it, you will encounter authentification issues. Do not assume the user is connected on the jobs' server.
390
  - Before calling a job, think about dependencies (they must be specified), which hardware flavor to run on (choose simplest for task), and whether to include secrets.
391
  """
392
  return {"formatted": usage_text, "totalResults": 1, "resultsShared": 1}
 
450
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
451
 
452
  # Not detached - wait for completion and stream logs
453
+ print(f"Job started: {job.url}")
454
  print("Streaming logs...\n---\n")
455
 
456
  final_status, all_logs = await self._wait_for_job_completion(
 
523
  return {"formatted": response, "totalResults": 1, "resultsShared": 1}
524
 
525
  # Not detached - wait for completion and stream logs
526
+ print(f"UV Job started: {job.url}")
527
  print("Streaming logs...\n---\n")
528
 
529
  final_status, all_logs = await self._wait_for_job_completion(
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. Re call the tool with correct format (mandatory).",
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}'. Re call the tool with correct format (mandatory).",
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)}. Re call the tool with correct format (mandatory).",
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)