mini-code-cli 0.0.2__py3-none-any.whl → 0.0.4__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.
File without changes
@@ -0,0 +1,138 @@
1
+ import os
2
+ import json
3
+
4
+ import prompts
5
+ import tools
6
+ import utils
7
+
8
+ import argparse
9
+ import sys
10
+
11
+ # ANSI escape codes for colors and bold
12
+ BOLD = '\033[1m'
13
+ RED = '\033[91m'
14
+ GREEN = '\033[92m'
15
+ YELLOW = '\033[93m'
16
+ BLUE = '\033[94m'
17
+ MAGENTA = '\033[95m'
18
+ CYAN = '\033[96m'
19
+ RESET = '\033[0m'
20
+
21
+ def save_history(messages):
22
+ # Save messages to history.json
23
+ with open("history.json", "w") as f:
24
+ json.dump(messages, f, indent=2)
25
+
26
+ def parse_args():
27
+ parser = argparse.ArgumentParser(description="Agent loop for interacting with a language model")
28
+ parser.add_argument("--url", type=str, default="http://0.0.0.0:30000", help="API host")
29
+ parser.add_argument("--api-key", type=str, default=None, help="API key for authentication")
30
+ parser.add_argument("--allowed-dir", type=str, default=".", help="Allowed directory for file operations")
31
+ parser.add_argument("--max-tokens", type=int, default=2048, help="Maximum tokens for LLM response")
32
+ parser.add_argument("--temperature", type=float, default=0.0, help="Temperature for LLM")
33
+ parser.add_argument("--model", type=str, default="default", help="Model name")
34
+ args = parser.parse_args()
35
+ return args
36
+
37
+ def main():
38
+ args = parse_args()
39
+
40
+ allowed_dir = os.path.abspath(args.allowed_dir)
41
+ server_url = f"{args.url}"
42
+
43
+ tools_dict = tools.util_get_tools_dict()
44
+ llm_tools_dict = tools.util_get_tools()
45
+
46
+ system_prompt = prompts.system_prompt(tools_dict=tools_dict, allowed_dir=allowed_dir)
47
+
48
+ messages = [{"role": "system", "content": system_prompt}]
49
+
50
+ print(f"""
51
+ ╔═══════════════════════════════════════════════════════════════════╗
52
+ ║ Coding Agent Loop Started\t\t\t\t\t║
53
+ ║ Type 'quit' or 'exit' to exit the loop.\t\t\t\t║
54
+ ║ Allowed dir: {allowed_dir}\t║
55
+ ╚═══════════════════════════════════════════════════════════════════╝
56
+ """)
57
+
58
+ while True:
59
+ llm_response_flag = False
60
+ llm_tool_response_flag = False
61
+ try:
62
+ print("-"*30)
63
+ user_input = input(f"{BOLD}{CYAN}You:{RESET} ")
64
+ if user_input.lower() in ["quit", "exit"]:
65
+ print("\nThank you. Goodbye! Saving history...")
66
+ save_history(messages)
67
+ break
68
+ except KeyboardInterrupt:
69
+ print("\nThank you. Goodbye! Saving history...")
70
+ save_history(messages)
71
+ break
72
+
73
+ messages.append({"role": "user", "content": user_input})
74
+
75
+ response_text, tool_calls = utils.call_openai_server(
76
+ messages,
77
+ max_tokens=args.max_tokens,
78
+ temperature=args.temperature,
79
+ model=args.model,
80
+ server_url=server_url,
81
+ tools=llm_tools_dict
82
+ )
83
+
84
+ if response_text:
85
+ print(f"{BOLD}{GREEN}Assistant:{RESET} {response_text}")
86
+ messages.append({"role": "assistant", "content": response_text})
87
+ else:
88
+ llm_response_flag = True
89
+
90
+ if tool_calls:
91
+ # Tool call parsing and execution
92
+ print(f"{BOLD}{YELLOW}Tool Call:{RESET} LLM is trying to call {len(tool_calls)} tools.")
93
+ if tool_calls:
94
+ for call in tool_calls:
95
+ call_id = call.get("id", "")
96
+ name = call.get("function", {}).get("name", "")
97
+ params_str = call.get("function", {}).get("arguments", "{}")
98
+ try:
99
+ tool_params = json.loads(params_str)
100
+ except json.JSONDecodeError:
101
+ print(f"{BOLD}{RED}Warning:{RESET} Error parsing arguments for tool {name}")
102
+ continue
103
+
104
+ if name in tools_dict:
105
+ # Check allowed directory for file operations
106
+ if name in ["read_file", "write_file", "grep", "glob"]:
107
+ filepath = tool_params.get("filepath", "")
108
+ if name == "write_file":
109
+ filepath = tool_params.get("filepath", "")
110
+ if filepath and not os.path.abspath(filepath).startswith(allowed_dir):
111
+ print(f"{BOLD}{RED}Warning:{RESET} File operation not allowed outside allowed directory: {allowed_dir}")
112
+ continue
113
+
114
+ func = tools_dict[name]["function"]
115
+ result = func(**tool_params)
116
+ # Limiting output to 300 characters for readability
117
+ result_str = str(result)
118
+ if len(result_str) > 300:
119
+ result_str = result_str[:300] + "... (truncated to 300 chars)"
120
+ print(f"{BOLD}{MAGENTA}Tool '{name}' returned:{RESET} {result_str}")
121
+
122
+ messages.append({
123
+ "role": "tool",
124
+ "tool_call_id": call_id,
125
+ "content": str(result)
126
+ })
127
+ else:
128
+ print(f"{BOLD}{RED}Warning:{RESET} Unknown tool: {name}")
129
+ else:
130
+ llm_tool_response_flag = True
131
+
132
+ if llm_response_flag and llm_tool_response_flag:
133
+ print(f"{BOLD}{RED}Warning:{RESET} No response was given by the LLM.")
134
+
135
+ save_history(messages)
136
+
137
+ if __name__ == "__main__":
138
+ main()
@@ -0,0 +1,51 @@
1
+ # System prompt defining available tools and compaction behavior
2
+ def system_prompt(tools_dict=None, allowed_dir=None):
3
+ """
4
+ Takes a list of tool names and returns a system prompt string.
5
+ """
6
+ allowed_dir_prompt = ""
7
+ if allowed_dir:
8
+ allowed_dir_prompt = f"\nThe path you are allowed to operate in is called: {allowed_dir}"
9
+ if tools_dict:
10
+ tool_descriptions = "\n".join([f"{name} : {data['description']}" for name, data in tools_dict.items()])
11
+ base_prompt = "You are a helpful AI coding assistant with access to the following tools:\n"
12
+ end_prompt = "\n\nWhen answering, first use available tools if needed, then provide a compact summary of the results."
13
+ else:
14
+ tool_descriptions = ""
15
+ base_prompt = "You are a helpful AI coding assistant without any tools."
16
+ end_prompt = ""
17
+
18
+ prompt = f"""
19
+ {base_prompt}
20
+ {tool_descriptions}{end_prompt}{allowed_dir_prompt}
21
+ Keep responses concise and focused.
22
+ """
23
+ return prompt.strip()
24
+
25
+ def compaction_prompt(messages):
26
+ """
27
+ Takes a messages list and returns a compact prompt string summarizing the conversation.
28
+ """
29
+ conversation = "\n".join([f"{msg['role']}: {msg['content']}" for msg in messages])
30
+ prompt = f"""
31
+ Please analyze the following conversation and output a concise summary covering:
32
+ 1. Key topics discussed
33
+ 2. Any decisions made
34
+ 3. Important information or facts provided
35
+ 4. Action items or next steps (if any)
36
+
37
+ Keep the summary to 2-3 sentences.
38
+
39
+ Conversation:
40
+ {conversation}
41
+ """
42
+ return prompt.strip()
43
+
44
+ if __name__=="__main__":
45
+ import tools
46
+ tools_dict = tools.util_get_tools_dict()
47
+ print(system_prompt(tools_dict))
48
+
49
+ print("="*30)
50
+
51
+ print(system_prompt())
mini_code_cli/tools.py ADDED
@@ -0,0 +1,124 @@
1
+ import os
2
+ import glob as glob_module
3
+ import subprocess
4
+ import urllib.parse
5
+ import urllib.request
6
+ import json
7
+
8
+ def read_file(filepath):
9
+ """Read the contents of a file and return as a string. Param: filepath"""
10
+ try:
11
+ with open(filepath, 'r', encoding='utf-8') as f:
12
+ return f.read()
13
+ except Exception as e:
14
+ print(f"Error: {e}")
15
+
16
+
17
+ def write_file(filepath, content):
18
+ """Write content to a file. Overwrites if file exists. Param: filepath, content"""
19
+ try:
20
+ with open(filepath, 'w', encoding='utf-8') as f:
21
+ f.write(content)
22
+ except Exception as e:
23
+ print(f"Error: {e}")
24
+ return f"Wrote to file: {filepath}, content: {content}."
25
+
26
+ def grep(pattern, filepath):
27
+ """Search for a pattern in a file using grep and return matching lines. Param: pattern, filepath"""
28
+ try:
29
+ result = subprocess.run(['grep', pattern, filepath], capture_output=True, text=True, check=False)
30
+ return result.stdout
31
+ except Exception as e:
32
+ return f"Error: {e}"
33
+
34
+ def glob(pattern):
35
+ """Return list of file paths matching the given glob pattern. Param: pattern"""
36
+ try:
37
+ return glob_module.glob(pattern)
38
+ except Exception as e:
39
+ return f"Error: {e}"
40
+
41
+ def web_search(query):
42
+ """Perform a simple web search using DuckDuckGo's API and return top results. Param: query"""
43
+ query_encoded = urllib.parse.quote(query)
44
+ url = f"https://api.duckduckgo.com/?q={query_encoded}&format=json&no_html=1"
45
+ try:
46
+ with urllib.request.urlopen(url) as response:
47
+ data = json.loads(response.read().decode())
48
+ # Extract relevant text from the response
49
+ results = []
50
+ if 'AbstractText' in data and data['AbstractText']:
51
+ results.append(data['AbstractText'])
52
+ if 'RelatedTopics' in data:
53
+ for topic in data['RelatedTopics']:
54
+ if 'Text' in topic:
55
+ results.append(topic['Text'])
56
+ return '\n'.join(results) if results else "No results found."
57
+ except Exception as e:
58
+ return f"Error: {e}"
59
+
60
+ def read_website(url):
61
+ """Fetch and return the text content of a given URL (simplified, returns raw HTML). Param: url"""
62
+ try:
63
+ with urllib.request.urlopen(url) as response:
64
+ return response.read().decode('utf-8')
65
+ except Exception as e:
66
+ return f"Error: {e}"
67
+
68
+ def util_get_tools(tool_names=None):
69
+ """Return a JSON string describing all tool functions in this file for LLM usage."""
70
+ import inspect
71
+ tools = []
72
+ if tool_names:
73
+ func_names = [name for name, obj in globals().items() if callable(obj) and obj.__module__ == __name__ and not name.startswith('util_') and not name.startswith("_") and name in tool_names]
74
+ else:
75
+ func_names = [name for name, obj in globals().items() if callable(obj) and obj.__module__ == __name__ and not name.startswith('util_') and not name.startswith("_")]
76
+
77
+ for name in func_names:
78
+ func = globals()[name]
79
+ sig = inspect.signature(func)
80
+ parameters = {}
81
+ for param_name, param in sig.parameters.items():
82
+ param_type = str(param.annotation) if param.annotation != inspect.Parameter.empty else "string"
83
+ # Simplify common types
84
+ if 'int' in param_type.lower() or 'integer' in param_type.lower():
85
+ param_type = "integer"
86
+ elif 'float' in param_type.lower() or 'number' in param_type.lower():
87
+ param_type = "number"
88
+ elif 'bool' in param_type.lower():
89
+ param_type = "boolean"
90
+ elif 'list' in param_type.lower() or 'array' in param_type.lower():
91
+ param_type = "array"
92
+ else:
93
+ param_type = "string"
94
+ parameters[param_name] = {"type": param_type}
95
+ tool = {
96
+ "type": "function",
97
+ "function": {
98
+ "name": name,
99
+ "description": func.__doc__ if func.__doc__ else f"Function {name}",
100
+ "parameters": {
101
+ "type": "object",
102
+ "properties": parameters,
103
+ "required": [p for p in sig.parameters if sig.parameters[p].default == inspect.Parameter.empty]
104
+ }
105
+ }
106
+ }
107
+ tools.append(tool)
108
+ return tools
109
+
110
+
111
+ def util_get_tools_dict():
112
+ """Return a dictionary mapping tool names to their function objects."""
113
+ result = {}
114
+ func_names = [name for name, obj in globals().items() if callable(obj) and obj.__module__ == __name__ and not name.startswith('util_') and not name.startswith("_")]
115
+ for name in func_names:
116
+ func = globals()[name]
117
+ desc = func.__doc__ if func.__doc__ else f"Function {name}"
118
+ result[name] = {"function": func, "description": desc}
119
+ return result
120
+
121
+
122
+ if __name__=="__main__":
123
+ # print(util_get_tools())
124
+ print(json.dumps(tools_dict, indent=4))
mini_code_cli/utils.py ADDED
@@ -0,0 +1,111 @@
1
+ import requests
2
+ import json
3
+ import argparse
4
+
5
+
6
+ def call_openai_server(messages, max_tokens=2048, temperature=0.0, top_p=0.95, model="default", server_url=None, api_key=None, tools=None,):
7
+ """
8
+ Querying an OpenAI-compatible server (local or remote) using the requests library.
9
+ Supports authentication via API key if provided.
10
+ """
11
+ url = f"{server_url}/v1/chat/completions"
12
+
13
+ headers = {
14
+ "Content-Type": "application/json"
15
+ }
16
+ if api_key:
17
+ headers["Authorization"] = f"Bearer {api_key}"
18
+
19
+ payload = {
20
+ "model": model,
21
+ "messages": messages,
22
+ "max_tokens": max_tokens,
23
+ "temperature": temperature,
24
+ "top_p": top_p
25
+ }
26
+
27
+ if tools:
28
+ payload["tools"] = tools
29
+ payload["tool_choice"] = "auto"
30
+
31
+ try:
32
+ response = requests.post(
33
+ url,
34
+ headers=headers,
35
+ json=payload,
36
+ timeout=60
37
+ )
38
+ response.raise_for_status()
39
+
40
+ result = response.json().get("choices", [{}])[0].get("message", {})
41
+ content = result.get("content", "")
42
+ tool_calls = result.get("tool_calls", [])
43
+
44
+ return content, tool_calls
45
+
46
+ except requests.exceptions.RequestException as e:
47
+ print(f"Error calling OpenAI Server...: {e}")
48
+ return None, None
49
+ except (json.JSONDecodeError, KeyError, IndexError) as e:
50
+ print(f"Error processing response: {e}")
51
+ return None, None
52
+
53
+ # Alternative: use the newer SGLang API format (v0.3+)
54
+ def call_openai_server_prompt(prompt, max_tokens=1024, temperature=0.0, top_p=0.95, model="default", server_url=None, api_key=None, tools=None):
55
+ """
56
+ OpenAI compatible call, pre-creating the messages object.
57
+ """
58
+ messages = [
59
+ {"role": "user", "content": prompt}
60
+ ]
61
+
62
+ return call_openai_server(messages, max_tokens, temperature, top_p, model, server_url=server_url, api_key=api_key, tools=tools)
63
+
64
+ def parse_args():
65
+ parser = argparse.ArgumentParser(description="Agent loop for interacting with a language model")
66
+ parser.add_argument("--host", type=str, default="localhost", help="API host")
67
+ parser.add_argument("--port", type=int, default=30000, help="API port")
68
+ parser.add_argument("--prompt", type=str, default="What is the purpose of life?", help="prompt")
69
+ parser.add_argument("--api-key", type=str, default=None, help="API key for authentication (optional)")
70
+
71
+ return parser.parse_args()
72
+
73
+
74
+ if __name__ == '__main__':
75
+ import tools
76
+ tools_dict = tools.util_get_tools()
77
+
78
+ args = parse_args()
79
+ # Configuration for local SGLang deployment
80
+ server_url=f"http://{args.host}:{args.port}"
81
+
82
+ print("="*60)
83
+ print("Entering LLM call:")
84
+ print("-"*30)
85
+ prompt = args.prompt
86
+
87
+ print(f"Prompt:\n{prompt}")
88
+ print("---")
89
+
90
+ response_text, tool_calls = call_openai_server_prompt(prompt, server_url=server_url, api_key=args.api_key, tools=tools_dict)
91
+ print(f"Response:\n{response_text}")
92
+ print("---")
93
+ print(f"Tools:\n{tool_calls}")
94
+ print("---")
95
+
96
+
97
+
98
+ # #################################
99
+ # Test call to DeepSeek endpoint
100
+ # #################################
101
+
102
+ # deepseek_server_url = "https://api.deepseek.com"
103
+ # deepseek_model = "deepseek-v4-flash"
104
+ # print("="*60)
105
+ # print("Entering DeepSeek call:")
106
+ # print("-"*30)
107
+ # response_text_ds, tools_ds = call_openai_server_prompt(prompt, server_url=deepseek_server_url, api_key=args.api_key, model=deepseek_model, tools=tools_dict)
108
+ # print(f"DeepSeek Response:\n{response_text_ds}")
109
+ # print("---")
110
+ # print(f"Tools:\n{tools_ds}")
111
+ # print("---")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-code-cli
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Minimal Coding CLI
5
5
  Author-email: Nikolai Rozanov <nikolai.rozanov@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -30,8 +30,8 @@ pip3 install mini-code-cli
30
30
 
31
31
  2. Run agent: (get LLM running via SGLang or Vllm)
32
32
  ```bash
33
- mini_code_cli #run the agent loop
34
- mini_code_cli --url "0.0.0.0" --allowed-dir ./experiments/ --api-key "sk-dummy" #localhost and 30000 is default, allowed_dir ./ is default
33
+ mini-code #run the agent loop
34
+ mini-code --url "https://api.deepseek.com" --model "deepseek-v4-flash" --api-key "sk-dummy" --allowed-dir ./experiments/
35
35
  ```
36
36
 
37
37
 
@@ -0,0 +1,11 @@
1
+ mini_code_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mini_code_cli/agent_loop.py,sha256=uuAeiqbiy3PE3r8Vs_dDkcbDKVmcxkefhUdXbWifPzE,5683
3
+ mini_code_cli/prompts.py,sha256=bMWPLsD0SCQK4oDru4MYSSUf2KQq8EO4ZD55PpdlR_I,1730
4
+ mini_code_cli/tools.py,sha256=i46l8IvsrXJgqt7r8rKAMTGKNGAGffxKTFlwmAuWoD0,4926
5
+ mini_code_cli/utils.py,sha256=KmI-hfVsuopVRr5lLXiW2T24RZawtGKHOy9n8Ak1UC4,3636
6
+ mini_code_cli-0.0.4.dist-info/licenses/LICENSE,sha256=WCkMINUFi--UtEg_alEE2FBKPtNKkzi7dGzaxzchifc,1088
7
+ mini_code_cli-0.0.4.dist-info/METADATA,sha256=thK7lq5Z6h2h934eeFcBpzWgV-LH9Q4p4YdO411fMpg,1943
8
+ mini_code_cli-0.0.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
9
+ mini_code_cli-0.0.4.dist-info/entry_points.txt,sha256=MwDp5UbXpI7wodSmOSYL8_nKutnBiyp96ZDDtaX7x14,60
10
+ mini_code_cli-0.0.4.dist-info/top_level.txt,sha256=3Hb6PpNkJYXvzDKXuuFJRvzCdsTRh0TuRWz-ScJ6qV0,14
11
+ mini_code_cli-0.0.4.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mini-code = mini_code_cli.agent_loop:main
@@ -0,0 +1 @@
1
+ mini_code_cli
@@ -1,6 +0,0 @@
1
- mini_code_cli-0.0.2.dist-info/licenses/LICENSE,sha256=WCkMINUFi--UtEg_alEE2FBKPtNKkzi7dGzaxzchifc,1088
2
- mini_code_cli-0.0.2.dist-info/METADATA,sha256=SccCYdxuHXjx1ASjSn3fZnvy3fOxQr3E6S0O1e9fp0s,1965
3
- mini_code_cli-0.0.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
4
- mini_code_cli-0.0.2.dist-info/entry_points.txt,sha256=ZPwaUZnKtV8ZwNPZPoocL-y2x6gkb3ZN5tYDlsNRIng,53
5
- mini_code_cli-0.0.2.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
- mini_code_cli-0.0.2.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- mini-code-cli = mini_code_cli:main
@@ -1 +0,0 @@
1
-