autosh 0.0.0__py3-none-any.whl → 0.0.1__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.
@@ -0,0 +1,87 @@
1
+ import rich
2
+ from rich.prompt import Confirm
3
+ from rich.panel import Panel
4
+ from rich.console import RenderableType
5
+ from autosh.config import CLI_OPTIONS, CONFIG
6
+
7
+
8
+ def banner(tag: str, text: str | None = None, dim: str | None = None):
9
+ if CLI_OPTIONS.quiet:
10
+ return
11
+ s = f"[bold magenta]{tag}[/bold magenta]"
12
+ if text:
13
+ s += f" [italic magenta]{text}[/italic magenta]"
14
+ if dim:
15
+ s += f" [italic dim]{dim}[/italic dim]"
16
+ s += "\n"
17
+ rich.print(s)
18
+
19
+
20
+ def confirm(message: str):
21
+ if CLI_OPTIONS.yes:
22
+ return True
23
+ result = Confirm.ask(
24
+ f"[magenta]{message}[/magenta]", default=True, case_sensitive=False
25
+ )
26
+ if not CLI_OPTIONS.quiet:
27
+ rich.print()
28
+ return result
29
+
30
+
31
+ def cmd_preview_panel(title: str, content: RenderableType, short: str | None = None):
32
+ if CLI_OPTIONS.quiet and not CLI_OPTIONS.yes:
33
+ if short:
34
+ rich.print(f"[magenta]{short}[/magenta]\n")
35
+ return
36
+ panel = Panel.fit(content, title=f"[magenta]{title}[/magenta]", title_align="left")
37
+ rich.print(panel)
38
+ rich.print()
39
+
40
+
41
+ def cmd_result_panel(
42
+ title: str,
43
+ out: str | None = None,
44
+ err: str | None = None,
45
+ ):
46
+ if CLI_OPTIONS.quiet:
47
+ return
48
+ if isinstance(out, str):
49
+ out = out.strip()
50
+ if isinstance(err, str):
51
+ err = err.strip()
52
+ if not out and not err:
53
+ rich.print(title)
54
+ else:
55
+ text = out if out else ""
56
+ text += (("\n---\n" if out else "") + err) if err else ""
57
+ panel = Panel.fit(text, title=title, title_align="left", style="dim")
58
+ rich.print(panel)
59
+ if not CLI_OPTIONS.quiet:
60
+ rich.print()
61
+
62
+
63
+ from . import calc
64
+ from . import clock
65
+ from . import code
66
+ from . import search
67
+ from . import web
68
+ from . import cli
69
+
70
+
71
+ def create_plugins():
72
+ """Get all plugins in the autosh.plugins module."""
73
+ cfgs = CONFIG.plugins
74
+ plugins = []
75
+ if cfgs.calc is not None:
76
+ plugins.append(calc.CalculatorPlugin(cfgs.calc.model_dump()))
77
+ if cfgs.cli is not None:
78
+ plugins.append(cli.CLIPlugin(cfgs.cli.model_dump()))
79
+ if cfgs.clock is not None:
80
+ plugins.append(clock.ClockPlugin(cfgs.clock.model_dump()))
81
+ if cfgs.code is not None:
82
+ plugins.append(code.CodePlugin(cfgs.code.model_dump()))
83
+ if cfgs.search is not None:
84
+ plugins.append(search.SearchPlugin(cfgs.search.model_dump()))
85
+ if cfgs.web is not None:
86
+ plugins.append(web.WebPlugin(cfgs.web.model_dump()))
87
+ return plugins
autosh/plugins/calc.py ADDED
@@ -0,0 +1,22 @@
1
+ from agentia.plugins import tool, Plugin
2
+ from typing import Annotated
3
+ from . import banner
4
+
5
+
6
+ class CalculatorPlugin(Plugin):
7
+ NAME = "calc"
8
+
9
+ @tool
10
+ def evaluate(
11
+ self,
12
+ expression: Annotated[
13
+ str, "The math expression to evaluate. Must be an valid python expression."
14
+ ],
15
+ ):
16
+ """
17
+ Execute a math expression and return the result. The expression must be an valid python expression that can be execuated by `eval()`.
18
+ """
19
+ banner("CALC", expression)
20
+
21
+ result = eval(expression)
22
+ return result
autosh/plugins/cli.py ADDED
@@ -0,0 +1,222 @@
1
+ import os
2
+ import sys
3
+ from typing import Annotated
4
+ from agentia.plugins import Plugin, tool
5
+ import rich
6
+ import subprocess
7
+ from enum import StrEnum
8
+
9
+ from autosh.config import CLI_OPTIONS
10
+
11
+ from . import banner, confirm, cmd_result_panel, cmd_preview_panel
12
+
13
+
14
+ class Color(StrEnum):
15
+ black = "black"
16
+ red = "red"
17
+ green = "green"
18
+ yellow = "yellow"
19
+ blue = "blue"
20
+ magenta = "magenta"
21
+ cyan = "cyan"
22
+ white = "white"
23
+ bright_black = "bright_black"
24
+ bright_red = "bright_red"
25
+ bright_green = "bright_green"
26
+ bright_yellow = "bright_yellow"
27
+ bright_blue = "bright_blue"
28
+ bright_magenta = "bright_magenta"
29
+ bright_cyan = "bright_cyan"
30
+ bright_white = "bright_white"
31
+ dim = "dim"
32
+
33
+
34
+ class CLIPlugin(Plugin):
35
+ EXIT_CODE = 0
36
+
37
+ @tool
38
+ def print(
39
+ self,
40
+ text: Annotated[
41
+ str,
42
+ "The text to print. Can be markdown or using python-rich's markup syntax.",
43
+ ],
44
+ color: Annotated[Color | None, "The color of the text"] = None,
45
+ bold: Annotated[bool, "Whether to print the text in bold"] = False,
46
+ italic: Annotated[bool, "Whether to print the text in italic"] = False,
47
+ stderr: Annotated[bool, "Whether to print the text to stderr"] = False,
48
+ end: Annotated[str, "The text to print at the end"] = "\n",
49
+ ):
50
+ """
51
+ Print an important message to the terminal. NOTE: Important message ONLY! Don't use it when you want to say something to the user.
52
+ """
53
+ if color:
54
+ text = f"[{color}]{text}[/{color}]"
55
+ if bold:
56
+ text = f"[bold]{text}[/bold]"
57
+ if italic:
58
+ text = f"[italic]{text}[/italic]"
59
+ rich.print(text, file=sys.stderr if stderr else sys.stdout, end=end)
60
+ return "DONE. You can continue and no need to repeat the text"
61
+
62
+ @tool
63
+ def chdir(self, path: Annotated[str, "The path to the new working directory"]):
64
+ """
65
+ Changes the current working directory of the terminal to another directory.
66
+ """
67
+ banner("CWD", path)
68
+ if not os.path.exists(path):
69
+ raise FileNotFoundError(f"Path `{path}` does not exist.")
70
+ os.chdir(path)
71
+
72
+ @tool
73
+ def get_argv(self):
74
+ """
75
+ Get the command line arguments.
76
+ """
77
+ banner("GET ARGV")
78
+ if not CLI_OPTIONS.script:
79
+ return CLI_OPTIONS.args
80
+ return {
81
+ "script": str(CLI_OPTIONS.script),
82
+ "args": CLI_OPTIONS.args,
83
+ }
84
+
85
+ @tool
86
+ def read(
87
+ self,
88
+ path: Annotated[str, "The path to the file to read"],
89
+ ):
90
+ """
91
+ Read a file and print its content.
92
+ """
93
+ banner("READ", path)
94
+ if not os.path.exists(path):
95
+ raise FileNotFoundError(f"File `{path}` does not exist.")
96
+ if not os.path.isfile(path):
97
+ raise FileNotFoundError(f"Path `{path}` is not a file.")
98
+ with open(path, "r") as f:
99
+ content = f.read()
100
+ return content
101
+
102
+ @tool
103
+ def write(
104
+ self,
105
+ path: Annotated[str, "The path to the file to write"],
106
+ content: Annotated[str, "The content to write to the file"],
107
+ create: Annotated[
108
+ bool, "Whether to create the file if it does not exist"
109
+ ] = True,
110
+ append: Annotated[bool, "Whether to append to the file if it exists"] = False,
111
+ ):
112
+ """
113
+ Write or append text content to a file.
114
+ """
115
+ banner("WRITE" if not append else "APPEND", path, f"({len(content)} bytes)")
116
+
117
+ if not create and not os.path.exists(path):
118
+ raise FileNotFoundError(f"File `{path}` does not exist.")
119
+ if not create and not os.path.isfile(path):
120
+ raise FileNotFoundError(f"Path `{path}` is not a file.")
121
+ if path == str(CLI_OPTIONS.script):
122
+ raise FileExistsError(
123
+ f"No, you cannot overwrite the script file `{path}`. You're likely writing to it by mistake."
124
+ )
125
+ if not confirm("Write file?"):
126
+ return {"error": "The user declined the write operation."}
127
+ flag = "a" if append else "w"
128
+ if create:
129
+ flag += "+"
130
+ with open(path, flag) as f:
131
+ f.write(content)
132
+ return "DONE. You can continue and no need to repeat the text"
133
+
134
+ @tool
135
+ def stdin_readline(
136
+ self,
137
+ prompt: Annotated[
138
+ str | None, "The optional prompt to display before reading from stdin"
139
+ ] = None,
140
+ ):
141
+ """
142
+ Read a line from stdin.
143
+ """
144
+ if not sys.stdin.isatty():
145
+ raise RuntimeError("stdin is not a terminal.")
146
+ return input(prompt)
147
+
148
+ @tool
149
+ def stdin_readall(self):
150
+ """
151
+ Read all from stdin until EOF.
152
+ """
153
+ if sys.stdin.isatty():
154
+ raise RuntimeError("No piped input. stdin is a terminal.")
155
+ if not CLI_OPTIONS.quiet:
156
+ rich.print("[bold magenta]READ ALL STDIN[/bold magenta]\n")
157
+ if CLI_OPTIONS.stdin_is_script:
158
+ raise RuntimeError("No piped input from stdin")
159
+ return sys.stdin.read()
160
+
161
+ @tool
162
+ def exec(
163
+ self,
164
+ command: Annotated[
165
+ str,
166
+ "The one-liner bash command to execute. This will be directly sent to `bash -c ...` so be careful with the quotes escaping!",
167
+ ],
168
+ explanation: Annotated[
169
+ str,
170
+ "Explain what this command does, and what are you going to use it for.",
171
+ ],
172
+ ):
173
+ """
174
+ Run a one-liner bash command
175
+ """
176
+
177
+ def run():
178
+ cmd = ["bash", "-c", command]
179
+ return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
180
+
181
+ # Print the command and explanation
182
+ cmd_preview_panel(
183
+ title="Run Command",
184
+ content=f"[magenta][bold]➜[/bold] [italic]{command}[/italic][/magenta]\n\n[dim]{explanation}[/dim]",
185
+ short=f"[magenta][bold]➜[/bold] [italic]{command}[/italic][/magenta]",
186
+ )
187
+
188
+ # Ask for confirmation
189
+ if not confirm("Execute this command?"):
190
+ return {"error": "The user declined to execute the command."}
191
+
192
+ # Execute the command
193
+ proc_result = run()
194
+
195
+ # Print the result
196
+ if not CLI_OPTIONS.quiet:
197
+ out = proc_result.stdout.decode("utf-8")
198
+ err = proc_result.stderr.decode("utf-8")
199
+ if not out and not err:
200
+ title = "[green][bold]✔[/bold] Command Finished[/green]"
201
+ else:
202
+ if proc_result.returncode != 0:
203
+ title = f"[red][bold]✘[/bold] Command Failed [{proc_result.returncode}][/red]"
204
+ else:
205
+ title = "[green][bold]✔[/bold] Command Finished[/green]"
206
+ cmd_result_panel(title, out, err)
207
+
208
+ result = {
209
+ "stdout": proc_result.stdout.decode("utf-8"),
210
+ "stderr": proc_result.stderr.decode("utf-8"),
211
+ "returncode": proc_result.returncode,
212
+ "success": proc_result.returncode == 0,
213
+ }
214
+ return result
215
+
216
+ @tool
217
+ def exit(self, exitcode: Annotated[int, "The exit code of this shell session"] = 0):
218
+ """
219
+ Exit the current shell session with an optional exit code.
220
+ """
221
+ banner("EXIT", str(exitcode))
222
+ sys.exit(exitcode)
@@ -0,0 +1,20 @@
1
+ import datetime
2
+ from agentia.plugins import tool, Plugin
3
+ import tzlocal
4
+
5
+ from autosh.plugins import banner
6
+
7
+
8
+ class ClockPlugin(Plugin):
9
+ @tool
10
+ def get_current_time(self):
11
+ """Get the current UTC time in ISO format"""
12
+ banner("GET TIME")
13
+ utc = datetime.datetime.now(datetime.timezone.utc).isoformat()
14
+ local = datetime.datetime.now().isoformat()
15
+ timezone = tzlocal.get_localzone_name()
16
+ return {
17
+ "utc": utc,
18
+ "local": local,
19
+ "timezone": timezone,
20
+ }
autosh/plugins/code.py ADDED
@@ -0,0 +1,68 @@
1
+ from agentia.plugins import tool, Plugin
2
+ from typing import Annotated
3
+ import traceback
4
+ from rich.syntax import Syntax
5
+ from rich.console import group
6
+ from contextlib import redirect_stdout, redirect_stderr
7
+ import io
8
+ from . import confirm, cmd_result_panel, cmd_preview_panel
9
+
10
+
11
+ class CodePlugin(Plugin):
12
+ @tool
13
+ def execute(
14
+ self,
15
+ python_code: Annotated[str, "The python code to run."],
16
+ explanation: Annotated[
17
+ str, "Explain what this code does, and what are you going to use it for."
18
+ ],
19
+ ):
20
+ """
21
+ Execute python code and return the result.
22
+ The python code must be a valid python source file that accepts no inputs.
23
+ Print results to stdout or stderr.
24
+ """
25
+
26
+ @group()
27
+ def code_with_explanation():
28
+ yield Syntax(python_code.strip(), "python")
29
+ yield "\n[dim]───[/dim]\n"
30
+ yield f"[dim]{explanation}[/dim]"
31
+
32
+ cmd_preview_panel(
33
+ title="Run Python",
34
+ content=code_with_explanation(),
35
+ short=f"[bold]RUN[/bold] [italic]Python Code[/italic]",
36
+ )
37
+
38
+ if not confirm("Execute this code?"):
39
+ return {"error": "The user declined to execute the command."}
40
+
41
+ out = io.StringIO()
42
+ err = io.StringIO()
43
+ with redirect_stdout(out):
44
+ with redirect_stderr(err):
45
+ try:
46
+ exec(python_code, globals())
47
+ o = out.getvalue()
48
+ e = err.getvalue()
49
+ title = "[green][bold]✔[/bold] Finished[/green]"
50
+ result = {
51
+ "stdout": o,
52
+ "stderr": e,
53
+ "success": True,
54
+ }
55
+ except Exception as ex:
56
+ o = out.getvalue()
57
+ e = err.getvalue()
58
+ title = "[red][bold]✘[/bold] Failed [/red]"
59
+ result = {
60
+ "stdout": o,
61
+ "stderr": e,
62
+ "success": False,
63
+ "error": str(ex),
64
+ "traceback": repr(traceback.format_exc()),
65
+ }
66
+
67
+ cmd_result_panel(title, o, e)
68
+ return result
@@ -0,0 +1,90 @@
1
+ from agentia.plugins import tool, Plugin
2
+ from typing import Annotated, override
3
+ import os
4
+ from tavily import TavilyClient
5
+
6
+ from autosh.plugins import banner
7
+
8
+
9
+ class SearchPlugin(Plugin):
10
+ @override
11
+ async def init(self):
12
+ if "tavily_api_key" in self.config:
13
+ key = self.config["tavily_api_key"]
14
+ elif "TAVILY_API_KEY" in os.environ:
15
+ key = os.environ["TAVILY_API_KEY"]
16
+ else:
17
+ raise ValueError("Please set the TAVILY_API_KEY environment variable.")
18
+ self.__tavily = TavilyClient(api_key=key)
19
+
20
+ @tool
21
+ async def web_search(
22
+ self,
23
+ query: Annotated[
24
+ str, "The search query. Please be as specific and verbose as possible."
25
+ ],
26
+ ):
27
+ """
28
+ Perform web search on the given query.
29
+ Returning the top related search results in json format.
30
+ When necessary, you need to combine this tool with the get_webpage_content tools (if available), to browse the web in depth by jumping through links.
31
+ """
32
+ banner("WEB SEARCH", dim=query)
33
+
34
+ tavily_results = self.__tavily.search(
35
+ query=query,
36
+ search_depth="advanced",
37
+ # max_results=10,
38
+ include_answer=True,
39
+ include_images=True,
40
+ include_image_descriptions=True,
41
+ )
42
+ return tavily_results
43
+
44
+ @tool
45
+ async def news_search(
46
+ self,
47
+ query: Annotated[
48
+ str, "The search query. Please be as specific and verbose as possible."
49
+ ],
50
+ ):
51
+ """
52
+ Perform news search on the given query.
53
+ Returning the top related results in json format.
54
+ """
55
+ banner("NEWS SEARCH", dim=query)
56
+
57
+ tavily_results = self.__tavily.search(
58
+ query=query,
59
+ search_depth="advanced",
60
+ topic="news",
61
+ # max_results=10,
62
+ include_answer=True,
63
+ include_images=True,
64
+ include_image_descriptions=True,
65
+ )
66
+ return tavily_results
67
+
68
+ @tool
69
+ async def finance_search(
70
+ self,
71
+ query: Annotated[
72
+ str, "The search query. Please be as specific and verbose as possible."
73
+ ],
74
+ ):
75
+ """
76
+ Search for finance-related news and information on the given query.
77
+ Returning the top related results in json format.
78
+ """
79
+ banner("FINANCE SEARCH", dim=query)
80
+
81
+ tavily_results = self.__tavily.search(
82
+ query=query,
83
+ search_depth="advanced",
84
+ topic="finance",
85
+ # max_results=10,
86
+ include_answer=True,
87
+ include_images=True,
88
+ include_image_descriptions=True,
89
+ )
90
+ return tavily_results
autosh/plugins/web.py ADDED
@@ -0,0 +1,73 @@
1
+ from io import BytesIO
2
+ import os
3
+ from agentia.plugins import tool, Plugin
4
+ from typing import Annotated, override
5
+ import requests
6
+ from markdownify import markdownify
7
+ import uuid
8
+ from tavily import TavilyClient
9
+
10
+ from autosh.plugins import banner
11
+
12
+
13
+ class WebPlugin(Plugin):
14
+
15
+ @override
16
+ async def init(self):
17
+ if "tavily_api_key" in self.config:
18
+ key = self.config["tavily_api_key"]
19
+ elif "TAVILY_API_KEY" in os.environ:
20
+ key = os.environ["TAVILY_API_KEY"]
21
+ else:
22
+ raise ValueError("Please set the TAVILY_API_KEY environment variable.")
23
+ self.__tavily = TavilyClient(api_key=key)
24
+
25
+ def __embed_file(self, content: bytes, file_ext: str):
26
+ assert self.agent.knowledge_base is not None
27
+ with BytesIO(content) as f:
28
+ ext = file_ext if file_ext.startswith(".") else "." + file_ext
29
+ f.name = str(uuid.uuid4()) + ext
30
+ # self.agent.knowledge_base.add_document(f)
31
+ # file_name = f.name
32
+ raise NotImplementedError
33
+ return {
34
+ "file_name": file_name,
35
+ "hint": f"This is a .{file_ext} file and it is embeded in the knowledge base. Use _file_search to query the content.",
36
+ }
37
+
38
+ def __get(self, url: str):
39
+ res = requests.get(url)
40
+ res.raise_for_status()
41
+ content_type = res.headers.get("content-type")
42
+ # if content_type == "application/pdf":
43
+ # # Add this file to the knowledge base
44
+ # if self.agent.knowledge_base is not None:
45
+ # return self.__embed_file(res.content, "pdf")
46
+ # return {"content": "This is a PDF file. You don't know how to view it."}
47
+ md = markdownify(res.text)
48
+ return {"content": md}
49
+
50
+ @tool
51
+ def get_webpage_content(
52
+ self,
53
+ url: Annotated[str, "The URL of the web page to get the content of"],
54
+ ):
55
+ """
56
+ Access a web page by a URL, and fetch the content of this web page (in markdown format).
57
+ You can always use this tool to directly access web content or access external sites.
58
+ Use it at any time when you think you may need to access the internet.
59
+ """
60
+ banner("BROWSE", dim=url)
61
+
62
+ result = self.__tavily.extract(
63
+ urls=url,
64
+ # extract_depth="advanced",
65
+ include_images=True,
66
+ )
67
+ failed_results = result.get("failed_results", [])
68
+ if len(failed_results) > 0:
69
+ try:
70
+ return self.__get(url)
71
+ except Exception as e:
72
+ pass
73
+ return result