rmk 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.
- rmk-0.1.0.dist-info/METADATA +40 -0
- rmk-0.1.0.dist-info/RECORD +33 -0
- rmk-0.1.0.dist-info/WHEEL +4 -0
- rmk-0.1.0.dist-info/entry_points.txt +2 -0
- rmonkey/__init__.py +17 -0
- rmonkey/action/__init__.py +3 -0
- rmonkey/action/tool_action.py +23 -0
- rmonkey/agents/__init__.py +11 -0
- rmonkey/agents/_base.py +138 -0
- rmonkey/agents/ask_agent.py +7 -0
- rmonkey/agents/root_monkey.py +22 -0
- rmonkey/agents/swe_agent.py +19 -0
- rmonkey/entrypoints/cli/main.py +134 -0
- rmonkey/llm/__init__.py +7 -0
- rmonkey/llm/providers/openai.py +160 -0
- rmonkey/memory/__init__.py +3 -0
- rmonkey/memory/memory.py +51 -0
- rmonkey/plan/__init__.py +5 -0
- rmonkey/plan/planning.py +108 -0
- rmonkey/tools/__init__.py +17 -0
- rmonkey/tools/_base.py +30 -0
- rmonkey/tools/code_interpreter.py +25 -0
- rmonkey/tools/human.py +23 -0
- rmonkey/tools/terminal_bash.py +31 -0
- rmonkey/tools/text_file_editor.py +130 -0
- rmonkey/tools/think.py +19 -0
- rmonkey/tools/tool_set.py +24 -0
- rmonkey/utils/__init__.py +0 -0
- rmonkey/utils/os_info.py +10 -0
- rmonkey/utils/pretty_console.py +19 -0
- rmonkey/utils/schema.py +28 -0
- rmonkey/utils/user_rules.py +14 -0
- rmonkey/utils/util.py +11 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: rmk
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Root Monkey (rmk) is an autonomous AI Agent system.
|
5
|
+
Author-email: rootmq <mqvvang@gmail.com>
|
6
|
+
Requires-Python: >=3.12
|
7
|
+
Requires-Dist: anthropic>=0.52.0
|
8
|
+
Requires-Dist: dotenv>=0.9.9
|
9
|
+
Requires-Dist: litellm>=1.72.0
|
10
|
+
Requires-Dist: openai>=1.82.0
|
11
|
+
Requires-Dist: pydantic>=2.11.5
|
12
|
+
Requires-Dist: rich>=14.0.0
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
|
15
|
+
# Root Monkey (🛠️🐒⚙️)
|
16
|
+
|
17
|
+
Root Monkey (rmk) is an autonomous AI Agent system.
|
18
|
+
|
19
|
+
Key Features:
|
20
|
+
- High Agent Autonomy: *Less human intervention, more agent autonomy.*
|
21
|
+
- Self-Developing: *The system can develop new capabilities on its own.*
|
22
|
+
- Simple Design: *Easy to understand and implement.*
|
23
|
+
|
24
|
+
|
25
|
+
## Design Philosophy
|
26
|
+
|
27
|
+
- "Simplicity is the ultimate sophistication." - Leonardo da Vinci
|
28
|
+
|
29
|
+
|
30
|
+
- "The evolution of the model will gradually reduce the complexity of the design." - Root Monkey
|
31
|
+
|
32
|
+
|
33
|
+
## Demo - dev setup
|
34
|
+
```bash
|
35
|
+
pip install rmk
|
36
|
+
export OPENAI_API_KEY="your-api-key"
|
37
|
+
export MODEL="gpt-4.1"
|
38
|
+
|
39
|
+
rmk -t "Please clone the rmonkey project from https://github.com/rootagent/rmk and set up the development environment as described in the contribution file." --verbos
|
40
|
+
```
|
@@ -0,0 +1,33 @@
|
|
1
|
+
rmonkey/__init__.py,sha256=8u5leZHRym4_CMor70g48x32wcpY6YT-TBBqRYFNLGk,359
|
2
|
+
rmonkey/action/__init__.py,sha256=flNLS2Gt9BzrK6TeAv_-z-bXdc3Cvx9N6IJeGc3yKjQ,62
|
3
|
+
rmonkey/action/tool_action.py,sha256=svZNxwgmIUst6JBiKCdEGw_h8Ntm_by9LfgSPpXY52U,655
|
4
|
+
rmonkey/agents/__init__.py,sha256=C5lhR67pMPt2Qm4iorHf4r1fMVKBA6tNGzVWrYQORns,203
|
5
|
+
rmonkey/agents/_base.py,sha256=0e_Z4uLAeNIZsXTfuRjLQjF1p52kzPlR4oe6dHHy528,4910
|
6
|
+
rmonkey/agents/ask_agent.py,sha256=pIjcO29vxDOOnzk-pbQzv6joOiHrLyXlhewd9d4w2dA,259
|
7
|
+
rmonkey/agents/root_monkey.py,sha256=D6pEY_RAoB3iUBg8oQ8KQtjugGsA25K2Yhp_hAEMAG8,778
|
8
|
+
rmonkey/agents/swe_agent.py,sha256=pDD_QzfhO-haiWRYMdmPkBa8RJBlOwJQ46cYGc0q9PM,603
|
9
|
+
rmonkey/entrypoints/cli/main.py,sha256=kGd9z7Dr8auCB6rhawdoweEWQRsSxootxgv4_GIh8C4,4562
|
10
|
+
rmonkey/llm/__init__.py,sha256=XvtkQCwiOr20gOasAWYeHQ2Y9dHEN1wpzd_xEselYb0,141
|
11
|
+
rmonkey/llm/providers/openai.py,sha256=yeBHpCxS62VtuPZZpLd8mdj_9vUnGzIRyxqL1Ji54dE,5736
|
12
|
+
rmonkey/memory/__init__.py,sha256=dYWEw_ZP6ZRlLAzdt8Ikc1xtktJmS3GLK-cB364-c-I,63
|
13
|
+
rmonkey/memory/memory.py,sha256=DolRhT2_JCeCi7r4ijdG8-Va33Us4BnAnCqAmMvJazo,1569
|
14
|
+
rmonkey/plan/__init__.py,sha256=smH3OGj7ppdR3zb9kh_CRJH0OjkrlMt4V9jYauaXwUQ,74
|
15
|
+
rmonkey/plan/planning.py,sha256=0k3GHck72qiybkdM4vWoKwf8Bod09J4pxgP5qpYo-iM,4146
|
16
|
+
rmonkey/tools/__init__.py,sha256=H0h-im1wnie9IMl0FJrmSigdu4ZyILfKeXw17jjXhfo,465
|
17
|
+
rmonkey/tools/_base.py,sha256=pLWRD6A8_Ebi42zxH9n_PjUpYYkOgA9gKUZnnIMXSrQ,881
|
18
|
+
rmonkey/tools/code_interpreter.py,sha256=1FP-Xr-QiU66SvUprHMZdKgXSRQ7MPDkt7EQseud8yc,796
|
19
|
+
rmonkey/tools/human.py,sha256=4gkMLQml14NUWvlQvKEVZO_-LMeyMkKLmwnfWEc3Y2M,677
|
20
|
+
rmonkey/tools/terminal_bash.py,sha256=tMhI9MVTPRy_GJqFKZMyCyDF_JV4Qubo4OnPVunDzE0,1014
|
21
|
+
rmonkey/tools/text_file_editor.py,sha256=sT5LqqLka2TKf6f4ZOa-0elj7o4XMPlwQkolohXtrCw,4836
|
22
|
+
rmonkey/tools/think.py,sha256=ye4nyMxJ2S1TcfFuzG2Bfplyus2kcs_RvdZ2MB-60a4,467
|
23
|
+
rmonkey/tools/tool_set.py,sha256=je6YOV4v7dPFa2VnYH_pCoPmLl2abQaHxL-pJzcsfy4,646
|
24
|
+
rmonkey/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
|
+
rmonkey/utils/os_info.py,sha256=RSId0xkbwXzwcOIzbE05lOw95dHXnFvRMWd6_Mi1fPs,261
|
26
|
+
rmonkey/utils/pretty_console.py,sha256=m0nUZ3IAi47rsckhOIRFKqiMKvipN_Ztqt0AlhKJWnM,444
|
27
|
+
rmonkey/utils/schema.py,sha256=l7ftopJcZAXzpBZ9JYiULH7qVheSS3vbuIgBtctvptw,773
|
28
|
+
rmonkey/utils/user_rules.py,sha256=D8MkLHyqUFZCBg1PMLW915jun_SKVoo4yzsH0xV1Gzk,439
|
29
|
+
rmonkey/utils/util.py,sha256=nup6b8FqTUTQVgn-yhFnkvNkFNBjMeg7wB3wEsa0vVM,336
|
30
|
+
rmk-0.1.0.dist-info/METADATA,sha256=7BxaLxt9solcC_HHesVblXhy0kOA7An1PRIzVBbNdcQ,1164
|
31
|
+
rmk-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
32
|
+
rmk-0.1.0.dist-info/entry_points.txt,sha256=CRR8j38CZ68SvkiYhm7FjCwKRXJrRHHCzNd5HT2LfSI,58
|
33
|
+
rmk-0.1.0.dist-info/RECORD,,
|
rmonkey/__init__.py
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
from rmonkey.action.tool_action import ToolAction
|
2
|
+
from rmonkey.agents import AskAgent, RootMonkey, SWEAgent
|
3
|
+
from rmonkey.memory import Memory
|
4
|
+
from rmonkey.utils.schema import Message, Role
|
5
|
+
|
6
|
+
__version__ = "0.1.0.dev"
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"Role",
|
10
|
+
"Message",
|
11
|
+
"AskAgent",
|
12
|
+
"RootMonkey",
|
13
|
+
"SWEAgent",
|
14
|
+
"Memory",
|
15
|
+
"ToolAction",
|
16
|
+
"__version__",
|
17
|
+
]
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
from rmonkey.tools import Tool
|
4
|
+
|
5
|
+
|
6
|
+
class ToolAction:
|
7
|
+
def __init__(self, tools: list[Tool] | None = None):
|
8
|
+
self.tools = tools
|
9
|
+
self.tool_map = {tool.name: tool for tool in tools}
|
10
|
+
|
11
|
+
def execute(self, name: str, arguments: str, **kwargs) -> str:
|
12
|
+
tool = self.tool_map.get(name, None)
|
13
|
+
if not tool:
|
14
|
+
return f"Tool {name} is not available."
|
15
|
+
try:
|
16
|
+
arguments = json.loads(arguments)
|
17
|
+
except json.JSONDecodeError:
|
18
|
+
return f"Invalid arguments for tool {name}."
|
19
|
+
|
20
|
+
try:
|
21
|
+
return tool(**arguments)
|
22
|
+
except Exception as e:
|
23
|
+
return str(e)
|
rmonkey/agents/_base.py
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
import os
|
2
|
+
from contextlib import ExitStack
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from rmonkey.action import ToolAction
|
6
|
+
from rmonkey.llm import OpenAIHandler
|
7
|
+
from rmonkey.memory import Memory
|
8
|
+
from rmonkey.tools import Tool, Toolset
|
9
|
+
from rmonkey.utils.pretty_console import PrettyConsole
|
10
|
+
from rmonkey.utils.schema import Message, Role
|
11
|
+
from rmonkey.utils.util import generate_session_id
|
12
|
+
|
13
|
+
|
14
|
+
class Agent:
|
15
|
+
# attributes
|
16
|
+
name: str = "BaseAgent"
|
17
|
+
description: str = "This is a base agent that does nothing."
|
18
|
+
|
19
|
+
# https://lilianweng.github.io/posts/2023-06-23-agent/
|
20
|
+
# agent key components: LLM, Memory, Tools, Planning, Action
|
21
|
+
|
22
|
+
llm_handler: OpenAIHandler = None
|
23
|
+
system: str = None
|
24
|
+
system_rules: str = None
|
25
|
+
|
26
|
+
memory: Memory = None
|
27
|
+
tools: Toolset = None
|
28
|
+
action: ToolAction = None
|
29
|
+
planning: str = None
|
30
|
+
|
31
|
+
# runtime settings
|
32
|
+
session_id: str | None = None
|
33
|
+
max_turns: int = 30
|
34
|
+
run_step: int = 0
|
35
|
+
run_state = None
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
name: str,
|
40
|
+
system: str = None,
|
41
|
+
tools: list[Tool] | None = None,
|
42
|
+
provider: str = "openai",
|
43
|
+
model: str = "gpt-4.1",
|
44
|
+
max_tokens: int = 4096,
|
45
|
+
temperature: float = 0.7,
|
46
|
+
session_id: str | None = None,
|
47
|
+
verbose: bool = False,
|
48
|
+
console: PrettyConsole = None,
|
49
|
+
**kwargs,
|
50
|
+
):
|
51
|
+
self.name = name
|
52
|
+
if system is not None:
|
53
|
+
self.system = system
|
54
|
+
self.system_rules = kwargs.get("system_rules")
|
55
|
+
self.model = model
|
56
|
+
self.max_tokens = max_tokens
|
57
|
+
self.temperature = temperature
|
58
|
+
|
59
|
+
if tools:
|
60
|
+
self.tools = Toolset(tools=tools)
|
61
|
+
self.action = ToolAction(tools=tools)
|
62
|
+
|
63
|
+
self.init_llm_handler(provider)
|
64
|
+
|
65
|
+
self.session_id = session_id if session_id else generate_session_id()
|
66
|
+
self.memory = Memory(id=self.session_id)
|
67
|
+
if self.system:
|
68
|
+
system_content = self.system
|
69
|
+
if self.system_rules:
|
70
|
+
system_content = f"{system_content}\n\n<system>\n{self.system_rules}\n</system>"
|
71
|
+
self.memory.add_message(role=Role.SYSTEM, content=system_content)
|
72
|
+
self.verbose = verbose
|
73
|
+
self.console = console
|
74
|
+
|
75
|
+
@classmethod
|
76
|
+
def create(cls, **kwargs) -> "Agent":
|
77
|
+
return cls(**kwargs)
|
78
|
+
|
79
|
+
def init_llm_handler(self, provider: str = "openai"):
|
80
|
+
if provider == "openai":
|
81
|
+
self.llm_handler = OpenAIHandler(
|
82
|
+
base_url=os.getenv("BASE_URL", None),
|
83
|
+
api_version=os.getenv("API_VERSION", None),
|
84
|
+
api_key=os.getenv("OPENAI_API_KEY", None),
|
85
|
+
model=os.getenv("OPENAI_MODEL", self.model),
|
86
|
+
max_tokens=self.max_tokens,
|
87
|
+
temperature=self.temperature,
|
88
|
+
)
|
89
|
+
else:
|
90
|
+
raise ValueError(f"Unsupported LLM provider: {provider}")
|
91
|
+
|
92
|
+
def _llm_with_tool(self, prompt: str) -> Message:
|
93
|
+
"""process a prompt and handle tool calls in a loop"""
|
94
|
+
|
95
|
+
self.memory.add_message(role=Role.USER, content=prompt)
|
96
|
+
if self.verbose:
|
97
|
+
self.console.print(prompt, Role.USER)
|
98
|
+
while True:
|
99
|
+
result: Message = self.llm_handler.call(
|
100
|
+
self.memory.get_messages(),
|
101
|
+
self.tools.schema() if self.tools else None,
|
102
|
+
)
|
103
|
+
self.memory.add_message(result)
|
104
|
+
if result.tool_calls:
|
105
|
+
if self.verbose and result.content:
|
106
|
+
self.console.print(result.content, result.role)
|
107
|
+
for tool_call in result.tool_calls:
|
108
|
+
if self.verbose:
|
109
|
+
self.console.print(tool_call, result.role)
|
110
|
+
input("Press Enter to continue with the tool call...")
|
111
|
+
name = tool_call.function.name
|
112
|
+
arguments = tool_call.function.arguments
|
113
|
+
tool_call_output = self.action.execute(name=name, arguments=arguments)
|
114
|
+
self.memory.add_message(role=Role.TOOL, content=tool_call_output, tool_call_id=tool_call.id)
|
115
|
+
if self.verbose:
|
116
|
+
self.console.print(tool_call_output, Role.TOOL)
|
117
|
+
else:
|
118
|
+
if self.verbose:
|
119
|
+
self.console.print(result.content, result.role)
|
120
|
+
return result
|
121
|
+
|
122
|
+
def run(self, prompt: str) -> str:
|
123
|
+
agent_result = None
|
124
|
+
|
125
|
+
with ExitStack() as stack:
|
126
|
+
try:
|
127
|
+
step_response = self._llm_with_tool(prompt)
|
128
|
+
agent_result = step_response.content
|
129
|
+
except Exception as e:
|
130
|
+
print(f"Agent Sys Error: {e}")
|
131
|
+
return agent_result
|
132
|
+
|
133
|
+
def save(self, save_dir: str) -> str:
|
134
|
+
if not Path(save_dir).exists():
|
135
|
+
Path(save_dir).mkdir(parents=True)
|
136
|
+
save_file = Path(save_dir) / f"{self.session_id}.jsonl"
|
137
|
+
self.memory.save(save_file)
|
138
|
+
return str(save_file)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from rmonkey.agents import Agent
|
2
|
+
from rmonkey.plan import Planning
|
3
|
+
from rmonkey.tools import TerminalBash, TextFileEditor, Think
|
4
|
+
|
5
|
+
|
6
|
+
class RootMonkey(Agent):
|
7
|
+
def __init__(self, system_rules: str, session_id: str, verbose: bool = False, console=None, *args, **kwargs):
|
8
|
+
super().__init__(
|
9
|
+
name="RootMonkey",
|
10
|
+
system="As an autonomous AI agent, I am known as Root Monkey (rmk).",
|
11
|
+
system_rules=system_rules,
|
12
|
+
tools=[
|
13
|
+
Planning(),
|
14
|
+
TextFileEditor(),
|
15
|
+
TerminalBash(),
|
16
|
+
Think(),
|
17
|
+
],
|
18
|
+
session_id=session_id,
|
19
|
+
verbose=verbose,
|
20
|
+
console=console,
|
21
|
+
)
|
22
|
+
self.description = "Root Monkey, an autonomous AI Agent system."
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from rmonkey.agents import Agent
|
2
|
+
from rmonkey.plan import Planning
|
3
|
+
from rmonkey.tools import CodeInterpreter, TerminalBash, TextFileEditor, Think
|
4
|
+
|
5
|
+
|
6
|
+
class SWEAgent(Agent):
|
7
|
+
def __init__(self, *args, **kwargs):
|
8
|
+
super().__init__(
|
9
|
+
name="SWEAgent",
|
10
|
+
system="You are an autonomous AI software engineer.",
|
11
|
+
tools=[
|
12
|
+
Planning(),
|
13
|
+
TextFileEditor(),
|
14
|
+
TerminalBash(),
|
15
|
+
CodeInterpreter(),
|
16
|
+
Think(),
|
17
|
+
],
|
18
|
+
)
|
19
|
+
self.description = "an autonomous AI Software Engineer agent."
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# The CLI entrypoint to root monkey (rmk).
|
2
|
+
|
3
|
+
import argparse
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
import signal
|
7
|
+
import sys
|
8
|
+
from pathlib import Path
|
9
|
+
|
10
|
+
from rmonkey import AskAgent, RootMonkey, SWEAgent, __version__
|
11
|
+
from rmonkey.utils import os_info, user_rules
|
12
|
+
from rmonkey.utils.pretty_console import PrettyConsole
|
13
|
+
from rmonkey.utils.util import generate_session_id
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
agent: AskAgent | SWEAgent | RootMonkey = None
|
18
|
+
agent_log_dir = Path.cwd() / ".rmk" / "traj"
|
19
|
+
|
20
|
+
|
21
|
+
def _register_signal_handlers():
|
22
|
+
def signal_handler(signum, frame):
|
23
|
+
logger.debug(f"Received {signum} signal.")
|
24
|
+
logger.info(f"Saved trajectory to {agent_log_dir} and exiting...")
|
25
|
+
global agent
|
26
|
+
traj_file = agent.save(agent_log_dir)
|
27
|
+
print(f"Saved agent trajectory to {traj_file} and exiting...")
|
28
|
+
sys.exit(0)
|
29
|
+
|
30
|
+
signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C
|
31
|
+
signal.signal(signal.SIGTSTP, signal_handler) # Handle Ctrl+Z
|
32
|
+
|
33
|
+
|
34
|
+
def main():
|
35
|
+
_register_signal_handlers()
|
36
|
+
parser = argparse.ArgumentParser(description="RootMonkey CLI.")
|
37
|
+
|
38
|
+
parser.add_argument(
|
39
|
+
"--version",
|
40
|
+
"-v",
|
41
|
+
action="version",
|
42
|
+
version=f"%(prog)s {__version__}",
|
43
|
+
help="Show the version number and exit",
|
44
|
+
)
|
45
|
+
parser.add_argument(
|
46
|
+
"--mode",
|
47
|
+
"-m",
|
48
|
+
type=str,
|
49
|
+
default="dev",
|
50
|
+
choices=["ask", "agent", "dev"],
|
51
|
+
required=False,
|
52
|
+
help="RMK mode: ask, agent, or dev. default is 'dev'.",
|
53
|
+
)
|
54
|
+
parser.add_argument(
|
55
|
+
"--task",
|
56
|
+
"-t",
|
57
|
+
type=str,
|
58
|
+
help="user's input task.",
|
59
|
+
)
|
60
|
+
parser.add_argument(
|
61
|
+
"--verbose",
|
62
|
+
action="store_true",
|
63
|
+
help="Enable verbose output to observe LLM behavior.",
|
64
|
+
)
|
65
|
+
args = parser.parse_args()
|
66
|
+
|
67
|
+
global agent
|
68
|
+
console = PrettyConsole()
|
69
|
+
session_id = generate_session_id()
|
70
|
+
verbose = args.verbose
|
71
|
+
if args.mode == "ask":
|
72
|
+
agent = AskAgent()
|
73
|
+
while True:
|
74
|
+
try:
|
75
|
+
input_message = input("RMK[ask] > ")
|
76
|
+
if not input_message.strip():
|
77
|
+
console.print("Input cannot be empty. Please try again.")
|
78
|
+
continue
|
79
|
+
if input_message.strip().lower() in ["/exit", "/quit"]:
|
80
|
+
traj_file = agent.save(agent_log_dir)
|
81
|
+
print(f"Saved ask history to {traj_file} and exiting...")
|
82
|
+
return
|
83
|
+
result = agent.run(input_message)
|
84
|
+
console.print(f"Agent Result:\n{result}", "assistant")
|
85
|
+
except Exception as e:
|
86
|
+
print(str(e))
|
87
|
+
return
|
88
|
+
elif args.mode == "agent":
|
89
|
+
agent = SWEAgent()
|
90
|
+
input_task = args.task
|
91
|
+
while True:
|
92
|
+
try:
|
93
|
+
if not input_task:
|
94
|
+
input_task = input("RMK[agent] > ")
|
95
|
+
if not input_task.strip():
|
96
|
+
console.print("Input task cannot be empty. Please try again.")
|
97
|
+
continue
|
98
|
+
result = agent.run(input_task)
|
99
|
+
console.print(f"RMK[agent] > \n{result}")
|
100
|
+
input_message = input("RMK[agent] > ")
|
101
|
+
if input_message.strip().lower() in ["/exit", "/quit"]:
|
102
|
+
traj_file = agent.save(agent_log_dir)
|
103
|
+
print(f"Saved agent trajectory to {traj_file} and exiting...")
|
104
|
+
return
|
105
|
+
except Exception as e:
|
106
|
+
print(str(e))
|
107
|
+
return
|
108
|
+
elif args.mode == "dev":
|
109
|
+
system_ctx = os_info.system()
|
110
|
+
user_rules_ctx = user_rules.load_rmk_rules(os.getcwd())
|
111
|
+
if user_rules_ctx:
|
112
|
+
system_ctx = f"{system_ctx}\n{user_rules_ctx}"
|
113
|
+
agent = RootMonkey(system_rules=system_ctx, session_id=session_id, verbose=verbose, console=console)
|
114
|
+
input_task = args.task
|
115
|
+
try:
|
116
|
+
if not input_task:
|
117
|
+
while True:
|
118
|
+
input_task = input("RMK[dev] > ")
|
119
|
+
if not input_task.strip():
|
120
|
+
console.print("Input task cannot be empty. Please try again.")
|
121
|
+
else:
|
122
|
+
break
|
123
|
+
result = agent.run(input_task)
|
124
|
+
console.print(f"RMK[dev] > {result}", "assistant")
|
125
|
+
traj_file = agent.save(agent_log_dir)
|
126
|
+
print(f"Saved dev trajectory to {traj_file} and exiting...")
|
127
|
+
except Exception as e:
|
128
|
+
print(str(e))
|
129
|
+
else:
|
130
|
+
parser.print_help()
|
131
|
+
|
132
|
+
|
133
|
+
if __name__ == "__main__":
|
134
|
+
main()
|
rmonkey/llm/__init__.py
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI
|
5
|
+
from openai.types.chat import ChatCompletion, ChatCompletionMessage
|
6
|
+
|
7
|
+
from rmonkey.utils.schema import Message, Role
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
class OpenAIHandler:
|
13
|
+
base_url: str = None
|
14
|
+
api_version: str = None
|
15
|
+
api_key: str = None
|
16
|
+
model: str = "gpt-4.1"
|
17
|
+
max_tokens: int = 1024 * 8
|
18
|
+
temperature: float = 0.7
|
19
|
+
|
20
|
+
client: AzureOpenAI | OpenAI = None
|
21
|
+
aclient: AsyncAzureOpenAI | AsyncOpenAI = None
|
22
|
+
|
23
|
+
def __init__(
|
24
|
+
self, base_url: str, api_version: str, api_key: str, model: str, max_tokens: int, temperature: float, **kwargs
|
25
|
+
):
|
26
|
+
self.base_url = base_url
|
27
|
+
self.api_version = api_version
|
28
|
+
self.api_key = api_key
|
29
|
+
self.model = model
|
30
|
+
self.max_tokens = max_tokens
|
31
|
+
self.temperature = temperature
|
32
|
+
|
33
|
+
if api_version and "azure.com" in base_url:
|
34
|
+
self.aclient = AsyncAzureOpenAI(
|
35
|
+
azure_endpoint=base_url,
|
36
|
+
api_version=api_version,
|
37
|
+
api_key=api_key,
|
38
|
+
)
|
39
|
+
self.client = AzureOpenAI(
|
40
|
+
azure_endpoint=base_url,
|
41
|
+
api_version=api_version,
|
42
|
+
api_key=api_key,
|
43
|
+
)
|
44
|
+
else:
|
45
|
+
self.aclient = AsyncOpenAI(
|
46
|
+
api_key=api_key,
|
47
|
+
)
|
48
|
+
self.client = OpenAI(
|
49
|
+
api_key=api_key,
|
50
|
+
)
|
51
|
+
|
52
|
+
def call(
|
53
|
+
self,
|
54
|
+
messages: list[dict],
|
55
|
+
tools: list[dict[str, Any]] | None = None,
|
56
|
+
model: str | None = None,
|
57
|
+
max_tokens: int | None = None,
|
58
|
+
temperature: float | None = None,
|
59
|
+
stream: bool = False,
|
60
|
+
top_p: float | None = None,
|
61
|
+
) -> Message:
|
62
|
+
if stream and tools:
|
63
|
+
logger.warning("Streaming is not supported for tool calls. Disabling streaming.")
|
64
|
+
stream = False
|
65
|
+
|
66
|
+
_config = {
|
67
|
+
"model": model if model else self.model,
|
68
|
+
"messages": messages,
|
69
|
+
"max_tokens": max_tokens if max_tokens else self.max_tokens,
|
70
|
+
"temperature": temperature if temperature else self.temperature,
|
71
|
+
"top_p": top_p if top_p else 0.95,
|
72
|
+
}
|
73
|
+
if tools:
|
74
|
+
_config["tools"] = tools
|
75
|
+
|
76
|
+
if not stream:
|
77
|
+
response: ChatCompletion = self.client.chat.completions.create(**_config)
|
78
|
+
if not response.choices or not response.choices[0].message:
|
79
|
+
raise ValueError(f"Invalid response from LLM {_config['model']}")
|
80
|
+
_message: ChatCompletionMessage = response.choices[0].message
|
81
|
+
response_content = _message.content
|
82
|
+
tool_calls = _message.tool_calls
|
83
|
+
return Message(role=Role.ASSISTANT, content=response_content, tool_calls=tool_calls)
|
84
|
+
else:
|
85
|
+
_config["stream"] = True
|
86
|
+
response = self.client.chat.completions.create(**_config)
|
87
|
+
chunks = []
|
88
|
+
|
89
|
+
for chunk in response:
|
90
|
+
chunk_text = chunk.choices[0].delta.content or ""
|
91
|
+
chunks.append(chunk_text)
|
92
|
+
print(chunk_text, end="", flush=True)
|
93
|
+
response_content = "".join(chunks)
|
94
|
+
return Message(role=Role.ASSISTANT, content=response_content)
|
95
|
+
|
96
|
+
async def acall(
|
97
|
+
self,
|
98
|
+
messages: list[dict],
|
99
|
+
tools: list[dict[str, Any]] | None = None,
|
100
|
+
config: dict[str, Any] | None = None,
|
101
|
+
) -> Message:
|
102
|
+
stream = config.get("stream", False)
|
103
|
+
if stream and tools:
|
104
|
+
logger.warning("Streaming is not supported for tool calls. Disabling streaming.")
|
105
|
+
stream = False
|
106
|
+
|
107
|
+
_config = {
|
108
|
+
"model": config.get("model", "gpt-4.1"),
|
109
|
+
"messages": messages,
|
110
|
+
"max_tokens": config.get("max_tokens", 1024 * 8),
|
111
|
+
"temperature": config.get("temperature", 0.7),
|
112
|
+
"top_p": config.get("top_p", 0.95),
|
113
|
+
}
|
114
|
+
if tools:
|
115
|
+
_config["tools"] = tools
|
116
|
+
|
117
|
+
if not stream:
|
118
|
+
response: ChatCompletion = await self.aclient.chat.completions.create(**_config)
|
119
|
+
if not response.choices or not response.choices[0].message:
|
120
|
+
raise ValueError(f"Invalid response from LLM {_config['model']}")
|
121
|
+
_message: ChatCompletionMessage = response.choices[0].message
|
122
|
+
response_content = _message.content
|
123
|
+
tool_calls = _message.tool_calls
|
124
|
+
return Message(role=Role.ASSISTANT, content=response_content, tool_calls=tool_calls)
|
125
|
+
else:
|
126
|
+
_config["stream"] = True
|
127
|
+
chunks = []
|
128
|
+
async for chunk in self.aclient.chat.completions.create(**_config):
|
129
|
+
chunk_text = chunk.choices[0].delta.content or ""
|
130
|
+
chunks.append(chunk_text)
|
131
|
+
print(chunk_text, end="", flush=True)
|
132
|
+
response_content = "".join(chunks)
|
133
|
+
return Message(role=Role.ASSISTANT, content=response_content)
|
134
|
+
|
135
|
+
|
136
|
+
def mock_openai_call(
|
137
|
+
messages: list[dict],
|
138
|
+
tools: list[dict[str, Any]] | None = None,
|
139
|
+
**kwargs,
|
140
|
+
) -> Message:
|
141
|
+
from openai.types.chat import ChatCompletionMessageToolCall
|
142
|
+
from openai.types.chat.chat_completion_message_tool_call import Function
|
143
|
+
|
144
|
+
mock_tool_calls = None
|
145
|
+
if tools:
|
146
|
+
mock_tool_calls = [
|
147
|
+
ChatCompletionMessageToolCall(
|
148
|
+
id="call_xxx",
|
149
|
+
type="function",
|
150
|
+
function=Function(
|
151
|
+
name="get_weather",
|
152
|
+
arguments='{"location":"Paris, France"}',
|
153
|
+
),
|
154
|
+
)
|
155
|
+
]
|
156
|
+
return Message(
|
157
|
+
role=Role.ASSISTANT,
|
158
|
+
content="This is a mock response.",
|
159
|
+
tool_calls=mock_tool_calls,
|
160
|
+
)
|
rmonkey/memory/memory.py
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
from rmonkey.utils.schema import Message
|
4
|
+
|
5
|
+
|
6
|
+
class Memory:
|
7
|
+
id: str = None
|
8
|
+
messages: list[Message] = []
|
9
|
+
|
10
|
+
max_messages: int = 100
|
11
|
+
context_window_tokens: int = 128_000
|
12
|
+
total_tokens: int = 0
|
13
|
+
|
14
|
+
def __init__(self, id: str = None, max_messages: int = 100, context_window_tokens: int = 128_000) -> None:
|
15
|
+
self.id = id
|
16
|
+
self.max_messages = max_messages
|
17
|
+
self.context_window_tokens = context_window_tokens
|
18
|
+
self.messages = []
|
19
|
+
self.total_tokens = 0
|
20
|
+
|
21
|
+
def set_id(self, session_id: str) -> None:
|
22
|
+
self.session_id = session_id
|
23
|
+
|
24
|
+
def add_message(
|
25
|
+
self,
|
26
|
+
message: Message = None,
|
27
|
+
role: str = None,
|
28
|
+
content: str = None,
|
29
|
+
tool_calls: list[dict] | None = None,
|
30
|
+
tool_call_id: str | None = None,
|
31
|
+
) -> None:
|
32
|
+
if message is not None and isinstance(message, Message):
|
33
|
+
self.messages.append(message)
|
34
|
+
else:
|
35
|
+
m = Message(role=role, content=content, tool_calls=tool_calls, tool_call_id=tool_call_id)
|
36
|
+
self.messages.append(m)
|
37
|
+
|
38
|
+
def clear(self) -> None:
|
39
|
+
self.messages.clear()
|
40
|
+
|
41
|
+
def get_messages(self) -> list[dict]:
|
42
|
+
return [msg.json() for msg in self.messages]
|
43
|
+
|
44
|
+
def truncate(self) -> None:
|
45
|
+
if len(self.messages) > self.max_messages:
|
46
|
+
self.messages = self.messages[-self.max_messages :]
|
47
|
+
|
48
|
+
def save(self, save_path: str):
|
49
|
+
with open(save_path, "w") as f:
|
50
|
+
for msg in self.messages:
|
51
|
+
f.write(json.dumps(msg.json(exclude_none=True)) + "\n")
|
rmonkey/plan/__init__.py
ADDED
rmonkey/plan/planning.py
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
import re
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from rmonkey.tools import Tool
|
5
|
+
|
6
|
+
_description = """
|
7
|
+
This planning tool helps you view, create, manage, and break down highly complex tasks into manageable sub-tasks, making intricate problem-solving more efficient.
|
8
|
+
**Only employ this tool when you encounter a task that is exceptionally difficult and complex**
|
9
|
+
""".strip() # noqa: E501
|
10
|
+
|
11
|
+
|
12
|
+
def parse_diff_blocks(diff: str) -> list:
|
13
|
+
pattern = r"<<<<<<< SEARCH\n(.*?)\n=======\n(.*?)\n>>>>>>> REPLACE"
|
14
|
+
matches = re.findall(pattern, diff, re.DOTALL)
|
15
|
+
blocks = []
|
16
|
+
for s, r in matches:
|
17
|
+
blocks.append((s, r))
|
18
|
+
return blocks
|
19
|
+
|
20
|
+
|
21
|
+
class Planning(Tool):
|
22
|
+
name: str = "planning"
|
23
|
+
description: str = _description
|
24
|
+
|
25
|
+
parameters: dict = {
|
26
|
+
"type": "object",
|
27
|
+
"properties": {
|
28
|
+
"operation": {
|
29
|
+
"description": "The operation to perform.",
|
30
|
+
"enum": [
|
31
|
+
"view",
|
32
|
+
"create",
|
33
|
+
"update",
|
34
|
+
"decompose",
|
35
|
+
],
|
36
|
+
"type": "string",
|
37
|
+
},
|
38
|
+
"path": {
|
39
|
+
"description": "The absolute file path for storing the plan.",
|
40
|
+
"type": "string",
|
41
|
+
},
|
42
|
+
"content": {
|
43
|
+
"description": "The Markdown-formatted overall plan to achieve the goal. Required for `create`.",
|
44
|
+
"type": "string",
|
45
|
+
},
|
46
|
+
"diff_content": {
|
47
|
+
"description": (
|
48
|
+
"To update the current plan in the `path`, "
|
49
|
+
"provide the diff content as SEARCH/REPLACE blocks. Required for `update`."
|
50
|
+
),
|
51
|
+
"type": "string",
|
52
|
+
},
|
53
|
+
"subtasks": {
|
54
|
+
"description": "The list of tasks or steps to be performed. "
|
55
|
+
"Subtasks will be appended to the plan. Required for `decompose`.",
|
56
|
+
"type": "array",
|
57
|
+
"items": {"type": "string"},
|
58
|
+
},
|
59
|
+
},
|
60
|
+
"required": ["command", "path"],
|
61
|
+
"additionalProperties": False,
|
62
|
+
}
|
63
|
+
|
64
|
+
def execute(self, **kwargs) -> str:
|
65
|
+
# valid parameters
|
66
|
+
# operation: str, path: str, content: str = None, diff_content: str = None, subtasks: str = None
|
67
|
+
operation = kwargs.get("operation")
|
68
|
+
path = kwargs.get("path")
|
69
|
+
content = kwargs.get("content")
|
70
|
+
diff_content = kwargs.get("diff_content")
|
71
|
+
subtasks = kwargs.get("subtasks")
|
72
|
+
|
73
|
+
if not operation:
|
74
|
+
return f"{self.name}: `operation` is required."
|
75
|
+
if not path:
|
76
|
+
return f"{self.name}: `path` is required."
|
77
|
+
_path = Path(path)
|
78
|
+
if not _path.exists():
|
79
|
+
return f"{self.name}: File not found: {path}"
|
80
|
+
|
81
|
+
if operation == "view":
|
82
|
+
plan_content = Path(path).read_text()
|
83
|
+
return plan_content
|
84
|
+
elif operation == "create":
|
85
|
+
if content is None:
|
86
|
+
return f"{self.name}: `content` is need for the `create`."
|
87
|
+
Path(path).write_text(content)
|
88
|
+
return "created plan."
|
89
|
+
elif operation == "update":
|
90
|
+
if diff_content is None:
|
91
|
+
return f"{self.name}: `diff_content` is need for the `update`."
|
92
|
+
try:
|
93
|
+
file_content = _path.read_text(encoding="utf-8")
|
94
|
+
blocks = parse_diff_blocks(diff_content)
|
95
|
+
for old_str, new_str in blocks:
|
96
|
+
file_content = file_content.replace(old_str, new_str)
|
97
|
+
_path.write_text(file_content)
|
98
|
+
return "updated plan."
|
99
|
+
except Exception as e:
|
100
|
+
return f"{self.name}: Error updating plan: {e}"
|
101
|
+
elif operation == "decompose":
|
102
|
+
if subtasks is None:
|
103
|
+
return f"{self.name}: `subtasks` is need for the `decompose`."
|
104
|
+
subtasks_str = "\n".join([f"[ ] Task-{i}\n{s}" for i, s in enumerate(subtasks, start=1)])
|
105
|
+
with open(path, "a") as f:
|
106
|
+
f.write("\n" + subtasks_str)
|
107
|
+
return "subtasks is added to the plan."
|
108
|
+
return f"{self.name}: {operation} is unsupported."
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from rmonkey.tools._base import Tool
|
2
|
+
from rmonkey.tools.code_interpreter import CodeInterpreter
|
3
|
+
from rmonkey.tools.human import AskHuman
|
4
|
+
from rmonkey.tools.terminal_bash import TerminalBash
|
5
|
+
from rmonkey.tools.text_file_editor import TextFileEditor
|
6
|
+
from rmonkey.tools.think import Think
|
7
|
+
from rmonkey.tools.tool_set import Toolset
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"Tool",
|
11
|
+
"CodeInterpreter",
|
12
|
+
"AskHuman",
|
13
|
+
"TerminalBash",
|
14
|
+
"Toolset",
|
15
|
+
"Think",
|
16
|
+
"TextFileEditor",
|
17
|
+
]
|
rmonkey/tools/_base.py
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
|
6
|
+
class Tool(BaseModel):
|
7
|
+
name: str
|
8
|
+
description: str
|
9
|
+
parameters: dict[str, Any] | None = None
|
10
|
+
|
11
|
+
def __call__(self, **kwargs) -> Any:
|
12
|
+
return self.execute(**kwargs)
|
13
|
+
|
14
|
+
def execute(self, **kwargs) -> str:
|
15
|
+
"""Execute the tool with provided parameters."""
|
16
|
+
raise NotImplementedError("Tool subclasses must implement execute method")
|
17
|
+
|
18
|
+
async def aexecute(self, **kwargs) -> str:
|
19
|
+
"""Execute the tool with provided parameters."""
|
20
|
+
raise NotImplementedError("Tool subclasses must implement execute method")
|
21
|
+
|
22
|
+
def schema(self) -> dict[str, Any]:
|
23
|
+
return {
|
24
|
+
"type": "function",
|
25
|
+
"function": {
|
26
|
+
"name": self.name,
|
27
|
+
"description": self.description,
|
28
|
+
"parameters": self.parameters,
|
29
|
+
},
|
30
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from rmonkey.tools._base import Tool
|
2
|
+
|
3
|
+
|
4
|
+
class CodeInterpreter(Tool):
|
5
|
+
name: str = "code_interpreter"
|
6
|
+
description: str = "Executes a Python code snippet and returns the print statement output."
|
7
|
+
parameters: dict = {
|
8
|
+
"type": "object",
|
9
|
+
"properties": {
|
10
|
+
"code": {
|
11
|
+
"type": "string",
|
12
|
+
"description": "The Python script to execute.",
|
13
|
+
},
|
14
|
+
},
|
15
|
+
"required": ["code"],
|
16
|
+
}
|
17
|
+
|
18
|
+
def execute(self, code: str, language: str = "python") -> str:
|
19
|
+
assert language == "python", "Only Python code is supported."
|
20
|
+
try:
|
21
|
+
local_scope = {}
|
22
|
+
exec(code, {}, local_scope)
|
23
|
+
return str(local_scope)
|
24
|
+
except Exception as e:
|
25
|
+
return f"Error executing code: {str(e)}"
|
rmonkey/tools/human.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
from ._base import Tool
|
2
|
+
|
3
|
+
|
4
|
+
class AskHuman(Tool):
|
5
|
+
name: str = "ask_human"
|
6
|
+
description: str = "Ask human for help and get a response from human."
|
7
|
+
parameters: dict = {
|
8
|
+
"type": "object",
|
9
|
+
"properties": {
|
10
|
+
"question": {
|
11
|
+
"type": "string",
|
12
|
+
"description": "The question to ask the human.",
|
13
|
+
}
|
14
|
+
},
|
15
|
+
"required": ["question"],
|
16
|
+
}
|
17
|
+
|
18
|
+
def execute(self, **kwargs) -> str:
|
19
|
+
question = kwargs.get("question", "")
|
20
|
+
human_input = input(f"rMonkey: {question}\nYou: ").strip()
|
21
|
+
if not human_input:
|
22
|
+
return "No response provided by human."
|
23
|
+
return human_input
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import subprocess
|
2
|
+
|
3
|
+
from ._base import Tool
|
4
|
+
|
5
|
+
|
6
|
+
class TerminalBash(Tool):
|
7
|
+
name: str = "bash"
|
8
|
+
description: str = "Execute a command in the terminal and return the output."
|
9
|
+
parameters: dict = {
|
10
|
+
"type": "object",
|
11
|
+
"properties": {
|
12
|
+
"command": {
|
13
|
+
"type": "string",
|
14
|
+
"description": "Command to execute in the bash shell.",
|
15
|
+
},
|
16
|
+
},
|
17
|
+
"required": ["command"],
|
18
|
+
}
|
19
|
+
|
20
|
+
def execute(self, **kwargs) -> str:
|
21
|
+
command = kwargs.get("command", "")
|
22
|
+
if not command:
|
23
|
+
return "No command provided."
|
24
|
+
|
25
|
+
try:
|
26
|
+
result = subprocess.run(command, shell=True, check=True, text=True, capture_output=True)
|
27
|
+
return result.stdout.strip() or "Command executed successfully with no output."
|
28
|
+
except subprocess.CalledProcessError as e:
|
29
|
+
return f"Error executing command: {e.stderr.strip()}"
|
30
|
+
except Exception as e:
|
31
|
+
return f"An unexpected error occurred: {str(e)}"
|
@@ -0,0 +1,130 @@
|
|
1
|
+
import re
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from ._base import Tool
|
5
|
+
|
6
|
+
_description = """
|
7
|
+
Text file tool for viewing, creating and editing.
|
8
|
+
|
9
|
+
* view will return the content of a file, optionally within a specified line range. Each line is prefixed with its line number to help with navigation.
|
10
|
+
* create will create a new file with the specified content.
|
11
|
+
* edit will apply diff edits to an existing file.
|
12
|
+
|
13
|
+
Note: The diff format is a series of SEARCH/REPLACE blocks, every block must use this format:
|
14
|
+
1. The start of search block: <<<<<<< SEARCH
|
15
|
+
2. A contiguous chunk of lines to search for in the existing file
|
16
|
+
3. The dividing line: =======
|
17
|
+
4. The lines to replace into the source file
|
18
|
+
5. The end of replace block: >>>>>>> REPLACE
|
19
|
+
""".strip() # noqa: E501
|
20
|
+
|
21
|
+
|
22
|
+
def parse_diff_blocks(diff: str) -> list:
|
23
|
+
pattern = r"<<<<<<< SEARCH\n(.*?)\n=======\n(.*?)\n>>>>>>> REPLACE"
|
24
|
+
matches = re.findall(pattern, diff, re.DOTALL)
|
25
|
+
blocks = []
|
26
|
+
for s, r in matches:
|
27
|
+
blocks.append((s, r))
|
28
|
+
return blocks
|
29
|
+
|
30
|
+
|
31
|
+
class TextFileEditor(Tool):
|
32
|
+
name: str = "text_file_editor"
|
33
|
+
description: str = _description
|
34
|
+
parameters: dict = {
|
35
|
+
"type": "object",
|
36
|
+
"properties": {
|
37
|
+
"operation": {
|
38
|
+
"description": "File operation to perform.",
|
39
|
+
"type": "string",
|
40
|
+
"enum": ["view", "create", "edit"],
|
41
|
+
},
|
42
|
+
"path": {
|
43
|
+
"description": "Absolute file path.",
|
44
|
+
"type": "string",
|
45
|
+
},
|
46
|
+
"content": {
|
47
|
+
"description": "Plain text content. Required for `create`.",
|
48
|
+
"type": "string",
|
49
|
+
},
|
50
|
+
"diff": {
|
51
|
+
"description": "Diff content (a series of SEARCH/REPLACE blocks). Required for `edit`.",
|
52
|
+
"type": "string",
|
53
|
+
},
|
54
|
+
"view_range": {
|
55
|
+
# `sed -n 'start_line,end_linep' file.txt`
|
56
|
+
"description": "Line range to view (start and end line numbers). Optional for `view`.",
|
57
|
+
"type": "array",
|
58
|
+
"items": {"type": "integer"},
|
59
|
+
},
|
60
|
+
},
|
61
|
+
"required": ["operation", "path"],
|
62
|
+
}
|
63
|
+
|
64
|
+
def execute(self, **kwargs) -> str:
|
65
|
+
operation = kwargs.get("operation")
|
66
|
+
path = kwargs.get("path")
|
67
|
+
|
68
|
+
if operation == "view":
|
69
|
+
view_range = kwargs.get("view_range")
|
70
|
+
return self.view(path, view_range)
|
71
|
+
elif operation == "create":
|
72
|
+
content = kwargs.get("content")
|
73
|
+
return self.create(path, content)
|
74
|
+
elif operation == "edit":
|
75
|
+
diff = kwargs.get("diff")
|
76
|
+
return self.edit(path, diff)
|
77
|
+
else:
|
78
|
+
return f"{self.name}: {operation} is unsupported."
|
79
|
+
|
80
|
+
def view(self, path: str, view_range: list[int] = None) -> str:
|
81
|
+
_path = Path(path)
|
82
|
+
if not _path.exists():
|
83
|
+
return f"{self.name}: File not found: {path}"
|
84
|
+
if view_range and (len(view_range) != 2 or not all(isinstance(i, int) for i in view_range)):
|
85
|
+
return f"{self.name}: Invalid `view_range`: {view_range}."
|
86
|
+
try:
|
87
|
+
file_content = _path.read_text(encoding="utf-8")
|
88
|
+
_lines = file_content.splitlines()
|
89
|
+
if not _lines:
|
90
|
+
return ""
|
91
|
+
max_line_num = len(_lines)
|
92
|
+
width = len(str(max_line_num))
|
93
|
+
|
94
|
+
lines = [f"{i:>{width}} {line}" for i, line in enumerate(_lines, start=1)]
|
95
|
+
if not view_range:
|
96
|
+
return "\n".join(lines)
|
97
|
+
else:
|
98
|
+
start, end = view_range
|
99
|
+
start = int(start)
|
100
|
+
end = int(end)
|
101
|
+
if start < 1 or start > end:
|
102
|
+
return f"{self.name}: Invalid `view_range`: {view_range}."
|
103
|
+
return "\n".join(lines[start - 1 : end])
|
104
|
+
except Exception as e:
|
105
|
+
return f"{self.name}: Error reading file: {e}"
|
106
|
+
|
107
|
+
def create(self, path: str, content: str) -> str:
|
108
|
+
_path = Path(path)
|
109
|
+
if _path.exists():
|
110
|
+
return f"{self.name}: File already exists: {path}"
|
111
|
+
try:
|
112
|
+
_path.write_text(content, encoding="utf-8")
|
113
|
+
return f"created: {path}"
|
114
|
+
except Exception as e:
|
115
|
+
return f"{self.name}: Error creating file: {e}"
|
116
|
+
|
117
|
+
def edit(self, path: str, diff: str) -> str:
|
118
|
+
_path = Path(path)
|
119
|
+
if not _path.exists():
|
120
|
+
return f"{self.name}: File not found: {path}"
|
121
|
+
try:
|
122
|
+
file_content = _path.read_text(encoding="utf-8")
|
123
|
+
blocks = parse_diff_blocks(diff)
|
124
|
+
for old_str, new_str in blocks:
|
125
|
+
file_content = file_content.replace(old_str, new_str)
|
126
|
+
|
127
|
+
_path.write_text(file_content)
|
128
|
+
return "edited the file."
|
129
|
+
except Exception as e:
|
130
|
+
return f"{self.name}: Error editing file: {e}"
|
rmonkey/tools/think.py
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
from ._base import Tool
|
2
|
+
|
3
|
+
|
4
|
+
class Think(Tool):
|
5
|
+
name: str = "think"
|
6
|
+
description: str = "Used for self-think about something."
|
7
|
+
parameters: dict = {
|
8
|
+
"type": "object",
|
9
|
+
"properties": {
|
10
|
+
"thought": {
|
11
|
+
"type": "string",
|
12
|
+
"description": "Thinking and summary.",
|
13
|
+
}
|
14
|
+
},
|
15
|
+
"required": ["thought"],
|
16
|
+
}
|
17
|
+
|
18
|
+
def execute(self, thought: str) -> str:
|
19
|
+
return "Thinking completed."
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from rmonkey.tools._base import Tool
|
2
|
+
|
3
|
+
|
4
|
+
class Toolset:
|
5
|
+
def __init__(self, tools: list[Tool] | None = None):
|
6
|
+
self.tools = tools if tools is not None else []
|
7
|
+
|
8
|
+
def add_tool(self, tool: Tool) -> None:
|
9
|
+
self.tools.append(tool)
|
10
|
+
|
11
|
+
def get_tool(self, name: str) -> Tool | None:
|
12
|
+
for tool in self.tools:
|
13
|
+
if tool.name == name:
|
14
|
+
return tool
|
15
|
+
return None
|
16
|
+
|
17
|
+
def schema(self) -> list[dict]:
|
18
|
+
return [tool.schema() for tool in self.tools]
|
19
|
+
|
20
|
+
def save(self, filepath: str):
|
21
|
+
import json
|
22
|
+
|
23
|
+
with open(filepath, "w") as f:
|
24
|
+
json.dump(self.schema(), f, indent=2)
|
File without changes
|
rmonkey/utils/os_info.py
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
from rich.console import Console
|
2
|
+
from rich.style import Style
|
3
|
+
|
4
|
+
roles_color = {
|
5
|
+
"error": "#e32636",
|
6
|
+
"user": "#00ffff",
|
7
|
+
"system": "#bd33a4",
|
8
|
+
"assistant": "#ead9c4",
|
9
|
+
"tool": "#318ce7",
|
10
|
+
"hint": "#fdee00",
|
11
|
+
}
|
12
|
+
|
13
|
+
|
14
|
+
class PrettyConsole:
|
15
|
+
console = Console()
|
16
|
+
|
17
|
+
def print(self, content: str, key: str = "hint"):
|
18
|
+
color = roles_color.get(key)
|
19
|
+
self.console.print(f"[{key}]: {content}", style=Style(bgcolor=color))
|
rmonkey/utils/schema.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
from openai.types.chat import ChatCompletionMessageToolCall
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
|
7
|
+
class Role(str, Enum):
|
8
|
+
"""LLM API message roles"""
|
9
|
+
|
10
|
+
SYSTEM = "system"
|
11
|
+
USER = "user"
|
12
|
+
ASSISTANT = "assistant"
|
13
|
+
TOOL = "tool"
|
14
|
+
|
15
|
+
def __repr__(self) -> str:
|
16
|
+
return str.__repr__(self.value)
|
17
|
+
|
18
|
+
|
19
|
+
class Message(BaseModel):
|
20
|
+
id: str | None = None
|
21
|
+
role: Role = Field(default=Role.USER)
|
22
|
+
content: str | None = Field(default=None)
|
23
|
+
tool_calls: list[ChatCompletionMessageToolCall] | None = Field(default=None)
|
24
|
+
tool_call_id: str | None = Field(default=None)
|
25
|
+
|
26
|
+
def json(self, **kwargs): # type: ignore
|
27
|
+
exclude_none = kwargs.pop("exclude_none", False)
|
28
|
+
return self.model_dump(exclude_none=exclude_none)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
|
4
|
+
def load_rmk_rules(dir: str, file: str = "system.md", xml=True) -> str | None:
|
5
|
+
"""
|
6
|
+
Load the rules from the .rmk/rules directory.
|
7
|
+
"""
|
8
|
+
rules_file = Path(dir) / ".rmk" / "rules" / file
|
9
|
+
if not rules_file.exists():
|
10
|
+
return None
|
11
|
+
rules_content = Path(rules_file).read_text().strip()
|
12
|
+
if not rules_content:
|
13
|
+
return None
|
14
|
+
return f"<user_rules>\n{rules_content}\n</user_rules>"
|
rmonkey/utils/util.py
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
import datetime
|
2
|
+
import uuid
|
3
|
+
|
4
|
+
|
5
|
+
def generate_session_id():
|
6
|
+
current_datetime = datetime.datetime.now()
|
7
|
+
date_string = current_datetime.strftime("%Y%m%d%H%M%S")
|
8
|
+
# Generate a random UUID and convert to hex string
|
9
|
+
unique_identifier = uuid.uuid4().hex
|
10
|
+
session_id = f"{date_string}-{unique_identifier}"
|
11
|
+
return session_id
|