avins 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.
- avins-0.1.0/LICENSE +21 -0
- avins-0.1.0/PKG-INFO +42 -0
- avins-0.1.0/README.md +25 -0
- avins-0.1.0/agents/mail.py +22 -0
- avins-0.1.0/agents/tool_maker.py +195 -0
- avins-0.1.0/avins.egg-info/PKG-INFO +42 -0
- avins-0.1.0/avins.egg-info/SOURCES.txt +20 -0
- avins-0.1.0/avins.egg-info/dependency_links.txt +1 -0
- avins-0.1.0/avins.egg-info/entry_points.txt +2 -0
- avins-0.1.0/avins.egg-info/requires.txt +4 -0
- avins-0.1.0/avins.egg-info/top_level.txt +5 -0
- avins-0.1.0/config/api_setup.py +62 -0
- avins-0.1.0/config/asyncModel.py +13 -0
- avins-0.1.0/config/model.py +12 -0
- avins-0.1.0/core/main.py +218 -0
- avins-0.1.0/model/tool.py +9 -0
- avins-0.1.0/model/user.py +11 -0
- avins-0.1.0/pyproject.toml +30 -0
- avins-0.1.0/setup.cfg +4 -0
- avins-0.1.0/tools/ast_builder.py +0 -0
- avins-0.1.0/tools/tools.py +293 -0
- avins-0.1.0/tools/tools_defination.py +116 -0
avins-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Avinash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
avins-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: avins
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An advanced, proactive AI desktop assistant with terminal and GUI automation capabilities.
|
|
5
|
+
Author: Avinash
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/avin-1/avins
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/avin-1/avins/issues
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: groq
|
|
13
|
+
Requires-Dist: python-dotenv
|
|
14
|
+
Requires-Dist: pydantic
|
|
15
|
+
Requires-Dist: pyautogui
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Avins
|
|
19
|
+
|
|
20
|
+
An advanced, proactive AI desktop assistant equipped with terminal and GUI automation capabilities.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
- **Cross-Platform**: Supports Windows, macOS, and Linux (including Wayland and X11).
|
|
24
|
+
- **GUI Automation**: Abstracted GUI controls for typing, clicking, scrolling, and hotkeys.
|
|
25
|
+
- **Terminal Execution**: Safely execute shell commands directly on your system (with user confirmation).
|
|
26
|
+
- **Dynamic Tool Creation**: The agent can write its own Python tools and add them dynamically.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install avins
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Run the assistant via the command line:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
avins
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This will start an interactive ReAct (Reason + Act) loop where you can instruct the assistant to perform various operations on your desktop.
|
avins-0.1.0/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Avins
|
|
2
|
+
|
|
3
|
+
An advanced, proactive AI desktop assistant equipped with terminal and GUI automation capabilities.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Cross-Platform**: Supports Windows, macOS, and Linux (including Wayland and X11).
|
|
7
|
+
- **GUI Automation**: Abstracted GUI controls for typing, clicking, scrolling, and hotkeys.
|
|
8
|
+
- **Terminal Execution**: Safely execute shell commands directly on your system (with user confirmation).
|
|
9
|
+
- **Dynamic Tool Creation**: The agent can write its own Python tools and add them dynamically.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install avins
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Run the assistant via the command line:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
avins
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This will start an interactive ReAct (Reason + Act) loop where you can instruct the assistant to perform various operations on your desktop.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from groq import Groq
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
load_dotenv()
|
|
5
|
+
|
|
6
|
+
client=Groq()
|
|
7
|
+
|
|
8
|
+
chat_completion=client.chat.completions.create(
|
|
9
|
+
messages=[
|
|
10
|
+
{
|
|
11
|
+
"role":"system",
|
|
12
|
+
"content": "You are a helpful assistant and at the end of each line append Avinash"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"role": "user",
|
|
16
|
+
"content": "what is the capital of india"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
model="openai/gpt-oss-120b"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
print(chat_completion.choices[0].message.content)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool-creation agent.
|
|
3
|
+
|
|
4
|
+
Given a task, asks an LLM whether a new tool is needed. If so, it
|
|
5
|
+
creates the tool (installing dependencies via use_terminal first),
|
|
6
|
+
persists it into tools.py / tools_defination.py, and makes it callable
|
|
7
|
+
immediately. If not, it just returns the model's answer.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import importlib
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from pprint import pformat
|
|
15
|
+
import multiprocessing
|
|
16
|
+
from config.asyncModel import async_client
|
|
17
|
+
from tools.tools_defination import tool
|
|
18
|
+
from tools.tools import use_terminal
|
|
19
|
+
import tools.tools as tools_module
|
|
20
|
+
from logger.logs import logging
|
|
21
|
+
import threading
|
|
22
|
+
|
|
23
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
24
|
+
TOOLS_FILE = PROJECT_ROOT / "tools" / "tools.py"
|
|
25
|
+
TOOLS_DEFINITION_FILE = PROJECT_ROOT / "tools" / "tools_defination.py"
|
|
26
|
+
|
|
27
|
+
MAX_TURNS = 6
|
|
28
|
+
|
|
29
|
+
SYSTEM_PROMPT = """Your job is to make a tool for a master agent.
|
|
30
|
+
|
|
31
|
+
The tool is a Python function, if needed, that uses lightweight and
|
|
32
|
+
safe libraries. Any library you use must first be installed with the
|
|
33
|
+
use_terminal tool, and the code you return must contain the imports
|
|
34
|
+
for those libraries.
|
|
35
|
+
|
|
36
|
+
No mistakes. This is a high stake task.
|
|
37
|
+
|
|
38
|
+
Always respond with ONLY a JSON object, nothing else (no prose, no
|
|
39
|
+
Markdown fences), in exactly this shape:
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
"tool": null,
|
|
43
|
+
"defination": null,
|
|
44
|
+
"result": null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
- If the task requires a NEW tool: set "tool" to the full Python
|
|
48
|
+
source of the function, and "defination" to its OpenAI-style tool
|
|
49
|
+
definition (with "type", "function.name", "function.description",
|
|
50
|
+
"function.parameters"). Leave "result" null.
|
|
51
|
+
- If the task does NOT require a new tool: leave "tool" and
|
|
52
|
+
"defination" null, and put your final answer in "result".
|
|
53
|
+
|
|
54
|
+
You may also make a tool call yourself (e.g. use_terminal to install
|
|
55
|
+
a dependency) instead of returning JSON, when you need to act before
|
|
56
|
+
you can answer.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
tool_call_history = []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_history_note() -> str:
|
|
63
|
+
if not tool_call_history:
|
|
64
|
+
return "No tools have been called yet."
|
|
65
|
+
return (
|
|
66
|
+
"Here is the tool call history so far:\n"
|
|
67
|
+
f"{tool_call_history}\n\n"
|
|
68
|
+
"If the requested operation has already been executed "
|
|
69
|
+
"successfully, do not run it again."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def chat_completion(msg: str):
|
|
74
|
+
return await async_client.chat.completions.create(
|
|
75
|
+
messages=[
|
|
76
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
77
|
+
{"role": "user", "content": msg},
|
|
78
|
+
{"role": "system", "content": build_history_note()},
|
|
79
|
+
],
|
|
80
|
+
tools=tool,
|
|
81
|
+
model="openai/gpt-oss-120b",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def save_tool_definition(tool_list):
|
|
86
|
+
"""Persist tool definitions into tools_defination.py."""
|
|
87
|
+
content = f"tool = {pformat(tool_list, indent=4)}\n\n__all__ = [\"tool\"]\n"
|
|
88
|
+
TOOLS_DEFINITION_FILE.write_text(content)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def execute_tool(name: str, arguments: str) -> str:
|
|
92
|
+
"""Execute a tool by name with JSON arguments."""
|
|
93
|
+
try:
|
|
94
|
+
args = json.loads(arguments) if arguments else {}
|
|
95
|
+
except json.JSONDecodeError as e:
|
|
96
|
+
return f"Error: could not parse arguments for '{name}': {e}"
|
|
97
|
+
|
|
98
|
+
if name == "use_terminal":
|
|
99
|
+
command = args.get("text", "")
|
|
100
|
+
res = use_terminal(command)
|
|
101
|
+
if res.returncode == 0:
|
|
102
|
+
output = res.stdout or "(Command executed successfully with no output)"
|
|
103
|
+
return f"Stdout:\n{output}"
|
|
104
|
+
return f"Stderr:\n{res.stderr}"
|
|
105
|
+
|
|
106
|
+
importlib.reload(tools_module)
|
|
107
|
+
func = getattr(tools_module, name, None)
|
|
108
|
+
if func is None:
|
|
109
|
+
return f"Error: Tool '{name}' is not supported."
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
return str(func(**args))
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return f"Error while executing '{name}': {e}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def create_tool(task: str) -> str:
|
|
118
|
+
"""
|
|
119
|
+
One turn with the model. Returns "Tool Called", "Success", or "Error".
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
chat = asyncio.run(chat_completion(task))
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print("API call failed:", e)
|
|
125
|
+
return "Error"
|
|
126
|
+
|
|
127
|
+
message = chat.choices[0].message
|
|
128
|
+
content = message.content
|
|
129
|
+
|
|
130
|
+
if message.tool_calls:
|
|
131
|
+
for call in message.tool_calls:
|
|
132
|
+
output = execute_tool(call.function.name, call.function.arguments)
|
|
133
|
+
tool_call_history.append({call.function.name: output})
|
|
134
|
+
return "Tool Called"
|
|
135
|
+
|
|
136
|
+
if content is None:
|
|
137
|
+
print("No response received from model.")
|
|
138
|
+
return "Error"
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
parsed = json.loads(content)
|
|
142
|
+
except json.JSONDecodeError:
|
|
143
|
+
print("Result:", content)
|
|
144
|
+
return "Success"
|
|
145
|
+
|
|
146
|
+
tool_code = parsed.get("tool")
|
|
147
|
+
definition = parsed.get("defination")
|
|
148
|
+
result_text = parsed.get("result")
|
|
149
|
+
|
|
150
|
+
if tool_code and definition:
|
|
151
|
+
with open(TOOLS_FILE, "a") as f:
|
|
152
|
+
f.write("\n\n")
|
|
153
|
+
f.write(tool_code)
|
|
154
|
+
|
|
155
|
+
tool.append(definition)
|
|
156
|
+
save_tool_definition(tool)
|
|
157
|
+
|
|
158
|
+
print("Tool created successfully!")
|
|
159
|
+
return "Success"
|
|
160
|
+
|
|
161
|
+
if result_text:
|
|
162
|
+
print("Result:", result_text)
|
|
163
|
+
return "Success"
|
|
164
|
+
|
|
165
|
+
print("Model returned neither a tool nor a result.")
|
|
166
|
+
return "Error"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def wrapper(task: str) -> None:
|
|
170
|
+
res = None
|
|
171
|
+
for _ in range(MAX_TURNS):
|
|
172
|
+
res = create_tool(task)
|
|
173
|
+
if res != "Tool Called":
|
|
174
|
+
print("Success")
|
|
175
|
+
break
|
|
176
|
+
else:
|
|
177
|
+
print(f"Stopped after {MAX_TURNS} turns without a final answer.")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
if res == "Success":
|
|
181
|
+
print("Task completed successfully.")
|
|
182
|
+
else:
|
|
183
|
+
print("Task execution failed.")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def create(user_task: str):
|
|
187
|
+
p = threading.Thread(target=wrapper, args=(user_task,))
|
|
188
|
+
p.start()
|
|
189
|
+
p.join(timeout=120)
|
|
190
|
+
if p.is_alive():
|
|
191
|
+
p.terminate()
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
user_task = input("Enter a task: ")
|
|
195
|
+
create(user_task)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: avins
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An advanced, proactive AI desktop assistant with terminal and GUI automation capabilities.
|
|
5
|
+
Author: Avinash
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/avin-1/avins
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/avin-1/avins/issues
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: groq
|
|
13
|
+
Requires-Dist: python-dotenv
|
|
14
|
+
Requires-Dist: pydantic
|
|
15
|
+
Requires-Dist: pyautogui
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Avins
|
|
19
|
+
|
|
20
|
+
An advanced, proactive AI desktop assistant equipped with terminal and GUI automation capabilities.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
- **Cross-Platform**: Supports Windows, macOS, and Linux (including Wayland and X11).
|
|
24
|
+
- **GUI Automation**: Abstracted GUI controls for typing, clicking, scrolling, and hotkeys.
|
|
25
|
+
- **Terminal Execution**: Safely execute shell commands directly on your system (with user confirmation).
|
|
26
|
+
- **Dynamic Tool Creation**: The agent can write its own Python tools and add them dynamically.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install avins
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Run the assistant via the command line:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
avins
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This will start an interactive ReAct (Reason + Act) loop where you can instruct the assistant to perform various operations on your desktop.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
agents/mail.py
|
|
5
|
+
agents/tool_maker.py
|
|
6
|
+
avins.egg-info/PKG-INFO
|
|
7
|
+
avins.egg-info/SOURCES.txt
|
|
8
|
+
avins.egg-info/dependency_links.txt
|
|
9
|
+
avins.egg-info/entry_points.txt
|
|
10
|
+
avins.egg-info/requires.txt
|
|
11
|
+
avins.egg-info/top_level.txt
|
|
12
|
+
config/api_setup.py
|
|
13
|
+
config/asyncModel.py
|
|
14
|
+
config/model.py
|
|
15
|
+
core/main.py
|
|
16
|
+
model/tool.py
|
|
17
|
+
model/user.py
|
|
18
|
+
tools/ast_builder.py
|
|
19
|
+
tools/tools.py
|
|
20
|
+
tools/tools_defination.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Key Setup — Hardcoded safety net for GROQ_API_KEY.
|
|
3
|
+
|
|
4
|
+
This module is imported at the very top of every client module
|
|
5
|
+
(config/asyncModel.py, config/model.py). It guarantees that by the
|
|
6
|
+
time a Groq client is instantiated, GROQ_API_KEY is present both in
|
|
7
|
+
os.environ and persisted in the project's .env file.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from dotenv import load_dotenv, set_key
|
|
13
|
+
|
|
14
|
+
# Always resolve .env relative to this file's project root
|
|
15
|
+
ENV_FILE = Path(__file__).resolve().parent.parent / ".env"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ensure_api_key() -> str:
|
|
19
|
+
"""
|
|
20
|
+
Ensure GROQ_API_KEY exists and is non-empty.
|
|
21
|
+
|
|
22
|
+
Priority order:
|
|
23
|
+
1. Already set in os.environ (e.g. system environment)
|
|
24
|
+
2. Found in .env file
|
|
25
|
+
3. Prompted from the user → saved to .env for future runs
|
|
26
|
+
|
|
27
|
+
Returns the validated API key string.
|
|
28
|
+
"""
|
|
29
|
+
# Load .env into os.environ (safe even if file doesn't exist)
|
|
30
|
+
load_dotenv(ENV_FILE, override=False)
|
|
31
|
+
|
|
32
|
+
key = os.environ.get("GROQ_API_KEY", "").strip()
|
|
33
|
+
|
|
34
|
+
if key:
|
|
35
|
+
return key # Already available — nothing to do
|
|
36
|
+
|
|
37
|
+
# ── Key is missing: inform and prompt ─────────────────────────────
|
|
38
|
+
print()
|
|
39
|
+
print("=" * 58)
|
|
40
|
+
print(" ⚠ GROQ API Key not found.")
|
|
41
|
+
print(" Get yours free at: https://console.groq.com/keys")
|
|
42
|
+
print("=" * 58)
|
|
43
|
+
|
|
44
|
+
while not key:
|
|
45
|
+
key = input(" Enter your GROQ_API_KEY: ").strip()
|
|
46
|
+
if not key:
|
|
47
|
+
print(" Key cannot be empty. Please try again.")
|
|
48
|
+
|
|
49
|
+
# ── Persist to .env ────────────────────────────────────────────────
|
|
50
|
+
ENV_FILE.touch(exist_ok=True) # create .env if it doesn't exist
|
|
51
|
+
set_key(str(ENV_FILE), "GROQ_API_KEY", key)
|
|
52
|
+
os.environ["GROQ_API_KEY"] = key # make available in current process
|
|
53
|
+
|
|
54
|
+
print()
|
|
55
|
+
print(" ✔ API key saved to .env — you won't be asked again.")
|
|
56
|
+
print("=" * 58)
|
|
57
|
+
print()
|
|
58
|
+
|
|
59
|
+
return key
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
__all__ = ["ensure_api_key"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from groq import AsyncGroq
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
from config.api_setup import ensure_api_key
|
|
4
|
+
|
|
5
|
+
# Guarantee key exists (prompts user and saves to .env if missing)
|
|
6
|
+
ensure_api_key()
|
|
7
|
+
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
async_client = AsyncGroq()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = ["async_client"]
|
avins-0.1.0/core/main.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import platform
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from config.asyncModel import async_client
|
|
7
|
+
from model.tool import Tool
|
|
8
|
+
from model.user import User
|
|
9
|
+
from tools.tools_defination import tool as tools_definition
|
|
10
|
+
from tools.tools import use_terminal, gui_control
|
|
11
|
+
|
|
12
|
+
user = User()
|
|
13
|
+
tool_log_history: dict[str, list[str]] = {}
|
|
14
|
+
|
|
15
|
+
_PLATFORM = platform.system()
|
|
16
|
+
|
|
17
|
+
_OS_GUIDELINES: dict[str, str] = {
|
|
18
|
+
"Windows": """\
|
|
19
|
+
### WINDOWS DESKTOP AUTOMATION GUIDELINES:
|
|
20
|
+
- Use PowerShell or cmd.exe commands for terminal actions.
|
|
21
|
+
e.g., `Start-Process chrome "https://www.google.com/search?q=<query>"`
|
|
22
|
+
or `start chrome "https://..."` (cmd style)
|
|
23
|
+
- For file paths always use Windows-style separators: C:\\Users\\...
|
|
24
|
+
- To get screen dimensions: `powershell "[System.Windows.Forms.Screen]::PrimaryScreen.Bounds"`
|
|
25
|
+
or simply call gui_control with action='move' to a known position.
|
|
26
|
+
- For GUI control: pyautogui is used automatically — no extra tools needed.
|
|
27
|
+
""",
|
|
28
|
+
"Darwin": """\
|
|
29
|
+
### macOS DESKTOP AUTOMATION GUIDELINES:
|
|
30
|
+
- Use bash/zsh commands for terminal actions.
|
|
31
|
+
e.g., `open -a "Google Chrome" "https://www.google.com/search?q=<query>"`
|
|
32
|
+
- To get screen dimensions: `system_profiler SPDisplaysDataType | grep Resolution`
|
|
33
|
+
- For GUI control: pyautogui is used automatically.
|
|
34
|
+
""",
|
|
35
|
+
"Linux": """\
|
|
36
|
+
### LINUX DESKTOP AUTOMATION GUIDELINES:
|
|
37
|
+
- Use bash commands for terminal actions.
|
|
38
|
+
e.g., `google-chrome "https://www.google.com/search?q=<query>"`
|
|
39
|
+
or `firefox "https://..."`
|
|
40
|
+
- To get screen geometry: `xdpyinfo | grep dimensions` or `xdotool getdisplaygeometry`
|
|
41
|
+
Once you have dimensions (e.g. 1920x1080), compute center (960, 540) and
|
|
42
|
+
call `gui_control(action='move', x=960, y=540)` immediately.
|
|
43
|
+
- On Wayland: ydotool is used automatically; on X11 pyautogui is used.
|
|
44
|
+
""",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_OS_SECTION = _OS_GUIDELINES.get(_PLATFORM, _OS_GUIDELINES["Linux"])
|
|
48
|
+
|
|
49
|
+
SYSTEM_PROMPT = f"""\
|
|
50
|
+
You are an advanced, proactive AI desktop assistant equipped with terminal and GUI automation capabilities.
|
|
51
|
+
You are running on: {_PLATFORM}
|
|
52
|
+
|
|
53
|
+
### OPERATIONAL FRAMEWORK: CHAIN OF THOUGHT & REACT
|
|
54
|
+
For every user instruction, you MUST reason step-by-step before acting:
|
|
55
|
+
1. **THINK (Reasoning & Plan)**: Analyze the user's intent. Break down the task into concrete, atomic steps.
|
|
56
|
+
2. **ACT (Tool Execution)**: Call the appropriate tool(s) for the next immediate step.
|
|
57
|
+
3. **OBSERVE & REFLECT**: Review the tool execution output. Did the step succeed? What information was obtained?
|
|
58
|
+
4. **REPEAT or FINISH**: Continue executing the remaining steps until the user's request is 100% fulfilled. Only provide your final message once all actions are physically completed.
|
|
59
|
+
|
|
60
|
+
{_OS_SECTION}
|
|
61
|
+
### GENERAL GUIDELINES:
|
|
62
|
+
- **Mouse & Screen Interaction**:
|
|
63
|
+
- Once screen dimensions are obtained, immediately compute center coordinates and move there.
|
|
64
|
+
- **Do NOT hallucinate completion**:
|
|
65
|
+
- Never state an action has been completed unless the corresponding tool call has been executed and confirmed.
|
|
66
|
+
- **Output Format**:
|
|
67
|
+
- Keep final responses direct, concise, and informative in plain text (no unnecessary markdown).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def execute_tool(name: str, arguments: str) -> str:
|
|
72
|
+
"""Safely executes a tool by name with JSON arguments and returns string result."""
|
|
73
|
+
try:
|
|
74
|
+
args: Dict[str, Any] = json.loads(arguments) if arguments else {}
|
|
75
|
+
except json.JSONDecodeError as e:
|
|
76
|
+
return f"Error: Failed to parse tool arguments for '{name}': {e}"
|
|
77
|
+
|
|
78
|
+
if name == "use_terminal":
|
|
79
|
+
command = args.get("text", "")
|
|
80
|
+
if not command:
|
|
81
|
+
return "Error: No command provided to use_terminal."
|
|
82
|
+
|
|
83
|
+
res = use_terminal(command)
|
|
84
|
+
terminal_tool = Tool(name="use_terminal")
|
|
85
|
+
|
|
86
|
+
if res.returncode == 0:
|
|
87
|
+
output = res.stdout.strip() if res.stdout else "(Command completed with no stdout output)"
|
|
88
|
+
terminal_tool.message.append(f"Command '{command}' succeeded: {output}")
|
|
89
|
+
result_str = f"Stdout:\n{output}"
|
|
90
|
+
else:
|
|
91
|
+
err = (res.stderr or res.stdout or "Command failed with non-zero exit code").strip()
|
|
92
|
+
terminal_tool.message.append(f"Command '{command}' failed: {err} (Exit code {res.returncode})")
|
|
93
|
+
result_str = f"Exit Code {res.returncode}\nStderr/Output:\n{err}"
|
|
94
|
+
|
|
95
|
+
tool_log_history.setdefault(name, []).extend(terminal_tool.message)
|
|
96
|
+
return result_str
|
|
97
|
+
|
|
98
|
+
if name == "gui_control":
|
|
99
|
+
gui_tool = Tool(name="gui_control")
|
|
100
|
+
try:
|
|
101
|
+
result_str = gui_control(
|
|
102
|
+
action=args.get("action"),
|
|
103
|
+
x=args.get("x"),
|
|
104
|
+
y=args.get("y"),
|
|
105
|
+
duration=args.get("duration", 0.0),
|
|
106
|
+
clicks=args.get("clicks", 1),
|
|
107
|
+
interval=args.get("interval", 0.0),
|
|
108
|
+
button=args.get("button", "left"),
|
|
109
|
+
text=args.get("text"),
|
|
110
|
+
keys=args.get("keys"),
|
|
111
|
+
scroll=args.get("scroll"),
|
|
112
|
+
)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
result_str = f"Error executing gui_control: {e}"
|
|
115
|
+
|
|
116
|
+
gui_tool.message.append(f"gui_control({args}) -> {result_str}")
|
|
117
|
+
tool_log_history.setdefault(name, []).extend(gui_tool.message)
|
|
118
|
+
return result_str
|
|
119
|
+
|
|
120
|
+
return f"Error: Tool '{name}' is not supported."
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def loop(message: str, max_iterations: int = 10):
|
|
124
|
+
"""Executes the agentic Chain-of-Thought loop with persistent multi-turn memory."""
|
|
125
|
+
# Build context: system prompt + past conversation turns + current user message
|
|
126
|
+
messages: List[Dict[str, Any]] = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
127
|
+
messages.extend(user.last_messages)
|
|
128
|
+
messages.append({"role": "user", "content": message})
|
|
129
|
+
|
|
130
|
+
for iteration in range(1, max_iterations + 1):
|
|
131
|
+
try:
|
|
132
|
+
chat_completion = await async_client.chat.completions.create(
|
|
133
|
+
messages=messages,
|
|
134
|
+
tools=tools_definition,
|
|
135
|
+
model="openai/gpt-oss-120b",
|
|
136
|
+
)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"\n[Model Error]: {e}")
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
msg = chat_completion.choices[0].message
|
|
142
|
+
content = msg.content or ""
|
|
143
|
+
|
|
144
|
+
if content.strip():
|
|
145
|
+
print(f"\n[Thought / Plan]:\n{content.strip()}\n")
|
|
146
|
+
|
|
147
|
+
# Handle tool execution
|
|
148
|
+
if msg.tool_calls:
|
|
149
|
+
assistant_msg: Dict[str, Any] = {
|
|
150
|
+
"role": "assistant",
|
|
151
|
+
"content": msg.content,
|
|
152
|
+
"tool_calls": [
|
|
153
|
+
{
|
|
154
|
+
"id": t.id,
|
|
155
|
+
"type": "function",
|
|
156
|
+
"function": {"name": t.function.name, "arguments": t.function.arguments},
|
|
157
|
+
}
|
|
158
|
+
for t in msg.tool_calls
|
|
159
|
+
],
|
|
160
|
+
}
|
|
161
|
+
messages.append(assistant_msg)
|
|
162
|
+
|
|
163
|
+
for t in msg.tool_calls:
|
|
164
|
+
function_name = t.function.name
|
|
165
|
+
args_str = t.function.arguments
|
|
166
|
+
print(f"[Action]: Calling tool '{function_name}' with args: {args_str}")
|
|
167
|
+
|
|
168
|
+
tool_output = execute_tool(function_name, args_str)
|
|
169
|
+
print(f"[Observation]:\n{tool_output}\n")
|
|
170
|
+
|
|
171
|
+
messages.append({
|
|
172
|
+
"role": "tool",
|
|
173
|
+
"tool_call_id": t.id,
|
|
174
|
+
"name": function_name,
|
|
175
|
+
"content": tool_output,
|
|
176
|
+
})
|
|
177
|
+
else:
|
|
178
|
+
# final completion reached without further tool calls
|
|
179
|
+
print(f"[Assistant]:\n{content.strip()}\n")
|
|
180
|
+
|
|
181
|
+
user.last_messages = messages[1:]
|
|
182
|
+
user.last_messages.append({"role": "assistant", "content": content})
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# if max iterations reached, persist current state
|
|
186
|
+
user.last_messages = messages[1:]
|
|
187
|
+
print("\n[Notice]: Maximum task iterations reached.")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def main():
|
|
191
|
+
print("=" * 60)
|
|
192
|
+
print(f" Desktop Agent initialized [{_PLATFORM}]")
|
|
193
|
+
print(" Chain-of-Thought + ReAct loop enabled")
|
|
194
|
+
print(" Type 'exit' or 'quit' to close.")
|
|
195
|
+
print("=" * 60 + "\n")
|
|
196
|
+
|
|
197
|
+
while True:
|
|
198
|
+
try:
|
|
199
|
+
msg = input("Enter Your Message: ").strip()
|
|
200
|
+
if not msg:
|
|
201
|
+
continue
|
|
202
|
+
if msg.lower() in ("exit", "quit"):
|
|
203
|
+
print("Goodbye!")
|
|
204
|
+
break
|
|
205
|
+
user.message = msg
|
|
206
|
+
await loop(user.message)
|
|
207
|
+
except (KeyboardInterrupt, EOFError):
|
|
208
|
+
print("\nExiting...")
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def cli():
|
|
213
|
+
asyncio.run(main())
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
cli()
|
|
218
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "avins"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "An advanced, proactive AI desktop assistant with terminal and GUI automation capabilities."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Avinash"},
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"groq",
|
|
17
|
+
"python-dotenv",
|
|
18
|
+
"pydantic",
|
|
19
|
+
"pyautogui"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
avins = "core.main:cli"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
"Homepage" = "https://github.com/avin-1/avins"
|
|
27
|
+
"Bug Tracker" = "https://github.com/avin-1/avins/issues"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
packages = ["core", "model", "agents", "config", "tools"]
|
avins-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools.py — OS-independent tool implementations.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- Windows → pyautogui for all GUI actions
|
|
6
|
+
- Linux X11 → pyautogui
|
|
7
|
+
- Linux Wayland → ydotool (with GNOME xdotool fallback) → pyautogui
|
|
8
|
+
- macOS → pyautogui
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
import pyautogui
|
|
18
|
+
|
|
19
|
+
pyautogui.FAILSAFE = False
|
|
20
|
+
|
|
21
|
+
# ── Detect current OS once at import time ─────────────────────────────────────
|
|
22
|
+
PLATFORM = platform.system() # 'Windows' | 'Linux' | 'Darwin'
|
|
23
|
+
|
|
24
|
+
# On Linux, grant the current user access to the X display (no-op on Wayland/Windows)
|
|
25
|
+
if PLATFORM == "Linux":
|
|
26
|
+
import getpass
|
|
27
|
+
_username = getpass.getuser()
|
|
28
|
+
subprocess.run(
|
|
29
|
+
["xhost", f"+SI:localuser:{_username}"],
|
|
30
|
+
stdout=subprocess.DEVNULL,
|
|
31
|
+
stderr=subprocess.DEVNULL,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
# Terminal tool
|
|
37
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
def use_terminal(text: str) -> subprocess.CompletedProcess:
|
|
40
|
+
"""Executes a terminal command after asking for user confirmation.
|
|
41
|
+
|
|
42
|
+
Works on Windows (cmd/PowerShell), Linux, and macOS.
|
|
43
|
+
"""
|
|
44
|
+
des = input(
|
|
45
|
+
f"Do you want to execute this command?\n"
|
|
46
|
+
f"{text}\n"
|
|
47
|
+
f"Enter 1 for Yes, 0 for No: "
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
des = int(des)
|
|
52
|
+
except ValueError:
|
|
53
|
+
print("Invalid input. Please enter 1 or 0.")
|
|
54
|
+
return subprocess.CompletedProcess(
|
|
55
|
+
args=text, returncode=1, stdout="", stderr="Invalid user input"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if des == 1:
|
|
59
|
+
print("Running Process....")
|
|
60
|
+
res = subprocess.run(
|
|
61
|
+
text,
|
|
62
|
+
shell=True,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
)
|
|
66
|
+
print("Command Executed Output is: ", res.stdout, "Exiting...")
|
|
67
|
+
return res
|
|
68
|
+
else:
|
|
69
|
+
print("Command Execution cancelled")
|
|
70
|
+
return subprocess.CompletedProcess(
|
|
71
|
+
args=text, returncode=1, stdout="", stderr="Command execution cancelled by user"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
76
|
+
# Linux Wayland helpers (all guarded — never called on Windows/macOS)
|
|
77
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
78
|
+
|
|
79
|
+
def _is_wayland() -> bool:
|
|
80
|
+
"""True only on Linux Wayland sessions."""
|
|
81
|
+
if PLATFORM != "Linux":
|
|
82
|
+
return False
|
|
83
|
+
return (
|
|
84
|
+
os.environ.get("XDG_SESSION_TYPE", "").lower() == "wayland"
|
|
85
|
+
or bool(os.environ.get("WAYLAND_DISPLAY"))
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _is_gnome() -> bool:
|
|
90
|
+
"""True only on GNOME desktops (Linux)."""
|
|
91
|
+
if PLATFORM != "Linux":
|
|
92
|
+
return False
|
|
93
|
+
return "gnome" in os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run_ydotool(cmd_args: list[str]) -> subprocess.CompletedProcess:
|
|
97
|
+
"""Run an ydotool command, injecting the socket path if needed."""
|
|
98
|
+
env = os.environ.copy()
|
|
99
|
+
if (
|
|
100
|
+
"/tmp/.ydotool_socket" not in env.get("YDOTOOL_SOCKET", "")
|
|
101
|
+
and os.path.exists("/tmp/.ydotool_socket")
|
|
102
|
+
):
|
|
103
|
+
env["YDOTOOL_SOCKET"] = "/tmp/.ydotool_socket"
|
|
104
|
+
return subprocess.run(["ydotool"] + cmd_args, capture_output=True, text=True, env=env)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _ydotool_working() -> bool:
|
|
108
|
+
"""True if the ydotool daemon is running and uinput is accessible."""
|
|
109
|
+
res = _run_ydotool(["key", "--delay", "0", "a:1", "a:0"])
|
|
110
|
+
return res.returncode == 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _run_gnome_hotkey(keys: list[str]) -> bool:
|
|
114
|
+
"""Send a hotkey via xdotool to the XWayland display.
|
|
115
|
+
|
|
116
|
+
Works on GNOME Wayland — the compositor intercepts system shortcuts
|
|
117
|
+
sent to the XWayland display. Returns True on success.
|
|
118
|
+
"""
|
|
119
|
+
if shutil.which("xdotool") is None:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
_xdotool_key_map = {
|
|
123
|
+
"ctrl": "ctrl", "alt": "alt", "shift": "shift",
|
|
124
|
+
"super": "super", "win": "super",
|
|
125
|
+
"tab": "Tab", "return": "Return", "enter": "Return",
|
|
126
|
+
"escape": "Escape", "esc": "Escape", "space": "space",
|
|
127
|
+
"left": "Left", "right": "Right", "up": "Up", "down": "Down",
|
|
128
|
+
}
|
|
129
|
+
combo = "+".join(_xdotool_key_map.get(k.lower(), k) for k in keys)
|
|
130
|
+
|
|
131
|
+
env = os.environ.copy()
|
|
132
|
+
if not env.get("DISPLAY"):
|
|
133
|
+
env["DISPLAY"] = ":0"
|
|
134
|
+
|
|
135
|
+
res = subprocess.run(
|
|
136
|
+
["xdotool", "key", "--clearmodifiers", combo],
|
|
137
|
+
capture_output=True, text=True, env=env, timeout=5,
|
|
138
|
+
)
|
|
139
|
+
return res.returncode == 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
143
|
+
# GUI control — unified cross-platform entry point
|
|
144
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
145
|
+
|
|
146
|
+
def gui_control(
|
|
147
|
+
action: str,
|
|
148
|
+
x: int = None,
|
|
149
|
+
y: int = None,
|
|
150
|
+
duration: float = 0.0,
|
|
151
|
+
clicks: int = 1,
|
|
152
|
+
interval: float = 0.0,
|
|
153
|
+
button: str = "left",
|
|
154
|
+
text: str = None,
|
|
155
|
+
keys: list = None,
|
|
156
|
+
scroll: int = None,
|
|
157
|
+
) -> str:
|
|
158
|
+
"""Perform GUI actions using the best available backend for the current OS.
|
|
159
|
+
|
|
160
|
+
Backend selection:
|
|
161
|
+
• Windows / macOS → pyautogui (always)
|
|
162
|
+
• Linux X11 → pyautogui
|
|
163
|
+
• Linux Wayland → ydotool → GNOME xdotool → pyautogui (fallback chain)
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
action : str
|
|
168
|
+
One of 'move', 'click', 'double_click', 'right_click',
|
|
169
|
+
'typewrite', 'press', 'hotkey', 'scroll'.
|
|
170
|
+
x, y : int, optional
|
|
171
|
+
Screen coordinates for mouse actions.
|
|
172
|
+
duration : float
|
|
173
|
+
Seconds taken to move the mouse (pyautogui).
|
|
174
|
+
clicks : int
|
|
175
|
+
Number of clicks (default 1).
|
|
176
|
+
interval : float
|
|
177
|
+
Seconds between clicks.
|
|
178
|
+
button : str
|
|
179
|
+
Mouse button — 'left', 'right', or 'middle'.
|
|
180
|
+
text : str, optional
|
|
181
|
+
Text to type ('typewrite') or key to press ('press').
|
|
182
|
+
keys : list[str], optional
|
|
183
|
+
Key combination for 'hotkey' (e.g. ['ctrl', 'c']).
|
|
184
|
+
scroll : int, optional
|
|
185
|
+
Scroll amount — positive = up, negative = down.
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
str
|
|
190
|
+
'Success' or an error description.
|
|
191
|
+
"""
|
|
192
|
+
# Wayland + ydotool are only relevant on Linux
|
|
193
|
+
is_wayland = _is_wayland()
|
|
194
|
+
has_ydotool = (PLATFORM == "Linux") and (shutil.which("ydotool") is not None)
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
# ── move ──────────────────────────────────────────────────────
|
|
198
|
+
if action == "move":
|
|
199
|
+
if x is None or y is None:
|
|
200
|
+
return "Error: x and y must be provided for move action"
|
|
201
|
+
if is_wayland and has_ydotool:
|
|
202
|
+
res = _run_ydotool(["mousemove", "-a", str(x), str(y)])
|
|
203
|
+
if res.returncode != 0:
|
|
204
|
+
pyautogui.moveTo(x, y, duration=duration)
|
|
205
|
+
else:
|
|
206
|
+
pyautogui.moveTo(x, y, duration=duration)
|
|
207
|
+
|
|
208
|
+
# ── click ─────────────────────────────────────────────────────
|
|
209
|
+
elif action == "click":
|
|
210
|
+
if is_wayland and has_ydotool:
|
|
211
|
+
if x is not None and y is not None:
|
|
212
|
+
_run_ydotool(["mousemove", "-a", str(x), str(y)])
|
|
213
|
+
btn_code = "0xC0" if button == "left" else "0xC1" if button == "right" else "0xC2"
|
|
214
|
+
_run_ydotool(["click", btn_code])
|
|
215
|
+
else:
|
|
216
|
+
if x is not None and y is not None:
|
|
217
|
+
pyautogui.click(x, y, clicks=clicks, interval=interval, button=button)
|
|
218
|
+
else:
|
|
219
|
+
pyautogui.click(clicks=clicks, interval=interval, button=button)
|
|
220
|
+
|
|
221
|
+
# ── double_click ──────────────────────────────────────────────
|
|
222
|
+
elif action == "double_click":
|
|
223
|
+
if is_wayland and has_ydotool:
|
|
224
|
+
if x is not None and y is not None:
|
|
225
|
+
_run_ydotool(["mousemove", "-a", str(x), str(y)])
|
|
226
|
+
_run_ydotool(["click", "0xC0"])
|
|
227
|
+
time.sleep(0.1)
|
|
228
|
+
_run_ydotool(["click", "0xC0"])
|
|
229
|
+
else:
|
|
230
|
+
if x is not None and y is not None:
|
|
231
|
+
pyautogui.doubleClick(x, y, interval=interval, button=button)
|
|
232
|
+
else:
|
|
233
|
+
pyautogui.doubleClick(interval=interval, button=button)
|
|
234
|
+
|
|
235
|
+
# ── right_click ───────────────────────────────────────────────
|
|
236
|
+
elif action == "right_click":
|
|
237
|
+
if is_wayland and has_ydotool:
|
|
238
|
+
if x is not None and y is not None:
|
|
239
|
+
_run_ydotool(["mousemove", "-a", str(x), str(y)])
|
|
240
|
+
_run_ydotool(["click", "0xC1"])
|
|
241
|
+
else:
|
|
242
|
+
if x is not None and y is not None:
|
|
243
|
+
pyautogui.rightClick(x, y, clicks=clicks, interval=interval)
|
|
244
|
+
else:
|
|
245
|
+
pyautogui.rightClick(clicks=clicks, interval=interval)
|
|
246
|
+
|
|
247
|
+
# ── typewrite ─────────────────────────────────────────────────
|
|
248
|
+
elif action == "typewrite":
|
|
249
|
+
if text is None:
|
|
250
|
+
return "Error: text must be provided for typewrite action"
|
|
251
|
+
if is_wayland and has_ydotool:
|
|
252
|
+
_run_ydotool(["type", "--", text])
|
|
253
|
+
else:
|
|
254
|
+
pyautogui.typewrite(text, interval=interval)
|
|
255
|
+
|
|
256
|
+
# ── press ─────────────────────────────────────────────────────
|
|
257
|
+
elif action == "press":
|
|
258
|
+
if text is None:
|
|
259
|
+
return "Error: key must be provided in text parameter for press action"
|
|
260
|
+
if is_wayland and has_ydotool:
|
|
261
|
+
_run_ydotool(["key", f"{text}:1", f"{text}:0"])
|
|
262
|
+
else:
|
|
263
|
+
pyautogui.press(text)
|
|
264
|
+
|
|
265
|
+
# ── hotkey ────────────────────────────────────────────────────
|
|
266
|
+
elif action == "hotkey":
|
|
267
|
+
if not keys:
|
|
268
|
+
return "Error: keys list must be provided for hotkey action"
|
|
269
|
+
if is_wayland:
|
|
270
|
+
# Fallback chain: GNOME xdotool → ydotool daemon → pyautogui
|
|
271
|
+
if _is_gnome() and _run_gnome_hotkey(keys):
|
|
272
|
+
pass # handled
|
|
273
|
+
elif has_ydotool and _ydotool_working():
|
|
274
|
+
key_args = [f"{k}:1" for k in keys] + [f"{k}:0" for k in reversed(keys)]
|
|
275
|
+
_run_ydotool(["key"] + key_args)
|
|
276
|
+
else:
|
|
277
|
+
pyautogui.hotkey(*keys)
|
|
278
|
+
else:
|
|
279
|
+
pyautogui.hotkey(*keys)
|
|
280
|
+
|
|
281
|
+
# ── scroll ────────────────────────────────────────────────────
|
|
282
|
+
elif action == "scroll":
|
|
283
|
+
if scroll is None:
|
|
284
|
+
return "Error: scroll amount must be provided for scroll action"
|
|
285
|
+
pyautogui.scroll(scroll, x=x, y=y)
|
|
286
|
+
|
|
287
|
+
else:
|
|
288
|
+
return f"Error: Unknown action '{action}'"
|
|
289
|
+
|
|
290
|
+
return "Success"
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
return f"Error: {e}"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
tool = [ { 'function': { 'description': 'Executes a terminal command in the '
|
|
2
|
+
'current working directory.',
|
|
3
|
+
'name': 'use_terminal',
|
|
4
|
+
'parameters': { 'properties': { 'text': { 'type': 'string'}},
|
|
5
|
+
'required': ['text'],
|
|
6
|
+
'type': 'object'}},
|
|
7
|
+
'type': 'function'},
|
|
8
|
+
{ 'function': { 'description': 'Perform GUI actions such as moving the '
|
|
9
|
+
'mouse, clicking, typing, pressing '
|
|
10
|
+
'keys, hotkeys, and scrolling using '
|
|
11
|
+
'pyautogui.',
|
|
12
|
+
'name': 'gui_control',
|
|
13
|
+
'parameters': { 'properties': { 'action': { 'description': 'The '
|
|
14
|
+
'GUI '
|
|
15
|
+
'action '
|
|
16
|
+
'to '
|
|
17
|
+
'perform '
|
|
18
|
+
'(move, '
|
|
19
|
+
'click, '
|
|
20
|
+
'double_click, '
|
|
21
|
+
'right_click, '
|
|
22
|
+
'typewrite, '
|
|
23
|
+
'press, '
|
|
24
|
+
'hotkey, '
|
|
25
|
+
'scroll).',
|
|
26
|
+
'enum': [ 'move',
|
|
27
|
+
'click',
|
|
28
|
+
'double_click',
|
|
29
|
+
'right_click',
|
|
30
|
+
'typewrite',
|
|
31
|
+
'press',
|
|
32
|
+
'hotkey',
|
|
33
|
+
'scroll'],
|
|
34
|
+
'type': 'string'},
|
|
35
|
+
'button': { 'default': 'left',
|
|
36
|
+
'description': 'Mouse '
|
|
37
|
+
'button '
|
|
38
|
+
'to '
|
|
39
|
+
'use '
|
|
40
|
+
'(left, '
|
|
41
|
+
'right, '
|
|
42
|
+
'middle).',
|
|
43
|
+
'enum': [ 'left',
|
|
44
|
+
'right',
|
|
45
|
+
'middle'],
|
|
46
|
+
'type': 'string'},
|
|
47
|
+
'clicks': { 'default': 1,
|
|
48
|
+
'description': 'Number '
|
|
49
|
+
'of '
|
|
50
|
+
'clicks '
|
|
51
|
+
'(default '
|
|
52
|
+
'1).',
|
|
53
|
+
'type': 'integer'},
|
|
54
|
+
'duration': { 'default': 0.0,
|
|
55
|
+
'description': 'Duration '
|
|
56
|
+
'for '
|
|
57
|
+
'mouse '
|
|
58
|
+
'movement '
|
|
59
|
+
'(seconds).',
|
|
60
|
+
'type': 'number'},
|
|
61
|
+
'interval': { 'default': 0.0,
|
|
62
|
+
'description': 'Interval '
|
|
63
|
+
'between '
|
|
64
|
+
'clicks '
|
|
65
|
+
'(seconds).',
|
|
66
|
+
'type': 'number'},
|
|
67
|
+
'keys': { 'description': 'List '
|
|
68
|
+
'of '
|
|
69
|
+
'keys '
|
|
70
|
+
'for '
|
|
71
|
+
'hotkey '
|
|
72
|
+
'action.',
|
|
73
|
+
'items': { 'type': 'string'},
|
|
74
|
+
'type': 'array'},
|
|
75
|
+
'scroll': { 'description': 'Amount '
|
|
76
|
+
'to '
|
|
77
|
+
'scroll '
|
|
78
|
+
'(positive '
|
|
79
|
+
'for '
|
|
80
|
+
'up, '
|
|
81
|
+
'negative '
|
|
82
|
+
'for '
|
|
83
|
+
'down).',
|
|
84
|
+
'type': 'integer'},
|
|
85
|
+
'text': { 'description': 'Text '
|
|
86
|
+
'to '
|
|
87
|
+
'type '
|
|
88
|
+
'or '
|
|
89
|
+
'key '
|
|
90
|
+
'to '
|
|
91
|
+
'press '
|
|
92
|
+
'for '
|
|
93
|
+
'certain '
|
|
94
|
+
'actions.',
|
|
95
|
+
'type': 'string'},
|
|
96
|
+
'x': { 'description': 'X '
|
|
97
|
+
'coordinate '
|
|
98
|
+
'for '
|
|
99
|
+
'mouse '
|
|
100
|
+
'actions '
|
|
101
|
+
'(optional).',
|
|
102
|
+
'type': 'integer'},
|
|
103
|
+
'y': { 'description': 'Y '
|
|
104
|
+
'coordinate '
|
|
105
|
+
'for '
|
|
106
|
+
'mouse '
|
|
107
|
+
'actions '
|
|
108
|
+
'(optional).',
|
|
109
|
+
'type': 'integer'}},
|
|
110
|
+
'required': ['action'],
|
|
111
|
+
'type': 'object'}},
|
|
112
|
+
'type': 'function'}]
|
|
113
|
+
|
|
114
|
+
__all__ = ["tool"]
|
|
115
|
+
|
|
116
|
+
|