minion-code 0.1.0__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.
- examples/advance_tui.py +508 -0
- examples/agent_with_todos.py +165 -0
- examples/file_freshness_example.py +97 -0
- examples/file_watching_example.py +110 -0
- examples/interruptible_tui.py +5 -0
- examples/message_response_children_demo.py +226 -0
- examples/rich_example.py +4 -0
- examples/simple_file_watching.py +57 -0
- examples/simple_tui.py +267 -0
- examples/simple_usage.py +69 -0
- minion_code/__init__.py +16 -0
- minion_code/agents/__init__.py +11 -0
- minion_code/agents/code_agent.py +320 -0
- minion_code/cli.py +502 -0
- minion_code/commands/__init__.py +90 -0
- minion_code/commands/clear_command.py +70 -0
- minion_code/commands/help_command.py +90 -0
- minion_code/commands/history_command.py +104 -0
- minion_code/commands/quit_command.py +32 -0
- minion_code/commands/status_command.py +115 -0
- minion_code/commands/tools_command.py +86 -0
- minion_code/commands/version_command.py +104 -0
- minion_code/components/Message.py +304 -0
- minion_code/components/MessageResponse.py +188 -0
- minion_code/components/PromptInput.py +534 -0
- minion_code/components/__init__.py +29 -0
- minion_code/screens/REPL.py +925 -0
- minion_code/screens/__init__.py +4 -0
- minion_code/services/__init__.py +50 -0
- minion_code/services/event_system.py +108 -0
- minion_code/services/file_freshness_service.py +582 -0
- minion_code/tools/__init__.py +69 -0
- minion_code/tools/bash_tool.py +58 -0
- minion_code/tools/file_edit_tool.py +238 -0
- minion_code/tools/file_read_tool.py +73 -0
- minion_code/tools/file_write_tool.py +36 -0
- minion_code/tools/glob_tool.py +58 -0
- minion_code/tools/grep_tool.py +105 -0
- minion_code/tools/ls_tool.py +65 -0
- minion_code/tools/multi_edit_tool.py +271 -0
- minion_code/tools/python_interpreter_tool.py +105 -0
- minion_code/tools/todo_read_tool.py +100 -0
- minion_code/tools/todo_write_tool.py +234 -0
- minion_code/tools/user_input_tool.py +53 -0
- minion_code/types.py +88 -0
- minion_code/utils/__init__.py +44 -0
- minion_code/utils/mcp_loader.py +211 -0
- minion_code/utils/todo_file_utils.py +110 -0
- minion_code/utils/todo_storage.py +149 -0
- minion_code-0.1.0.dist-info/METADATA +350 -0
- minion_code-0.1.0.dist-info/RECORD +59 -0
- minion_code-0.1.0.dist-info/WHEEL +5 -0
- minion_code-0.1.0.dist-info/entry_points.txt +4 -0
- minion_code-0.1.0.dist-info/licenses/LICENSE +661 -0
- minion_code-0.1.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +1 -0
- tests/test_basic.py +20 -0
- tests/test_readonly_tools.py +102 -0
- tests/test_tools.py +83 -0
examples/simple_tui.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Simple TUI using MinionCodeAgent
|
|
5
|
+
|
|
6
|
+
This example shows how the new MinionCodeAgent class simplifies
|
|
7
|
+
the TUI implementation by handling all the tool setup internally.
|
|
8
|
+
|
|
9
|
+
Compare this with minion_agent_tui.py to see the reduction in boilerplate code.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# Add project root to path
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
18
|
+
|
|
19
|
+
from minion_code import MinionCodeAgent
|
|
20
|
+
from minion_code.commands import command_registry
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
from rich.markdown import Markdown
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
27
|
+
from rich.prompt import Prompt
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SimpleTUI:
|
|
31
|
+
"""Simplified TUI using MinionCodeAgent."""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.agent = None
|
|
35
|
+
self.running = True
|
|
36
|
+
self.console = Console()
|
|
37
|
+
|
|
38
|
+
async def setup(self):
|
|
39
|
+
"""Setup the agent."""
|
|
40
|
+
with Progress(
|
|
41
|
+
SpinnerColumn(),
|
|
42
|
+
TextColumn("[progress.description]{task.description}"),
|
|
43
|
+
console=self.console,
|
|
44
|
+
) as progress:
|
|
45
|
+
task = progress.add_task("š§ Setting up MinionCodeAgent...", total=None)
|
|
46
|
+
|
|
47
|
+
# Much simpler setup - no manual tool configuration needed
|
|
48
|
+
self.agent = await MinionCodeAgent.create(
|
|
49
|
+
name="Simple TUI Assistant",
|
|
50
|
+
llm="gpt-4o-mini",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
progress.update(task, completed=True)
|
|
54
|
+
|
|
55
|
+
success_panel = Panel(
|
|
56
|
+
f"ā
Agent ready with [bold green]{len(self.agent.tools)}[/bold green] tools!",
|
|
57
|
+
title="[bold green]Setup Complete[/bold green]",
|
|
58
|
+
border_style="green"
|
|
59
|
+
)
|
|
60
|
+
self.console.print(success_panel)
|
|
61
|
+
|
|
62
|
+
def show_help(self):
|
|
63
|
+
"""Show help information."""
|
|
64
|
+
help_table = Table(title="š Simple TUI Help", show_header=True, header_style="bold blue")
|
|
65
|
+
help_table.add_column("Command", style="cyan", no_wrap=True)
|
|
66
|
+
help_table.add_column("Description", style="white")
|
|
67
|
+
|
|
68
|
+
help_table.add_row("help", "Show this help")
|
|
69
|
+
help_table.add_row("tools", "List available tools")
|
|
70
|
+
help_table.add_row("history", "Show conversation history")
|
|
71
|
+
help_table.add_row("clear", "Clear history")
|
|
72
|
+
help_table.add_row("quit", "Exit")
|
|
73
|
+
|
|
74
|
+
self.console.print(help_table)
|
|
75
|
+
self.console.print("\nš” [italic]Just type your message to chat with the AI agent![/italic]")
|
|
76
|
+
|
|
77
|
+
async def process_input(self, user_input: str):
|
|
78
|
+
"""Process user input."""
|
|
79
|
+
user_input = user_input.strip()
|
|
80
|
+
|
|
81
|
+
# Check if it's a command (starts with /)
|
|
82
|
+
if user_input.startswith('/'):
|
|
83
|
+
await self.process_command(user_input)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Process with agent
|
|
87
|
+
try:
|
|
88
|
+
with Progress(
|
|
89
|
+
SpinnerColumn(),
|
|
90
|
+
TextColumn("[progress.description]{task.description}"),
|
|
91
|
+
console=self.console,
|
|
92
|
+
) as progress:
|
|
93
|
+
task = progress.add_task("š¤ Processing...", total=None)
|
|
94
|
+
response = await self.agent.run_async(user_input)
|
|
95
|
+
progress.update(task, completed=True)
|
|
96
|
+
|
|
97
|
+
# Display agent response with rich formatting
|
|
98
|
+
if "```" in response.answer:
|
|
99
|
+
# If response contains code blocks, render as markdown
|
|
100
|
+
agent_content = Markdown(response.answer)
|
|
101
|
+
else:
|
|
102
|
+
agent_content = response.answer
|
|
103
|
+
|
|
104
|
+
response_panel = Panel(
|
|
105
|
+
agent_content,
|
|
106
|
+
title="š¤ [bold green]Agent Response[/bold green]",
|
|
107
|
+
border_style="green"
|
|
108
|
+
)
|
|
109
|
+
self.console.print(response_panel)
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
error_panel = Panel(
|
|
113
|
+
f"ā [bold red]Error: {e}[/bold red]",
|
|
114
|
+
title="[bold red]Error[/bold red]",
|
|
115
|
+
border_style="red"
|
|
116
|
+
)
|
|
117
|
+
self.console.print(error_panel)
|
|
118
|
+
|
|
119
|
+
async def process_command(self, command_input: str):
|
|
120
|
+
"""Process a command input."""
|
|
121
|
+
# Remove the leading /
|
|
122
|
+
command_input = command_input[1:] if command_input.startswith('/') else command_input
|
|
123
|
+
|
|
124
|
+
# Split command and arguments
|
|
125
|
+
parts = command_input.split(' ', 1)
|
|
126
|
+
command_name = parts[0].lower()
|
|
127
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
128
|
+
|
|
129
|
+
# Get command class
|
|
130
|
+
command_class = command_registry.get_command(command_name)
|
|
131
|
+
if not command_class:
|
|
132
|
+
error_panel = Panel(
|
|
133
|
+
f"ā [bold red]Unknown command: /{command_name}[/bold red]\n"
|
|
134
|
+
f"š” [italic]Use '/help' to see available commands[/italic]",
|
|
135
|
+
title="[bold red]Error[/bold red]",
|
|
136
|
+
border_style="red"
|
|
137
|
+
)
|
|
138
|
+
self.console.print(error_panel)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# Create and execute command
|
|
142
|
+
try:
|
|
143
|
+
command_instance = command_class(self.console, self.agent)
|
|
144
|
+
|
|
145
|
+
# Special handling for quit command
|
|
146
|
+
if command_name in ["quit", "exit", "q", "bye"]:
|
|
147
|
+
command_instance._tui_instance = self
|
|
148
|
+
|
|
149
|
+
await command_instance.execute(args)
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
error_panel = Panel(
|
|
153
|
+
f"ā [bold red]Error executing command /{command_name}: {e}[/bold red]",
|
|
154
|
+
title="[bold red]Command Error[/bold red]",
|
|
155
|
+
border_style="red"
|
|
156
|
+
)
|
|
157
|
+
self.console.print(error_panel)
|
|
158
|
+
|
|
159
|
+
def show_tools(self):
|
|
160
|
+
"""Show available tools in a beautiful table."""
|
|
161
|
+
if not self.agent or not self.agent.tools:
|
|
162
|
+
self.console.print("ā No tools available")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
tools_table = Table(title="š ļø Available Tools", show_header=True, header_style="bold magenta")
|
|
166
|
+
tools_table.add_column("Tool Name", style="cyan", no_wrap=True)
|
|
167
|
+
tools_table.add_column("Description", style="white")
|
|
168
|
+
tools_table.add_column("Type", style="yellow")
|
|
169
|
+
|
|
170
|
+
for tool in self.agent.tools:
|
|
171
|
+
tool_type = "Read-only" if getattr(tool, 'readonly', False) else "Read-write"
|
|
172
|
+
tools_table.add_row(
|
|
173
|
+
tool.name,
|
|
174
|
+
tool.description[:60] + "..." if len(tool.description) > 60 else tool.description,
|
|
175
|
+
tool_type
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
self.console.print(tools_table)
|
|
179
|
+
|
|
180
|
+
def show_history(self):
|
|
181
|
+
"""Show conversation history in a beautiful format."""
|
|
182
|
+
if not self.agent:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
history = self.agent.get_conversation_history()
|
|
186
|
+
if not history:
|
|
187
|
+
no_history_panel = Panel(
|
|
188
|
+
"š [italic]No conversation history yet.[/italic]",
|
|
189
|
+
title="[bold blue]History[/bold blue]",
|
|
190
|
+
border_style="blue"
|
|
191
|
+
)
|
|
192
|
+
self.console.print(no_history_panel)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
history_panel = Panel(
|
|
196
|
+
f"š [bold blue]Conversation History ({len(history)} messages)[/bold blue]",
|
|
197
|
+
border_style="blue"
|
|
198
|
+
)
|
|
199
|
+
self.console.print(history_panel)
|
|
200
|
+
|
|
201
|
+
for i, entry in enumerate(history[-5:], 1): # Show last 5 messages
|
|
202
|
+
# User message
|
|
203
|
+
user_panel = Panel(
|
|
204
|
+
entry['user_message'][:200] + "..." if len(entry['user_message']) > 200 else entry['user_message'],
|
|
205
|
+
title=f"š¤ [bold cyan]You (Message {len(history)-5+i})[/bold cyan]",
|
|
206
|
+
border_style="cyan"
|
|
207
|
+
)
|
|
208
|
+
self.console.print(user_panel)
|
|
209
|
+
|
|
210
|
+
# Agent response
|
|
211
|
+
agent_response = entry['agent_response'][:200] + "..." if len(entry['agent_response']) > 200 else entry['agent_response']
|
|
212
|
+
agent_panel = Panel(
|
|
213
|
+
agent_response,
|
|
214
|
+
title="š¤ [bold green]Agent[/bold green]",
|
|
215
|
+
border_style="green"
|
|
216
|
+
)
|
|
217
|
+
self.console.print(agent_panel)
|
|
218
|
+
self.console.print() # Add spacing
|
|
219
|
+
|
|
220
|
+
async def run(self):
|
|
221
|
+
"""Run the TUI."""
|
|
222
|
+
# Welcome banner
|
|
223
|
+
welcome_panel = Panel(
|
|
224
|
+
"š [bold blue]Simple MinionCodeAgent TUI[/bold blue]\n"
|
|
225
|
+
"š” [italic]Use '/help' for commands or just chat with the agent![/italic]\n"
|
|
226
|
+
"š [italic]Type '/quit' to exit[/italic]",
|
|
227
|
+
title="[bold magenta]Welcome[/bold magenta]",
|
|
228
|
+
border_style="magenta"
|
|
229
|
+
)
|
|
230
|
+
self.console.print(welcome_panel)
|
|
231
|
+
|
|
232
|
+
await self.setup()
|
|
233
|
+
|
|
234
|
+
while self.running:
|
|
235
|
+
try:
|
|
236
|
+
# Use rich prompt for better input experience
|
|
237
|
+
user_input = Prompt.ask(
|
|
238
|
+
"\n[bold cyan]š¤ You[/bold cyan]",
|
|
239
|
+
console=self.console
|
|
240
|
+
).strip()
|
|
241
|
+
|
|
242
|
+
if user_input:
|
|
243
|
+
await self.process_input(user_input)
|
|
244
|
+
|
|
245
|
+
except (EOFError, KeyboardInterrupt):
|
|
246
|
+
goodbye_panel = Panel(
|
|
247
|
+
"\nš [bold yellow]Goodbye![/bold yellow]",
|
|
248
|
+
title="[bold red]Exit[/bold red]",
|
|
249
|
+
border_style="red"
|
|
250
|
+
)
|
|
251
|
+
self.console.print(goodbye_panel)
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def main():
|
|
256
|
+
"""Main function."""
|
|
257
|
+
tui = SimpleTUI()
|
|
258
|
+
await tui.run()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def run():
|
|
262
|
+
"""Synchronous entry point."""
|
|
263
|
+
asyncio.run(main())
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
run()
|
examples/simple_usage.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Simple usage example of FileFreshnessService."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Add parent directory to path for imports
|
|
8
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
9
|
+
|
|
10
|
+
from minion_code.services import (
|
|
11
|
+
record_file_read,
|
|
12
|
+
record_file_edit,
|
|
13
|
+
check_file_freshness,
|
|
14
|
+
generate_file_modification_reminder,
|
|
15
|
+
add_event_listener,
|
|
16
|
+
emit_event,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
"""Simple usage demonstration."""
|
|
22
|
+
|
|
23
|
+
# Set up a simple event listener
|
|
24
|
+
def on_conflict(context):
|
|
25
|
+
print(f"ā ļø Conflict: {context.data['file_path']}")
|
|
26
|
+
|
|
27
|
+
add_event_listener('file:conflict', on_conflict)
|
|
28
|
+
|
|
29
|
+
# Create test file
|
|
30
|
+
test_file = "example.py"
|
|
31
|
+
with open(test_file, "w") as f:
|
|
32
|
+
f.write("print('Hello, World!')")
|
|
33
|
+
|
|
34
|
+
# Record reading the file
|
|
35
|
+
record_file_read(test_file)
|
|
36
|
+
print(f"ā
Recorded reading {test_file}")
|
|
37
|
+
|
|
38
|
+
# Check freshness (should be fresh)
|
|
39
|
+
result = check_file_freshness(test_file)
|
|
40
|
+
print(f"š File is fresh: {result.is_fresh}")
|
|
41
|
+
|
|
42
|
+
# Simulate external modification
|
|
43
|
+
with open(test_file, "w") as f:
|
|
44
|
+
f.write("print('Modified externally!')")
|
|
45
|
+
|
|
46
|
+
# Check freshness again (should detect conflict)
|
|
47
|
+
result = check_file_freshness(test_file)
|
|
48
|
+
print(f"š After external change - Fresh: {result.is_fresh}, Conflict: {result.conflict}")
|
|
49
|
+
|
|
50
|
+
# Generate reminder for external modification
|
|
51
|
+
reminder = generate_file_modification_reminder(test_file)
|
|
52
|
+
if reminder:
|
|
53
|
+
print(f"š” Reminder: {reminder}")
|
|
54
|
+
|
|
55
|
+
# Record agent edit (resolves conflict)
|
|
56
|
+
record_file_edit(test_file, "print('Agent fixed this!')")
|
|
57
|
+
print(f"ā
Agent edited {test_file}")
|
|
58
|
+
|
|
59
|
+
# Check freshness after agent edit
|
|
60
|
+
result = check_file_freshness(test_file)
|
|
61
|
+
print(f"š After agent edit - Fresh: {result.is_fresh}, Conflict: {result.conflict}")
|
|
62
|
+
|
|
63
|
+
# Cleanup
|
|
64
|
+
os.remove(test_file)
|
|
65
|
+
print("š§¹ Cleaned up test file")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
minion_code/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Minion Code Tools Package
|
|
5
|
+
|
|
6
|
+
A collection of tools and enhanced agents for the Minion framework including
|
|
7
|
+
file operations, system commands, web interactions, and specialized agents
|
|
8
|
+
with dynamic system prompts and state management.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from . import tools
|
|
12
|
+
from . import agents
|
|
13
|
+
from .agents import MinionCodeAgent, create_minion_code_agent
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
__all__ = ["tools", "agents", "MinionCodeAgent", "create_minion_code_agent"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Minion Code Agents Package
|
|
5
|
+
|
|
6
|
+
Enhanced agents with specialized capabilities for code development tasks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .code_agent import MinionCodeAgent,create_minion_code_agent
|
|
10
|
+
|
|
11
|
+
__all__ = ["MinionCodeAgent","create_minion_code_agent"]
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
MinionCodeAgent - Enhanced CodeAgent with minion_code tools
|
|
5
|
+
|
|
6
|
+
This module provides a specialized CodeAgent subclass that comes pre-configured
|
|
7
|
+
with all minion_code tools and optimized system prompts for code development tasks.
|
|
8
|
+
|
|
9
|
+
Key features:
|
|
10
|
+
- Pre-configured with all minion_code tools
|
|
11
|
+
- Optimized system prompt for coding tasks
|
|
12
|
+
- Async/sync support
|
|
13
|
+
- Easy setup and initialization
|
|
14
|
+
- Conversation history management
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import logging
|
|
19
|
+
import traceback
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import List, Optional, Union, Any
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
from minion.agents import CodeAgent
|
|
26
|
+
|
|
27
|
+
# Import all minion_code tools
|
|
28
|
+
from ..tools import (
|
|
29
|
+
FileReadTool,
|
|
30
|
+
FileWriteTool,
|
|
31
|
+
FileEditTool,
|
|
32
|
+
MultiEditTool,
|
|
33
|
+
BashTool,
|
|
34
|
+
GrepTool,
|
|
35
|
+
GlobTool,
|
|
36
|
+
LsTool,
|
|
37
|
+
PythonInterpreterTool,
|
|
38
|
+
UserInputTool,
|
|
39
|
+
|
|
40
|
+
TodoWriteTool,
|
|
41
|
+
TodoReadTool,
|
|
42
|
+
TOOL_MAPPING,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
# Todo reminder constants
|
|
48
|
+
INITIAL_REMINDER = (
|
|
49
|
+
'<reminder source="system" topic="todos">'
|
|
50
|
+
"System message: complex work should be tracked with the Todo tool. "
|
|
51
|
+
"Do not respond to this reminder and do not mention it to the user."
|
|
52
|
+
'</reminder>'
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
NAG_REMINDER = (
|
|
56
|
+
'<reminder source="system" topic="todos">'
|
|
57
|
+
"System notice: more than ten rounds passed without Todo usage. "
|
|
58
|
+
"Update the Todo board if the task still requires multiple steps. "
|
|
59
|
+
"Do not reply to or mention this reminder to the user."
|
|
60
|
+
'</reminder>'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class MinionCodeAgent(CodeAgent):
|
|
65
|
+
"""
|
|
66
|
+
Enhanced CodeAgent with pre-configured minion_code tools.
|
|
67
|
+
|
|
68
|
+
This class wraps the Minion CodeAgent with all minion_code tools
|
|
69
|
+
and provides optimized system prompts for coding tasks.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
DEFAULT_SYSTEM_PROMPT = (
|
|
73
|
+
"You are a coding agent operating INSIDE the user's repository at {workdir}.\n"
|
|
74
|
+
"Follow this loop strictly: plan briefly ā use TOOLS to act directly on files/shell ā report concise results.\n"
|
|
75
|
+
"Rules:\n"
|
|
76
|
+
"- Prefer taking actions with tools (read/write/edit/bash) over long prose.\n"
|
|
77
|
+
"- Keep outputs terse. Use bullet lists / checklists when summarizing.\n"
|
|
78
|
+
"- Never invent file paths. Ask via reads or list directories first if unsure.\n"
|
|
79
|
+
"- For edits, choose the right tool: string_edit for single string replacements, multi_edit for multiple changes to same file, file_edit for advanced operations.\n"
|
|
80
|
+
"- Always read files before editing to establish freshness tracking.\n"
|
|
81
|
+
"- For bash, avoid destructive or privileged commands; stay inside the workspace.\n"
|
|
82
|
+
"- Use the Todo tool to maintain multi-step plans when needed.\n"
|
|
83
|
+
"- After finishing, summarize what changed and how to run or test."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def __post_init__(self):
|
|
87
|
+
"""Initialize the CodeAgent with thinking capabilities and optional state tracking."""
|
|
88
|
+
super().__post_init__()
|
|
89
|
+
self.conversation_history = []
|
|
90
|
+
|
|
91
|
+
async def pre_step(self, input_data, kwargs):
|
|
92
|
+
"""Override pre_step to track iterations without todo usage."""
|
|
93
|
+
# Call parent pre_step first
|
|
94
|
+
result = await super().pre_step(input_data, kwargs)
|
|
95
|
+
|
|
96
|
+
# Initialize metadata if not exists
|
|
97
|
+
if not hasattr(self.state, 'metadata'):
|
|
98
|
+
self.state.metadata = {}
|
|
99
|
+
if "iteration_without_todos" not in self.state.metadata:
|
|
100
|
+
self.state.metadata["iteration_without_todos"] = 0
|
|
101
|
+
|
|
102
|
+
# Increment iteration counter
|
|
103
|
+
self.state.metadata["iteration_without_todos"] += 1
|
|
104
|
+
|
|
105
|
+
# Add nag reminder if more than 10 iterations without todo usage
|
|
106
|
+
if self.state.metadata["iteration_without_todos"] > 10:
|
|
107
|
+
self.state.history.append({
|
|
108
|
+
'role': 'user',
|
|
109
|
+
'content': NAG_REMINDER
|
|
110
|
+
})
|
|
111
|
+
# Reset counter to avoid spamming reminders
|
|
112
|
+
self.state.metadata["iteration_without_todos"] = 0
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
async def create(
|
|
120
|
+
cls,
|
|
121
|
+
name: str = "Minion Code Assistant",
|
|
122
|
+
llm: str = "sonnet",
|
|
123
|
+
system_prompt: Optional[str] = None,
|
|
124
|
+
workdir: Optional[Union[str, Path]] = None,
|
|
125
|
+
additional_tools: Optional[List[Any]] = None,
|
|
126
|
+
**kwargs
|
|
127
|
+
) -> "MinionCodeAgent":
|
|
128
|
+
"""
|
|
129
|
+
Create a new MinionCodeAgent with all minion_code tools.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
name: Agent name
|
|
133
|
+
llm: LLM model to use
|
|
134
|
+
system_prompt: Custom system prompt (uses default if None)
|
|
135
|
+
workdir: Working directory (uses current if None)
|
|
136
|
+
additional_tools: Extra tools to add beyond minion_code tools
|
|
137
|
+
**kwargs: Additional arguments passed to CodeAgent.create()
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Configured MinionCodeAgent instance
|
|
141
|
+
"""
|
|
142
|
+
if workdir is None:
|
|
143
|
+
workdir = Path.cwd()
|
|
144
|
+
else:
|
|
145
|
+
workdir = Path(workdir)
|
|
146
|
+
|
|
147
|
+
# Use default system prompt if none provided
|
|
148
|
+
if system_prompt is None:
|
|
149
|
+
system_prompt = cls.DEFAULT_SYSTEM_PROMPT.format(workdir=workdir)
|
|
150
|
+
|
|
151
|
+
# Get all minion_code tools
|
|
152
|
+
minion_tools = [
|
|
153
|
+
FileReadTool(),
|
|
154
|
+
FileWriteTool(),
|
|
155
|
+
FileEditTool(),
|
|
156
|
+
MultiEditTool(),
|
|
157
|
+
BashTool(),
|
|
158
|
+
GrepTool(),
|
|
159
|
+
GlobTool(),
|
|
160
|
+
LsTool(),
|
|
161
|
+
PythonInterpreterTool(),
|
|
162
|
+
UserInputTool(),
|
|
163
|
+
TodoWriteTool(),
|
|
164
|
+
TodoReadTool(),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
# Add any additional tools
|
|
168
|
+
all_tools = minion_tools[:]
|
|
169
|
+
if additional_tools:
|
|
170
|
+
all_tools.extend(additional_tools)
|
|
171
|
+
|
|
172
|
+
logger.info(f"Creating MinionCodeAgent with {len(all_tools)} tools")
|
|
173
|
+
|
|
174
|
+
# Create the underlying CodeAgent
|
|
175
|
+
agent = await super().create(
|
|
176
|
+
name=name,
|
|
177
|
+
llm=llm,
|
|
178
|
+
system_prompt=system_prompt,
|
|
179
|
+
tools=all_tools,
|
|
180
|
+
**kwargs
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Initialize todo tracking metadata
|
|
184
|
+
if not hasattr(agent.state, 'metadata'):
|
|
185
|
+
agent.state.metadata = {}
|
|
186
|
+
agent.state.metadata["iteration_without_todos"] = 0
|
|
187
|
+
|
|
188
|
+
# Add initial todo reminder to history
|
|
189
|
+
agent.state.history.append({
|
|
190
|
+
'role': 'user',
|
|
191
|
+
'content': INITIAL_REMINDER
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return agent
|
|
195
|
+
|
|
196
|
+
async def run_async(self, message: str, **kwargs) -> Any:
|
|
197
|
+
"""
|
|
198
|
+
Run agent asynchronously and track conversation history.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
message: User message
|
|
202
|
+
**kwargs: Additional arguments passed to agent.run_async()
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Agent response
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
response = await super().run_async(message, **kwargs)
|
|
209
|
+
|
|
210
|
+
# Track conversation history
|
|
211
|
+
self.conversation_history.append({
|
|
212
|
+
'user_message': message,
|
|
213
|
+
'agent_response': response.answer if hasattr(response, 'answer') else str(response),
|
|
214
|
+
'timestamp': asyncio.get_event_loop().time()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
return response
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
logger.error(f"Error in run_async: {e}")
|
|
221
|
+
traceback.print_exc()
|
|
222
|
+
raise
|
|
223
|
+
|
|
224
|
+
def run(self, message: str, **kwargs) -> Any:
|
|
225
|
+
"""
|
|
226
|
+
Run agent synchronously.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
message: User message
|
|
230
|
+
**kwargs: Additional arguments
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Agent response
|
|
234
|
+
"""
|
|
235
|
+
return asyncio.run(self.run_async(message, **kwargs))
|
|
236
|
+
|
|
237
|
+
def get_conversation_history(self) -> List[dict]:
|
|
238
|
+
"""Get conversation history."""
|
|
239
|
+
return self.conversation_history.copy()
|
|
240
|
+
|
|
241
|
+
def clear_conversation_history(self):
|
|
242
|
+
"""Clear conversation history."""
|
|
243
|
+
self.conversation_history.clear()
|
|
244
|
+
|
|
245
|
+
def get_tools_info(self) -> List[dict]:
|
|
246
|
+
"""
|
|
247
|
+
Get information about available tools.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
List of tool information dictionaries
|
|
251
|
+
"""
|
|
252
|
+
tools_info = []
|
|
253
|
+
for tool in self.tools:
|
|
254
|
+
readonly_status = getattr(tool, "readonly", None)
|
|
255
|
+
tools_info.append({
|
|
256
|
+
'name': tool.name,
|
|
257
|
+
'description': tool.description,
|
|
258
|
+
'readonly': readonly_status,
|
|
259
|
+
'type': type(tool).__name__
|
|
260
|
+
})
|
|
261
|
+
return tools_info
|
|
262
|
+
|
|
263
|
+
def print_tools_summary(self):
|
|
264
|
+
"""Print a summary of available tools."""
|
|
265
|
+
tools_info = self.get_tools_info()
|
|
266
|
+
|
|
267
|
+
print(f"\nš ļø Available Tools ({len(tools_info)} total):")
|
|
268
|
+
|
|
269
|
+
# Group tools by category
|
|
270
|
+
categories = {
|
|
271
|
+
'File & Directory': ['file', 'read', 'write', 'grep', 'glob', 'ls'],
|
|
272
|
+
'System & Execution': ['bash', 'python', 'calc', 'system'],
|
|
273
|
+
'Web & Search': ['web', 'search', 'wikipedia', 'visit'],
|
|
274
|
+
'Other': []
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
categorized_tools = {cat: [] for cat in categories}
|
|
278
|
+
|
|
279
|
+
for tool in tools_info:
|
|
280
|
+
categorized = False
|
|
281
|
+
for category, keywords in categories.items():
|
|
282
|
+
if category == 'Other':
|
|
283
|
+
continue
|
|
284
|
+
if any(keyword in tool['name'].lower() for keyword in keywords):
|
|
285
|
+
categorized_tools[category].append(tool)
|
|
286
|
+
categorized = True
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
if not categorized:
|
|
290
|
+
categorized_tools['Other'].append(tool)
|
|
291
|
+
|
|
292
|
+
# Print categorized tools
|
|
293
|
+
for category, tools in categorized_tools.items():
|
|
294
|
+
if tools:
|
|
295
|
+
print(f"\nš {category} Tools:")
|
|
296
|
+
for tool in tools:
|
|
297
|
+
readonly_icon = "š" if tool['readonly'] else "āļø"
|
|
298
|
+
print(f" {readonly_icon} {tool['name']}: {tool['description']}")
|
|
299
|
+
|
|
300
|
+
print(f"\nš = readonly tool, āļø = read/write tool")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# Convenience function for quick setup
|
|
304
|
+
async def create_minion_code_agent(
|
|
305
|
+
name: str = "Minion Code Assistant",
|
|
306
|
+
llm: str = "gpt-4o-mini",
|
|
307
|
+
**kwargs
|
|
308
|
+
) -> MinionCodeAgent:
|
|
309
|
+
"""
|
|
310
|
+
Convenience function to create a MinionCodeAgent.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
name: Agent name
|
|
314
|
+
llm: LLM model to use
|
|
315
|
+
**kwargs: Additional arguments passed to MinionCodeAgent.create()
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Configured MinionCodeAgent instance
|
|
319
|
+
"""
|
|
320
|
+
return await MinionCodeAgent.create(name=name, llm=llm, **kwargs)
|