autosh 0.0.6__tar.gz → 0.0.7__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.
- {autosh-0.0.6 → autosh-0.0.7}/PKG-INFO +8 -8
- {autosh-0.0.6 → autosh-0.0.7}/README.md +7 -7
- {autosh-0.0.6 → autosh-0.0.7}/autosh/config.py +8 -0
- {autosh-0.0.6 → autosh-0.0.7}/autosh/main.py +5 -3
- autosh-0.0.7/autosh/plugins/__init__.py +160 -0
- {autosh-0.0.6 → autosh-0.0.7}/autosh/plugins/calc.py +2 -6
- {autosh-0.0.6 → autosh-0.0.7}/autosh/plugins/cli.py +28 -19
- {autosh-0.0.6 → autosh-0.0.7}/autosh/plugins/clock.py +2 -2
- {autosh-0.0.6 → autosh-0.0.7}/autosh/plugins/code.py +7 -6
- {autosh-0.0.6 → autosh-0.0.7}/autosh/plugins/search.py +4 -16
- {autosh-0.0.6 → autosh-0.0.7}/autosh/plugins/web.py +2 -2
- {autosh-0.0.6 → autosh-0.0.7}/autosh/session.py +57 -18
- {autosh-0.0.6 → autosh-0.0.7}/pyproject.toml +1 -1
- autosh-0.0.6/autosh/plugins/__init__.py +0 -101
- {autosh-0.0.6 → autosh-0.0.7}/.gitignore +0 -0
- {autosh-0.0.6 → autosh-0.0.7}/LICENSE +0 -0
- {autosh-0.0.6 → autosh-0.0.7}/autosh/__init__.py +0 -0
- {autosh-0.0.6 → autosh-0.0.7}/autosh/config-template.toml +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: autosh
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.7
|
4
4
|
Summary: The AI-powered, noob-friendly interactive shell
|
5
5
|
Author-email: Wenyu Zhao <wenyuzhaox@gmail.com>
|
6
6
|
License-Expression: MIT
|
@@ -37,7 +37,7 @@ As an interactive shell: `ash` (alternatively, `autosh`)
|
|
37
37
|
Execute a single prompt: `ash "list current directory"`
|
38
38
|
|
39
39
|
Process piped data:
|
40
|
-
* `cat README.md | ash -y "
|
40
|
+
* `cat README.md | ash -y "summarize"`
|
41
41
|
* `cat in.csv | ash -y -q "double the first numeric column" > out.csv`
|
42
42
|
|
43
43
|
## Scripting
|
@@ -56,15 +56,15 @@ First, please display a welcome message:)
|
|
56
56
|
Write "Hello, world" to _test.log
|
57
57
|
```
|
58
58
|
|
59
|
-
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && simple.a.md`
|
60
|
-
* Auto
|
59
|
+
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && ./simple.a.md`
|
60
|
+
* Auto-generate help messages:
|
61
61
|
|
62
62
|
```console
|
63
63
|
$ ash simple.a.md -h
|
64
64
|
|
65
65
|
Usage: simple.a.md [OPTIONS]
|
66
66
|
|
67
|
-
This is a simple file manipulation script that writes "Hello, world" to a log file named
|
67
|
+
This is a simple file manipulation script that writes "Hello, world" to a log file named _test.log.
|
68
68
|
|
69
69
|
Options:
|
70
70
|
|
@@ -73,9 +73,9 @@ Write "Hello, world" to _test.log
|
|
73
73
|
|
74
74
|
## Plugins
|
75
75
|
|
76
|
-
`autosh`
|
76
|
+
`autosh` comes with several plugins to expand its capabilities:
|
77
77
|
|
78
|
-
* `ash "Create a directory
|
78
|
+
* `ash "Create a directory 'my-news', list the latest news, and for each news item, put the summary in a separate markdown file in this directory"`
|
79
79
|
|
80
80
|
# TODO
|
81
81
|
|
@@ -83,4 +83,4 @@ Write "Hello, world" to _test.log
|
|
83
83
|
- [ ] RAG for non-text files
|
84
84
|
- [ ] Plugin system
|
85
85
|
- [ ] MCP support
|
86
|
-
- [
|
86
|
+
- [x] Improved input widget with history and auto-completion
|
@@ -15,7 +15,7 @@ As an interactive shell: `ash` (alternatively, `autosh`)
|
|
15
15
|
Execute a single prompt: `ash "list current directory"`
|
16
16
|
|
17
17
|
Process piped data:
|
18
|
-
* `cat README.md | ash -y "
|
18
|
+
* `cat README.md | ash -y "summarize"`
|
19
19
|
* `cat in.csv | ash -y -q "double the first numeric column" > out.csv`
|
20
20
|
|
21
21
|
## Scripting
|
@@ -34,15 +34,15 @@ First, please display a welcome message:)
|
|
34
34
|
Write "Hello, world" to _test.log
|
35
35
|
```
|
36
36
|
|
37
|
-
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && simple.a.md`
|
38
|
-
* Auto
|
37
|
+
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && ./simple.a.md`
|
38
|
+
* Auto-generate help messages:
|
39
39
|
|
40
40
|
```console
|
41
41
|
$ ash simple.a.md -h
|
42
42
|
|
43
43
|
Usage: simple.a.md [OPTIONS]
|
44
44
|
|
45
|
-
This is a simple file manipulation script that writes "Hello, world" to a log file named
|
45
|
+
This is a simple file manipulation script that writes "Hello, world" to a log file named _test.log.
|
46
46
|
|
47
47
|
Options:
|
48
48
|
|
@@ -51,9 +51,9 @@ Write "Hello, world" to _test.log
|
|
51
51
|
|
52
52
|
## Plugins
|
53
53
|
|
54
|
-
`autosh`
|
54
|
+
`autosh` comes with several plugins to expand its capabilities:
|
55
55
|
|
56
|
-
* `ash "Create a directory
|
56
|
+
* `ash "Create a directory 'my-news', list the latest news, and for each news item, put the summary in a separate markdown file in this directory"`
|
57
57
|
|
58
58
|
# TODO
|
59
59
|
|
@@ -61,4 +61,4 @@ Write "Hello, world" to _test.log
|
|
61
61
|
- [ ] RAG for non-text files
|
62
62
|
- [ ] Plugin system
|
63
63
|
- [ ] MCP support
|
64
|
-
- [
|
64
|
+
- [x] Improved input widget with history and auto-completion
|
@@ -35,6 +35,14 @@ class Config(BaseModel):
|
|
35
35
|
description="The LLM model to use for reasoning before executing commands",
|
36
36
|
)
|
37
37
|
api_key: str | None = Field(default=None, description="OpenRouter API key.")
|
38
|
+
repl_banner: str = Field(
|
39
|
+
default="🦄 Welcome to [cyan]autosh[/cyan]. The AI-powered, noob-friendly interactive shell.",
|
40
|
+
description="The banner for the REPL.",
|
41
|
+
)
|
42
|
+
repl_prompt: str = Field(
|
43
|
+
default="[bold on cyan]{short_cwd}[/bold on cyan][cyan]\ue0b0[/cyan] ",
|
44
|
+
description="The prompt for the REPL user input.",
|
45
|
+
)
|
38
46
|
|
39
47
|
plugins: Plugins = Field(
|
40
48
|
default_factory=Plugins,
|
@@ -32,9 +32,11 @@ async def start_session(prompt: str | None, args: list[str]):
|
|
32
32
|
os.environ["OPENROUTER_INCLUDE_REASONING"] = "false"
|
33
33
|
await session.init()
|
34
34
|
piped_stdin = not sys.stdin.isatty()
|
35
|
-
|
35
|
+
piped_stdout = not sys.stdout.isatty()
|
36
|
+
if (not CLI_OPTIONS.yes) and (piped_stdin or piped_stdout):
|
36
37
|
rich.print(
|
37
|
-
"[bold red]Error:[/bold red] [red]--yes is required when using piped stdin.[/red]"
|
38
|
+
"[bold red]Error:[/bold red] [red]--yes (-y) is required when using piped stdin or stdout.[/red]",
|
39
|
+
file=sys.stderr,
|
38
40
|
)
|
39
41
|
sys.exit(1)
|
40
42
|
if prompt:
|
@@ -121,7 +123,7 @@ def parse_args() -> tuple[str | None, list[str]]:
|
|
121
123
|
try:
|
122
124
|
args = p.parse_args()
|
123
125
|
except argparse.ArgumentError as e:
|
124
|
-
rich.print(f"[bold red]Error:[/bold red] {str(e)}")
|
126
|
+
rich.print(f"[bold red]Error:[/bold red] {str(e)}", file=sys.stderr)
|
125
127
|
print_help()
|
126
128
|
sys.exit(1)
|
127
129
|
|
@@ -0,0 +1,160 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
import sys
|
3
|
+
from typing import Any, Callable
|
4
|
+
import rich
|
5
|
+
from rich.prompt import Confirm
|
6
|
+
from rich.panel import Panel
|
7
|
+
from rich.console import RenderableType
|
8
|
+
from autosh.config import CLI_OPTIONS, CONFIG
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class Banner:
|
13
|
+
title: str | Callable[[Any], str]
|
14
|
+
|
15
|
+
text: str | Callable[[Any], str] | None = None
|
16
|
+
|
17
|
+
text_key: str | None = None
|
18
|
+
|
19
|
+
code: Callable[[Any], RenderableType] | None = None
|
20
|
+
"""
|
21
|
+
Turn the banner into a code block
|
22
|
+
"""
|
23
|
+
|
24
|
+
user_consent: bool = False
|
25
|
+
|
26
|
+
def __get_text(self, args: Any):
|
27
|
+
if self.text:
|
28
|
+
return self.text(args) if callable(self.text) else self.text
|
29
|
+
elif self.text_key:
|
30
|
+
return args.get(self.text_key)
|
31
|
+
return None
|
32
|
+
|
33
|
+
def __print_simple_banner(self, args: Any):
|
34
|
+
title = self.title(args) if callable(self.title) else self.title
|
35
|
+
if not sys.stdout.isatty():
|
36
|
+
s = f"[TOOL] {title}"
|
37
|
+
if text := self.__get_text(args):
|
38
|
+
s += f" {text}"
|
39
|
+
print(s)
|
40
|
+
else:
|
41
|
+
s = f"[bold on magenta] {title} [/bold on magenta]"
|
42
|
+
if text := self.__get_text(args):
|
43
|
+
s += f" [italic dim]{text}[/italic dim]"
|
44
|
+
rich.print(s)
|
45
|
+
|
46
|
+
def render(self, args: Any, prefix_newline: bool = True) -> bool:
|
47
|
+
if CLI_OPTIONS.quiet and not (self.user_consent and not CLI_OPTIONS.yes):
|
48
|
+
return False
|
49
|
+
if prefix_newline:
|
50
|
+
print()
|
51
|
+
if self.code:
|
52
|
+
code = self.code(args)
|
53
|
+
if CLI_OPTIONS.quiet and self.user_consent and not CLI_OPTIONS.yes:
|
54
|
+
self.__print_simple_banner(args)
|
55
|
+
return True
|
56
|
+
panel = Panel.fit(
|
57
|
+
code, title=f"[magenta]{self.title}[/magenta]", title_align="left"
|
58
|
+
)
|
59
|
+
rich.print(panel)
|
60
|
+
else:
|
61
|
+
self.__print_simple_banner(args)
|
62
|
+
return True
|
63
|
+
|
64
|
+
|
65
|
+
# def __print_simple_banner(tag: str, text: str | None = None):
|
66
|
+
# if CLI_OPTIONS.quiet:
|
67
|
+
# return
|
68
|
+
# if not sys.stdout.isatty():
|
69
|
+
# s = f"\n[TOOL] {tag}"
|
70
|
+
# if text:
|
71
|
+
# s += f" {text}"
|
72
|
+
# print(s)
|
73
|
+
# return
|
74
|
+
# s = f"\n[bold on magenta] {tag} [/bold on magenta]"
|
75
|
+
# if text:
|
76
|
+
# s += f" [italic dim]{text}[/italic dim]"
|
77
|
+
# rich.print(s)
|
78
|
+
|
79
|
+
|
80
|
+
# def simple_banner(
|
81
|
+
# tag: str | Callable[[Any], str],
|
82
|
+
# text: Callable[[Any], str] | None = None,
|
83
|
+
# text_key: str | None = None,
|
84
|
+
# ):
|
85
|
+
# return lambda x: __print_simple_banner(
|
86
|
+
# tag if isinstance(tag, str) else tag(x),
|
87
|
+
# text(x) if text else (x.get(text_key) if text_key else None),
|
88
|
+
# )
|
89
|
+
|
90
|
+
|
91
|
+
# def __print_code_preview_banner(
|
92
|
+
# title: str, content: RenderableType, short: str | None = None
|
93
|
+
# ):
|
94
|
+
# if CLI_OPTIONS.quiet:
|
95
|
+
# if short and not CLI_OPTIONS.yes:
|
96
|
+
# rich.print(f"\n[magenta]{short}[/magenta]\n")
|
97
|
+
# return
|
98
|
+
# panel = Panel.fit(content, title=f"[magenta]{title}[/magenta]", title_align="left")
|
99
|
+
# rich.print()
|
100
|
+
# rich.print(panel)
|
101
|
+
|
102
|
+
|
103
|
+
# def code_preview_banner(
|
104
|
+
# title: str | Callable[[Any], str],
|
105
|
+
# short: str | Callable[[Any], str],
|
106
|
+
# content: Callable[[Any], RenderableType],
|
107
|
+
# ):
|
108
|
+
# return lambda x: __print_code_preview_banner(
|
109
|
+
# title=title if isinstance(title, str) else title(x),
|
110
|
+
# content=content(x),
|
111
|
+
# short=short if isinstance(short, str) else short(x),
|
112
|
+
# )
|
113
|
+
|
114
|
+
|
115
|
+
def code_result_panel(
|
116
|
+
title: str,
|
117
|
+
out: str | None = None,
|
118
|
+
err: str | None = None,
|
119
|
+
):
|
120
|
+
if CLI_OPTIONS.quiet:
|
121
|
+
return
|
122
|
+
print()
|
123
|
+
if isinstance(out, str):
|
124
|
+
out = out.strip()
|
125
|
+
if isinstance(err, str):
|
126
|
+
err = err.strip()
|
127
|
+
if not out and not err:
|
128
|
+
rich.print(title)
|
129
|
+
else:
|
130
|
+
text = out if out else ""
|
131
|
+
text += (("\n---\n" if out else "") + err) if err else ""
|
132
|
+
panel = Panel.fit(text, title=title, title_align="left", style="dim")
|
133
|
+
rich.print(panel)
|
134
|
+
|
135
|
+
|
136
|
+
from . import calc
|
137
|
+
from . import clock
|
138
|
+
from . import code
|
139
|
+
from . import search
|
140
|
+
from . import web
|
141
|
+
from . import cli
|
142
|
+
|
143
|
+
|
144
|
+
def create_plugins():
|
145
|
+
"""Get all plugins in the autosh.plugins module."""
|
146
|
+
cfgs = CONFIG.plugins
|
147
|
+
plugins = []
|
148
|
+
if cfgs.calc is not None:
|
149
|
+
plugins.append(calc.CalculatorPlugin(cfgs.calc.model_dump()))
|
150
|
+
if cfgs.cli is not None:
|
151
|
+
plugins.append(cli.CLIPlugin(cfgs.cli.model_dump()))
|
152
|
+
if cfgs.clock is not None:
|
153
|
+
plugins.append(clock.ClockPlugin(cfgs.clock.model_dump()))
|
154
|
+
if cfgs.code is not None:
|
155
|
+
plugins.append(code.CodePlugin(cfgs.code.model_dump()))
|
156
|
+
if cfgs.search is not None:
|
157
|
+
plugins.append(search.SearchPlugin(cfgs.search.model_dump()))
|
158
|
+
if cfgs.web is not None:
|
159
|
+
plugins.append(web.WebPlugin(cfgs.web.model_dump()))
|
160
|
+
return plugins
|
@@ -1,16 +1,12 @@
|
|
1
1
|
from agentia.plugins import tool, Plugin
|
2
2
|
from typing import Annotated
|
3
|
-
from . import
|
3
|
+
from . import Banner
|
4
4
|
|
5
5
|
|
6
6
|
class CalculatorPlugin(Plugin):
|
7
7
|
NAME = "calc"
|
8
8
|
|
9
|
-
@tool(
|
10
|
-
metadata={
|
11
|
-
"banner": simple_banner("CALC", dim=lambda a: a.get("expression", ""))
|
12
|
-
}
|
13
|
-
)
|
9
|
+
@tool(metadata={"banner": Banner("CALC", text_key="expression")})
|
14
10
|
def evaluate(
|
15
11
|
self,
|
16
12
|
expression: Annotated[
|
@@ -8,7 +8,7 @@ from enum import StrEnum
|
|
8
8
|
|
9
9
|
from autosh.config import CLI_OPTIONS
|
10
10
|
|
11
|
-
from . import code_result_panel,
|
11
|
+
from . import code_result_panel, Banner
|
12
12
|
|
13
13
|
|
14
14
|
class Color(StrEnum):
|
@@ -59,7 +59,7 @@ class CLIPlugin(Plugin):
|
|
59
59
|
rich.print(text, file=sys.stderr if stderr else sys.stdout, end=end)
|
60
60
|
return "DONE. You can continue and no need to repeat the text"
|
61
61
|
|
62
|
-
@tool(metadata={"banner":
|
62
|
+
@tool(metadata={"banner": Banner("CWD", text_key="path")})
|
63
63
|
def chdir(self, path: Annotated[str, "The path to the new working directory"]):
|
64
64
|
"""
|
65
65
|
Changes the current working directory of the terminal to another directory.
|
@@ -69,7 +69,7 @@ class CLIPlugin(Plugin):
|
|
69
69
|
os.chdir(path)
|
70
70
|
return f"DONE"
|
71
71
|
|
72
|
-
@tool(metadata={"banner":
|
72
|
+
@tool(metadata={"banner": Banner("GET ARGV")})
|
73
73
|
def get_argv(self):
|
74
74
|
"""
|
75
75
|
Get the command line arguments.
|
@@ -81,7 +81,7 @@ class CLIPlugin(Plugin):
|
|
81
81
|
"args": CLI_OPTIONS.args,
|
82
82
|
}
|
83
83
|
|
84
|
-
@tool(metadata={"banner":
|
84
|
+
@tool(metadata={"banner": Banner("GET ENV", text_key="key")})
|
85
85
|
def get_env(self, key: Annotated[str, "The environment variable to get"]):
|
86
86
|
"""
|
87
87
|
Get an environment variable.
|
@@ -90,7 +90,7 @@ class CLIPlugin(Plugin):
|
|
90
90
|
raise KeyError(f"Environment variable `{key}` does not exist.")
|
91
91
|
return os.environ[key]
|
92
92
|
|
93
|
-
@tool(metadata={"banner":
|
93
|
+
@tool(metadata={"banner": Banner("GET ALL ENVS")})
|
94
94
|
def get_all_envs(self):
|
95
95
|
"""
|
96
96
|
Get all environment variables.
|
@@ -102,10 +102,13 @@ class CLIPlugin(Plugin):
|
|
102
102
|
|
103
103
|
@tool(
|
104
104
|
metadata={
|
105
|
-
"banner":
|
106
|
-
|
107
|
-
text=lambda a:
|
108
|
-
|
105
|
+
"banner": Banner(
|
106
|
+
title=lambda a: "SET ENV" if a.get("value") else "DEL ENV",
|
107
|
+
text=lambda a: (
|
108
|
+
f"{a.get("key", "")} = {a.get("value", "")}"
|
109
|
+
if a.get("value")
|
110
|
+
else a.get("key", "")
|
111
|
+
),
|
109
112
|
),
|
110
113
|
}
|
111
114
|
)
|
@@ -127,7 +130,7 @@ class CLIPlugin(Plugin):
|
|
127
130
|
os.environ[key] = value
|
128
131
|
return f"DONE"
|
129
132
|
|
130
|
-
@tool(metadata={"banner":
|
133
|
+
@tool(metadata={"banner": Banner("READ", text_key="path")})
|
131
134
|
def read(
|
132
135
|
self,
|
133
136
|
path: Annotated[str, "The path to the file to read"],
|
@@ -145,10 +148,9 @@ class CLIPlugin(Plugin):
|
|
145
148
|
|
146
149
|
@tool(
|
147
150
|
metadata={
|
148
|
-
"banner":
|
149
|
-
|
150
|
-
text=lambda a: a.get("path", ""),
|
151
|
-
dim=lambda a: f"({len(a.get('content', ''))} bytes)",
|
151
|
+
"banner": Banner(
|
152
|
+
title=lambda a: "WRITE" if not a.get("append") else "APPEND",
|
153
|
+
text=lambda a: f"{a.get("path", "")} ({len(a.get('content', ''))} bytes)",
|
152
154
|
),
|
153
155
|
}
|
154
156
|
)
|
@@ -210,10 +212,11 @@ class CLIPlugin(Plugin):
|
|
210
212
|
|
211
213
|
@tool(
|
212
214
|
metadata={
|
213
|
-
"banner":
|
215
|
+
"banner": Banner(
|
214
216
|
title="Run Command",
|
215
|
-
|
216
|
-
|
217
|
+
text_key="command",
|
218
|
+
code=lambda a: f"[magenta][bold]➜[/bold] [italic]{a.get("command", "")}[/italic][/magenta]\n\n[dim]{a.get("explanation", "")}[/dim]",
|
219
|
+
user_consent=True,
|
217
220
|
)
|
218
221
|
}
|
219
222
|
)
|
@@ -264,10 +267,16 @@ class CLIPlugin(Plugin):
|
|
264
267
|
}
|
265
268
|
return result
|
266
269
|
|
267
|
-
@tool(metadata={"banner":
|
268
|
-
def exit(
|
270
|
+
@tool(metadata={"banner": Banner("EXIT")})
|
271
|
+
def exit(
|
272
|
+
self,
|
273
|
+
exitcode: Annotated[int, "The exit code of this shell session"] = 0,
|
274
|
+
reason: Annotated[str | None, "The reason for exiting"] = None,
|
275
|
+
):
|
269
276
|
"""
|
270
277
|
Exit the current shell session with an optional exit code.
|
271
278
|
"""
|
279
|
+
if reason and exitcode != 0:
|
280
|
+
rich.print(f"\n[bold red]ABORT: {reason}[/bold red]")
|
272
281
|
sys.exit(exitcode)
|
273
282
|
return f"EXITED with code {exitcode}"
|
@@ -2,11 +2,11 @@ import datetime
|
|
2
2
|
from agentia.plugins import tool, Plugin
|
3
3
|
import tzlocal
|
4
4
|
|
5
|
-
from autosh.plugins import
|
5
|
+
from autosh.plugins import Banner
|
6
6
|
|
7
7
|
|
8
8
|
class ClockPlugin(Plugin):
|
9
|
-
@tool(metadata={"banner":
|
9
|
+
@tool(metadata={"banner": Banner("GET TIME")})
|
10
10
|
def get_current_time(self):
|
11
11
|
"""Get the current UTC time in ISO format"""
|
12
12
|
utc = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
@@ -5,7 +5,7 @@ from rich.syntax import Syntax
|
|
5
5
|
from rich.console import group
|
6
6
|
from contextlib import redirect_stdout, redirect_stderr
|
7
7
|
import io
|
8
|
-
from . import
|
8
|
+
from . import Banner, code_result_panel
|
9
9
|
|
10
10
|
|
11
11
|
@group()
|
@@ -18,10 +18,10 @@ def code_with_explanation(code: str, explanation: str):
|
|
18
18
|
class CodePlugin(Plugin):
|
19
19
|
@tool(
|
20
20
|
metadata={
|
21
|
-
"banner":
|
22
|
-
title="Run Python",
|
23
|
-
|
24
|
-
|
21
|
+
"banner": Banner(
|
22
|
+
title="Run Python Code",
|
23
|
+
text_key="explanation",
|
24
|
+
code=lambda a: code_with_explanation(
|
25
25
|
a.get("python_code", ""), a.get("explanation", "")
|
26
26
|
),
|
27
27
|
)
|
@@ -31,7 +31,8 @@ class CodePlugin(Plugin):
|
|
31
31
|
self,
|
32
32
|
python_code: Annotated[str, "The python code to run."],
|
33
33
|
explanation: Annotated[
|
34
|
-
str,
|
34
|
+
str,
|
35
|
+
"Briefly explain what this code does, and what are you going to use it for.",
|
35
36
|
],
|
36
37
|
):
|
37
38
|
"""
|
@@ -3,7 +3,7 @@ from typing import Annotated, override
|
|
3
3
|
import os
|
4
4
|
from tavily import TavilyClient
|
5
5
|
|
6
|
-
from autosh.plugins import
|
6
|
+
from autosh.plugins import Banner
|
7
7
|
|
8
8
|
|
9
9
|
class SearchPlugin(Plugin):
|
@@ -17,11 +17,7 @@ class SearchPlugin(Plugin):
|
|
17
17
|
raise ValueError("Please set the TAVILY_API_KEY environment variable.")
|
18
18
|
self.__tavily = TavilyClient(api_key=key)
|
19
19
|
|
20
|
-
@tool(
|
21
|
-
metadata={
|
22
|
-
"banner": simple_banner("WEB SEARCH", dim=lambda a: a.get("query", "")),
|
23
|
-
}
|
24
|
-
)
|
20
|
+
@tool(metadata={"banner": Banner("WEB SEARCH", text_key="query")})
|
25
21
|
async def web_search(
|
26
22
|
self,
|
27
23
|
query: Annotated[
|
@@ -43,11 +39,7 @@ class SearchPlugin(Plugin):
|
|
43
39
|
)
|
44
40
|
return tavily_results
|
45
41
|
|
46
|
-
@tool(
|
47
|
-
metadata={
|
48
|
-
"banner": simple_banner("NEWS SEARCH", dim=lambda a: a.get("query", "")),
|
49
|
-
}
|
50
|
-
)
|
42
|
+
@tool(metadata={"banner": Banner("NEWS SEARCH", text_key="query")})
|
51
43
|
async def news_search(
|
52
44
|
self,
|
53
45
|
query: Annotated[
|
@@ -70,11 +62,7 @@ class SearchPlugin(Plugin):
|
|
70
62
|
)
|
71
63
|
return tavily_results
|
72
64
|
|
73
|
-
@tool(
|
74
|
-
metadata={
|
75
|
-
"banner": simple_banner("FINANCE SEARCH", dim=lambda a: a.get("query", ""))
|
76
|
-
}
|
77
|
-
)
|
65
|
+
@tool(metadata={"banner": Banner("FINANCE SEARCH", text_key="query")})
|
78
66
|
async def finance_search(
|
79
67
|
self,
|
80
68
|
query: Annotated[
|
@@ -7,7 +7,7 @@ from markdownify import markdownify
|
|
7
7
|
import uuid
|
8
8
|
from tavily import TavilyClient
|
9
9
|
|
10
|
-
from autosh.plugins import
|
10
|
+
from autosh.plugins import Banner
|
11
11
|
|
12
12
|
|
13
13
|
class WebPlugin(Plugin):
|
@@ -47,7 +47,7 @@ class WebPlugin(Plugin):
|
|
47
47
|
md = markdownify(res.text)
|
48
48
|
return {"content": md}
|
49
49
|
|
50
|
-
@tool(metadata={"banner":
|
50
|
+
@tool(metadata={"banner": Banner("BROWSE", text_key="url")})
|
51
51
|
def get_webpage_content(
|
52
52
|
self,
|
53
53
|
url: Annotated[str, "The URL of the web page to get the content of"],
|
@@ -1,4 +1,6 @@
|
|
1
|
+
from io import StringIO
|
1
2
|
from pathlib import Path
|
3
|
+
import socket
|
2
4
|
import sys
|
3
5
|
from agentia import (
|
4
6
|
Agent,
|
@@ -14,10 +16,11 @@ from neongrid.loading import Loading
|
|
14
16
|
|
15
17
|
from autosh.config import CLI_OPTIONS, CONFIG
|
16
18
|
import neongrid as ng
|
17
|
-
from .plugins import create_plugins
|
19
|
+
from .plugins import Banner, create_plugins
|
18
20
|
import rich
|
19
21
|
import platform
|
20
22
|
from rich.prompt import Confirm
|
23
|
+
import os
|
21
24
|
|
22
25
|
|
23
26
|
INSTRUCTIONS = f"""
|
@@ -158,34 +161,39 @@ class Session:
|
|
158
161
|
prompt = f.read()
|
159
162
|
await self.exec_prompt(prompt)
|
160
163
|
|
161
|
-
async def __process_event(self, e: Event):
|
164
|
+
async def __process_event(self, e: Event, first: bool, repl: bool):
|
165
|
+
prefix_newline = repl or not first
|
162
166
|
if isinstance(e, UserConsentEvent):
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
if CLI_OPTIONS.yes:
|
167
|
+
if CLI_OPTIONS.yes:
|
168
|
+
e.response = True
|
169
|
+
return False
|
170
|
+
if prefix_newline:
|
171
|
+
print()
|
172
|
+
e.response = ng.confirm(e.message)
|
170
173
|
return True
|
171
|
-
result
|
172
|
-
|
173
|
-
|
174
|
-
|
174
|
+
if isinstance(e, ToolCallEvent) and e.result is None:
|
175
|
+
if (banner := (e.metadata or {}).get("banner")) and isinstance(
|
176
|
+
banner, Banner
|
177
|
+
):
|
178
|
+
return banner.render(e.arguments, prefix_newline=prefix_newline)
|
179
|
+
return False
|
175
180
|
|
176
181
|
async def __process_run(
|
177
182
|
self, run: Run[Event | MessageStream], loading: Loading | None, repl: bool
|
178
183
|
):
|
184
|
+
first = True
|
179
185
|
async for e in run:
|
180
186
|
if loading:
|
181
187
|
await loading.finish()
|
182
188
|
|
183
189
|
if isinstance(e, Event):
|
184
|
-
await self.__process_event(e)
|
190
|
+
if await self.__process_event(e, first=first, repl=repl):
|
191
|
+
first = False
|
185
192
|
else:
|
186
|
-
if repl or not
|
193
|
+
if repl or not first:
|
187
194
|
print()
|
188
195
|
await self.__render_streamed_markdown(e)
|
196
|
+
first = False
|
189
197
|
|
190
198
|
if loading:
|
191
199
|
loading = self.__create_loading_indicator()
|
@@ -193,16 +201,47 @@ class Session:
|
|
193
201
|
if loading:
|
194
202
|
await loading.finish()
|
195
203
|
|
204
|
+
def __get_input_prompt(self):
|
205
|
+
cwd = Path.cwd()
|
206
|
+
relative_to_home = False
|
207
|
+
if cwd.is_relative_to(Path.home()):
|
208
|
+
cwd = cwd.relative_to(Path.home())
|
209
|
+
relative_to_home = True
|
210
|
+
# short cwd
|
211
|
+
short_cwd = "/" if not relative_to_home else "~/"
|
212
|
+
parts = []
|
213
|
+
for i, p in enumerate(cwd.parts):
|
214
|
+
if i == 0 and p == "/":
|
215
|
+
continue
|
216
|
+
if i != len(cwd.parts) - 1:
|
217
|
+
parts.append(p[0])
|
218
|
+
else:
|
219
|
+
parts.append(p)
|
220
|
+
short_cwd += "/".join(parts)
|
221
|
+
cwd = str(cwd) if not relative_to_home else "~/" + str(cwd)
|
222
|
+
host = socket.gethostname()
|
223
|
+
user = os.getlogin()
|
224
|
+
prompt = CONFIG.repl_prompt.format(
|
225
|
+
cwd=cwd,
|
226
|
+
short_cwd=short_cwd,
|
227
|
+
host=host,
|
228
|
+
user=user,
|
229
|
+
)
|
230
|
+
return prompt
|
231
|
+
|
196
232
|
async def run_repl(self):
|
233
|
+
if CONFIG.repl_banner:
|
234
|
+
rich.print(CONFIG.repl_banner)
|
197
235
|
first = True
|
198
236
|
while True:
|
199
237
|
try:
|
200
238
|
if not first:
|
201
239
|
print()
|
202
240
|
first = False
|
203
|
-
|
204
|
-
|
205
|
-
|
241
|
+
input_prompt = self.__get_input_prompt()
|
242
|
+
rich.print(input_prompt, end="", flush=True)
|
243
|
+
prompt = await ng.input("", sync=False, persist="/tmp/autosh-history")
|
244
|
+
prompt = prompt.strip()
|
206
245
|
if prompt in ["exit", "quit"]:
|
207
246
|
break
|
208
247
|
if len(prompt) == 0:
|
@@ -1,101 +0,0 @@
|
|
1
|
-
from typing import Any, Callable
|
2
|
-
import rich
|
3
|
-
from rich.prompt import Confirm
|
4
|
-
from rich.panel import Panel
|
5
|
-
from rich.console import RenderableType
|
6
|
-
from autosh.config import CLI_OPTIONS, CONFIG
|
7
|
-
|
8
|
-
|
9
|
-
def __print_simple_banner(tag: str, text: str | None = None, dim: str | None = None):
|
10
|
-
if CLI_OPTIONS.quiet:
|
11
|
-
return
|
12
|
-
s = f"\n[bold on magenta] {tag} [/bold on magenta]"
|
13
|
-
if text:
|
14
|
-
s += f" [italic magenta]{text}[/italic magenta]"
|
15
|
-
if dim:
|
16
|
-
s += f" [italic dim]{dim}[/italic dim]"
|
17
|
-
rich.print(s)
|
18
|
-
|
19
|
-
|
20
|
-
def simple_banner(
|
21
|
-
tag: str | Callable[[Any], str],
|
22
|
-
text: Callable[[Any], str] | None = None,
|
23
|
-
dim: Callable[[Any], str] | None = None,
|
24
|
-
):
|
25
|
-
return lambda x: __print_simple_banner(
|
26
|
-
tag if isinstance(tag, str) else tag(x),
|
27
|
-
text(x) if text else None,
|
28
|
-
dim(x) if dim else None,
|
29
|
-
)
|
30
|
-
|
31
|
-
|
32
|
-
def __print_code_preview_banner(
|
33
|
-
title: str, content: RenderableType, short: str | None = None
|
34
|
-
):
|
35
|
-
if CLI_OPTIONS.quiet:
|
36
|
-
if short and not CLI_OPTIONS.yes:
|
37
|
-
rich.print(f"\n[magenta]{short}[/magenta]\n")
|
38
|
-
return
|
39
|
-
panel = Panel.fit(content, title=f"[magenta]{title}[/magenta]", title_align="left")
|
40
|
-
rich.print()
|
41
|
-
rich.print(panel)
|
42
|
-
|
43
|
-
|
44
|
-
def code_preview_banner(
|
45
|
-
title: str | Callable[[Any], str],
|
46
|
-
short: str | Callable[[Any], str],
|
47
|
-
content: Callable[[Any], RenderableType],
|
48
|
-
):
|
49
|
-
return lambda x: __print_code_preview_banner(
|
50
|
-
title=title if isinstance(title, str) else title(x),
|
51
|
-
content=content(x),
|
52
|
-
short=short if isinstance(short, str) else short(x),
|
53
|
-
)
|
54
|
-
|
55
|
-
|
56
|
-
def code_result_panel(
|
57
|
-
title: str,
|
58
|
-
out: str | None = None,
|
59
|
-
err: str | None = None,
|
60
|
-
):
|
61
|
-
if CLI_OPTIONS.quiet:
|
62
|
-
return
|
63
|
-
print()
|
64
|
-
if isinstance(out, str):
|
65
|
-
out = out.strip()
|
66
|
-
if isinstance(err, str):
|
67
|
-
err = err.strip()
|
68
|
-
if not out and not err:
|
69
|
-
rich.print(title)
|
70
|
-
else:
|
71
|
-
text = out if out else ""
|
72
|
-
text += (("\n---\n" if out else "") + err) if err else ""
|
73
|
-
panel = Panel.fit(text, title=title, title_align="left", style="dim")
|
74
|
-
rich.print(panel)
|
75
|
-
|
76
|
-
|
77
|
-
from . import calc
|
78
|
-
from . import clock
|
79
|
-
from . import code
|
80
|
-
from . import search
|
81
|
-
from . import web
|
82
|
-
from . import cli
|
83
|
-
|
84
|
-
|
85
|
-
def create_plugins():
|
86
|
-
"""Get all plugins in the autosh.plugins module."""
|
87
|
-
cfgs = CONFIG.plugins
|
88
|
-
plugins = []
|
89
|
-
if cfgs.calc is not None:
|
90
|
-
plugins.append(calc.CalculatorPlugin(cfgs.calc.model_dump()))
|
91
|
-
if cfgs.cli is not None:
|
92
|
-
plugins.append(cli.CLIPlugin(cfgs.cli.model_dump()))
|
93
|
-
if cfgs.clock is not None:
|
94
|
-
plugins.append(clock.ClockPlugin(cfgs.clock.model_dump()))
|
95
|
-
if cfgs.code is not None:
|
96
|
-
plugins.append(code.CodePlugin(cfgs.code.model_dump()))
|
97
|
-
if cfgs.search is not None:
|
98
|
-
plugins.append(search.SearchPlugin(cfgs.search.model_dump()))
|
99
|
-
if cfgs.web is not None:
|
100
|
-
plugins.append(web.WebPlugin(cfgs.web.model_dump()))
|
101
|
-
return plugins
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|