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 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,2 @@
1
+ [console_scripts]
2
+ avins = core.main:cli
@@ -0,0 +1,4 @@
1
+ groq
2
+ python-dotenv
3
+ pydantic
4
+ pyautogui
@@ -0,0 +1,5 @@
1
+ agents
2
+ config
3
+ core
4
+ model
5
+ tools
@@ -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"]
@@ -0,0 +1,12 @@
1
+ from groq import Groq
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
+ client = Groq()
11
+
12
+ __all__ = ["client"]
@@ -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,9 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class Tool(BaseModel):
5
+ name: str = ""
6
+ message: list[str] = Field(default_factory=list)
7
+
8
+
9
+ __all__ = ["Tool"]
@@ -0,0 +1,11 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class User(BaseModel):
6
+ message: Optional[str] = None
7
+ last_messages: list[dict] = Field(default_factory=list)
8
+
9
+
10
+ __all__ = ["User"]
11
+
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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
+