rmk 0.1.0__tar.gz

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.
Files changed (33) hide show
  1. rmk-0.1.0/.gitignore +10 -0
  2. rmk-0.1.0/PKG-INFO +40 -0
  3. rmk-0.1.0/README.md +26 -0
  4. rmk-0.1.0/pyproject.toml +65 -0
  5. rmk-0.1.0/src/rmonkey/__init__.py +17 -0
  6. rmk-0.1.0/src/rmonkey/action/__init__.py +3 -0
  7. rmk-0.1.0/src/rmonkey/action/tool_action.py +23 -0
  8. rmk-0.1.0/src/rmonkey/agents/__init__.py +11 -0
  9. rmk-0.1.0/src/rmonkey/agents/_base.py +138 -0
  10. rmk-0.1.0/src/rmonkey/agents/ask_agent.py +7 -0
  11. rmk-0.1.0/src/rmonkey/agents/root_monkey.py +22 -0
  12. rmk-0.1.0/src/rmonkey/agents/swe_agent.py +19 -0
  13. rmk-0.1.0/src/rmonkey/entrypoints/cli/main.py +134 -0
  14. rmk-0.1.0/src/rmonkey/llm/__init__.py +7 -0
  15. rmk-0.1.0/src/rmonkey/llm/providers/openai.py +160 -0
  16. rmk-0.1.0/src/rmonkey/memory/__init__.py +3 -0
  17. rmk-0.1.0/src/rmonkey/memory/memory.py +51 -0
  18. rmk-0.1.0/src/rmonkey/plan/__init__.py +5 -0
  19. rmk-0.1.0/src/rmonkey/plan/planning.py +108 -0
  20. rmk-0.1.0/src/rmonkey/tools/__init__.py +17 -0
  21. rmk-0.1.0/src/rmonkey/tools/_base.py +30 -0
  22. rmk-0.1.0/src/rmonkey/tools/code_interpreter.py +25 -0
  23. rmk-0.1.0/src/rmonkey/tools/human.py +23 -0
  24. rmk-0.1.0/src/rmonkey/tools/terminal_bash.py +31 -0
  25. rmk-0.1.0/src/rmonkey/tools/text_file_editor.py +130 -0
  26. rmk-0.1.0/src/rmonkey/tools/think.py +19 -0
  27. rmk-0.1.0/src/rmonkey/tools/tool_set.py +24 -0
  28. rmk-0.1.0/src/rmonkey/utils/__init__.py +0 -0
  29. rmk-0.1.0/src/rmonkey/utils/os_info.py +10 -0
  30. rmk-0.1.0/src/rmonkey/utils/pretty_console.py +19 -0
  31. rmk-0.1.0/src/rmonkey/utils/schema.py +28 -0
  32. rmk-0.1.0/src/rmonkey/utils/user_rules.py +14 -0
  33. rmk-0.1.0/src/rmonkey/utils/util.py +11 -0
rmk-0.1.0/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
rmk-0.1.0/PKG-INFO ADDED
@@ -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
+ ```
rmk-0.1.0/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Root Monkey (🛠️🐒⚙️)
2
+
3
+ Root Monkey (rmk) is an autonomous AI Agent system.
4
+
5
+ Key Features:
6
+ - High Agent Autonomy: *Less human intervention, more agent autonomy.*
7
+ - Self-Developing: *The system can develop new capabilities on its own.*
8
+ - Simple Design: *Easy to understand and implement.*
9
+
10
+
11
+ ## Design Philosophy
12
+
13
+ - "Simplicity is the ultimate sophistication." - Leonardo da Vinci
14
+
15
+
16
+ - "The evolution of the model will gradually reduce the complexity of the design." - Root Monkey
17
+
18
+
19
+ ## Demo - dev setup
20
+ ```bash
21
+ pip install rmk
22
+ export OPENAI_API_KEY="your-api-key"
23
+ export MODEL="gpt-4.1"
24
+
25
+ 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
26
+ ```
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "rmk"
3
+ version = "0.1.0"
4
+ description = "Root Monkey (rmk) is an autonomous AI Agent system."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "rootmq", email = "mqvvang@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "anthropic>=0.52.0",
12
+ "dotenv>=0.9.9",
13
+ "litellm>=1.72.0",
14
+ "openai>=1.82.0",
15
+ "pydantic>=2.11.5",
16
+ "rich>=14.0.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ rmk = "rmonkey.entrypoints.cli.main:main"
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [tool.hatch.build.targets.sdist]
27
+ include = ["src"]
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/rmonkey"]
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "black>=25.1.0",
35
+ "isort>=6.0.1",
36
+ "pre-commit>=4.2.0",
37
+ "pytest>=8.3.5",
38
+ "pytest-asyncio>=0.26.0",
39
+ "pytest-mock>=3.14.0",
40
+ "ruff>=0.11.11",
41
+ ]
42
+
43
+ [tool.ruff]
44
+ line-length = 119
45
+ fix = true
46
+ target-version = "py310"
47
+
48
+ [tool.ruff.format]
49
+ quote-style = "preserve"
50
+
51
+ [tool.ruff.lint]
52
+ select = [
53
+ # pycodestyle
54
+ "E",
55
+ # Pyflakes
56
+ "F",
57
+ # pyupgrade
58
+ "UP",
59
+ # flake8-bugbear
60
+ "B",
61
+ # flake8-simplify
62
+ "SIM",
63
+ # isort
64
+ "I",
65
+ ]
@@ -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,3 @@
1
+ from .tool_action import ToolAction
2
+
3
+ __all__ = ["ToolAction"]
@@ -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)
@@ -0,0 +1,11 @@
1
+ from ._base import Agent
2
+ from .ask_agent import AskAgent
3
+ from .root_monkey import RootMonkey
4
+ from .swe_agent import SWEAgent
5
+
6
+ __all__ = [
7
+ "Agent",
8
+ "AskAgent",
9
+ "RootMonkey",
10
+ "SWEAgent",
11
+ ]
@@ -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,7 @@
1
+ from rmonkey.agents import Agent
2
+
3
+
4
+ class AskAgent(Agent):
5
+ def __init__(self, *args, **kwargs):
6
+ super().__init__(name="AskAgent", system="You are a powerful AI agent.", **kwargs)
7
+ self.description = "This agent is used to answer questions."
@@ -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()
@@ -0,0 +1,7 @@
1
+ from rmonkey.llm.providers.openai import OpenAIHandler
2
+ from rmonkey.utils.schema import Role
3
+
4
+ __all__ = [
5
+ "Role",
6
+ "OpenAIHandler",
7
+ ]
@@ -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
+ )
@@ -0,0 +1,3 @@
1
+ from rmonkey.memory.memory import Memory
2
+
3
+ __all__ = ["Memory"]
@@ -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")
@@ -0,0 +1,5 @@
1
+ from rmonkey.plan.planning import Planning
2
+
3
+ __all__ = [
4
+ "Planning",
5
+ ]
@@ -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
+ ]
@@ -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)}"
@@ -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}"
@@ -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
@@ -0,0 +1,10 @@
1
+ import platform
2
+
3
+
4
+ def system(xml=True) -> str:
5
+ name = f"system/OS name: {platform.system()}".strip()
6
+ arch = f"arch: {platform.machine()}".strip()
7
+ info = f"{name}\n{arch}"
8
+ if xml:
9
+ return f"<platform>\n{info}\n</platform>"
10
+ return info
@@ -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))
@@ -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>"
@@ -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