autosh 0.0.3__py3-none-any.whl → 0.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
autosh/session.py CHANGED
@@ -1,17 +1,24 @@
1
1
  import asyncio
2
2
  from pathlib import Path
3
3
  import sys
4
- from agentia import Agent
5
- from agentia.chat_completion import MessageStream
6
- from agentia.message import UserMessage
4
+ from agentia import (
5
+ Agent,
6
+ UserMessage,
7
+ Event,
8
+ ToolCallEvent,
9
+ MessageStream,
10
+ ChatCompletion,
11
+ UserConsentEvent,
12
+ )
7
13
  from agentia.plugins import PluginInitError
14
+ from neongrid.loading import Loading
8
15
 
9
16
  from autosh.config import CLI_OPTIONS, CONFIG
10
- from autosh.md import stream_md
17
+ import neongrid as ng
11
18
  from .plugins import create_plugins
12
19
  import rich
13
20
  import platform
14
- from rich.prompt import Prompt
21
+ from rich.prompt import Confirm
15
22
 
16
23
 
17
24
  INSTRUCTIONS = f"""
@@ -79,13 +86,13 @@ class Session:
79
86
  * -h, --help Show this message and exit.
80
87
  """,
81
88
  )
82
- agent.history.add(self._get_argv_message())
89
+ agent.history.add(self.__get_argv_message())
83
90
  completion = agent.chat_completion(prompt, stream=True)
84
91
  async for stream in completion:
85
92
  await self.__render_streamed_markdown(stream)
86
93
  sys.exit(0)
87
94
 
88
- def _get_argv_message(self):
95
+ def __get_argv_message(self):
89
96
  args = str(CLI_OPTIONS.args)
90
97
  if not CLI_OPTIONS.script:
91
98
  cmd = Path(sys.argv[0]).name
@@ -112,7 +119,7 @@ class Session:
112
119
  # Execute the prompt
113
120
  loading = self.__create_loading_indicator()
114
121
  CLI_OPTIONS.prompt = prompt
115
- self.agent.history.add(self._get_argv_message())
122
+ self.agent.history.add(self.__get_argv_message())
116
123
  if CLI_OPTIONS.stdin_has_data():
117
124
  self.agent.history.add(
118
125
  UserMessage(
@@ -134,13 +141,8 @@ class Session:
134
141
  role="user",
135
142
  )
136
143
  )
137
- completion = self.agent.chat_completion(prompt, stream=True)
138
- async for stream in completion:
139
- if not loading:
140
- loading = self.__create_loading_indicator()
141
- if await self.__render_streamed_markdown(stream, loading=loading):
142
- print()
143
- loading = None
144
+ completion = self.agent.chat_completion(prompt, stream=True, events=True)
145
+ await self.__process_completion(completion, loading)
144
146
 
145
147
  async def exec_from_stdin(self):
146
148
  if sys.stdin.isatty():
@@ -157,104 +159,65 @@ class Session:
157
159
  prompt = f.read()
158
160
  await self.exec_prompt(prompt)
159
161
 
162
+ async def __process_event(self, e: Event):
163
+ if isinstance(e, UserConsentEvent):
164
+ e.response = await self.__confirm(e.message)
165
+ if isinstance(e, ToolCallEvent) and e.result is None:
166
+ if banner := (e.metadata or {}).get("banner"):
167
+ banner(e.arguments)
168
+
169
+ async def __confirm(self, message: str) -> bool:
170
+ if CLI_OPTIONS.yes:
171
+ return True
172
+ result = Confirm.ask(
173
+ f"[magenta]{message}[/magenta]", default=True, case_sensitive=False
174
+ )
175
+ if not CLI_OPTIONS.quiet:
176
+ rich.print()
177
+ return result
178
+
179
+ async def __process_completion(
180
+ self, completion: ChatCompletion[Event | MessageStream], loading: Loading
181
+ ):
182
+ async for stream in completion:
183
+ await loading.finish()
184
+
185
+ if isinstance(stream, Event):
186
+ await self.__process_event(stream)
187
+ else:
188
+ print()
189
+ await self.__render_streamed_markdown(stream)
190
+ print()
191
+
192
+ loading = self.__create_loading_indicator()
193
+
194
+ await loading.finish()
195
+
160
196
  async def run_repl(self):
161
- console = rich.console.Console()
162
197
  while True:
163
198
  try:
164
- prompt = console.input("[bold blue]>[/bold blue] ").strip()
199
+ prompt = (
200
+ await ng.input("> ", sync=False, persist="/tmp/autosh-history")
201
+ ).strip()
165
202
  if prompt in ["exit", "quit"]:
166
203
  break
167
204
  if len(prompt) == 0:
168
205
  continue
169
- loading = self.__create_loading_indicator(newline=True)
170
- completion = self.agent.chat_completion(prompt, stream=True)
171
- async for stream in completion:
172
- if not loading:
173
- loading = self.__create_loading_indicator()
174
- if await self.__render_streamed_markdown(stream, loading=loading):
175
- print()
176
- loading = None
206
+ loading = self.__create_loading_indicator()
207
+ completion = self.agent.chat_completion(
208
+ prompt, stream=True, events=True
209
+ )
210
+ await self.__process_completion(completion, loading)
177
211
  except KeyboardInterrupt:
178
212
  break
179
213
 
180
- def __create_loading_indicator(self, newline: bool = False):
181
- return (
182
- asyncio.create_task(self.__loading(newline))
183
- if sys.stdout.isatty()
184
- else None
185
- )
186
-
187
- async def __loading(self, newline: bool = False):
188
- chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
189
- char_width = 1
190
- msg = "Loading..."
191
- count = 0
192
- print("\x1b[2m", end="", flush=True)
193
- while True:
194
- try:
195
- print(chars[count], end="", flush=True)
196
- print(" " + msg, end="", flush=True)
197
- count += 1
198
- await asyncio.sleep(0.1)
199
- length = char_width + len(msg) + 1
200
- print("\b" * length, end="", flush=True)
201
- print(" " * length, end="", flush=True)
202
- print("\b" * length, end="", flush=True)
203
- if count == len(chars):
204
- count = 0
205
- except asyncio.CancelledError:
206
- length = char_width + len(msg) + 1
207
- print("\b" * length, end="", flush=True)
208
- print(" " * length, end="", flush=True)
209
- print("\b" * length, end="", flush=True)
210
- print("\x1b[0m", end="", flush=True)
211
- if newline:
212
- print()
213
- break
214
+ def __create_loading_indicator(self):
215
+ return ng.loading.kana()
214
216
 
215
- async def __render_streamed_markdown(
216
- self, stream: MessageStream, loading: asyncio.Task[None] | None = None
217
- ):
217
+ async def __render_streamed_markdown(self, stream: MessageStream):
218
218
  if sys.stdout.isatty():
219
- # buffer first few chars so we don't need to launch glow if there is no output
220
- chunks = aiter(stream)
221
- buf = ""
222
- while len(buf) < 8:
223
- try:
224
- buf += await anext(chunks)
225
- except StopAsyncIteration:
226
- if len(buf) == 0:
227
- if loading:
228
- loading.cancel()
229
- await loading
230
- return False
231
- break
232
- if loading:
233
- loading.cancel()
234
- await loading
235
-
236
- content = {"v": ""}
237
-
238
- async def gen():
239
- content["v"] = buf
240
- if buf:
241
- yield buf
242
- while True:
243
- try:
244
- s = await anext(chunks)
245
- content["v"] += s
246
- for c in s:
247
- yield c
248
- except StopAsyncIteration:
249
- break
250
-
251
- await stream_md(gen())
219
+ await ng.stream.markdown(aiter(stream))
252
220
  return True
253
221
  else:
254
- has_content = False
255
222
  async for chunk in stream:
256
- if chunk == "":
257
- continue
258
- has_content = True
259
223
  print(chunk, end="", flush=True)
260
- return has_content
@@ -1,12 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autosh
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: Add your description here
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.13
7
- Requires-Dist: agentia>=0.0.5
7
+ Requires-Dist: agentia>=0.0.7
8
8
  Requires-Dist: asyncio>=3.4.3
9
9
  Requires-Dist: markdownify>=1.1.0
10
+ Requires-Dist: neongrid
11
+ Requires-Dist: prompt-toolkit>=3.0.51
10
12
  Requires-Dist: pydantic>=2.11.3
11
13
  Requires-Dist: python-dotenv>=1.1.0
12
14
  Requires-Dist: rich>=14.0.0
@@ -0,0 +1,17 @@
1
+ autosh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ autosh/config-template.toml,sha256=iLdCBHIK0czWgNHtwAgvuhV670aiNc-IOmaPHP12i0Y,269
3
+ autosh/config.py,sha256=7DXUSsqHm8dPsXln-NTu8D0_Uwcs2h4sT8pFtQM8tso,2654
4
+ autosh/main.py,sha256=bgWkYpU9cmyU2AyWt4o8Mcj6LH_UGYSYrFx4Qkl8vqk,5117
5
+ autosh/session.py,sha256=fOJySGU9abjFReNOyvQf-s9rFQdGy8Pv01GgplqrRuw,7943
6
+ autosh/plugins/__init__.py,sha256=oUtO7M1AuZKshbaS-kO0fbV4CYal0uOvg-GpYVFE16I,2996
7
+ autosh/plugins/calc.py,sha256=x_fZW3kkStlAAQEhejcD2_MQnpqCd5ghU72CNSjqIsg,672
8
+ autosh/plugins/cli.py,sha256=G4WqTWeCi7OxdB36lAUiR1OBdtSvxmKRcQGGLdMcxms,9230
9
+ autosh/plugins/clock.py,sha256=gIrYBz3GmnAJTujzd3B8ewuThXpF-OvWYBugcZMYGTQ,570
10
+ autosh/plugins/code.py,sha256=HK7cd7gNit2yZAukaCnTGXb6BAh31cPh0EHCx6msOWc,2457
11
+ autosh/plugins/search.py,sha256=aWbIDJ7E5m-Xrw2A1DB6w53oGiFe1Uc3EuZikIH5eTs,2984
12
+ autosh/plugins/web.py,sha256=LxE7ed7IS9B4spWCm-3aBMLfC9ghcNnmJzMiD6ayLdc,2648
13
+ autosh-0.0.4.dist-info/METADATA,sha256=cv69lLqKHEFwz5AZWEmmL0scvhzYv8-ECPdEiRzOaus,1959
14
+ autosh-0.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ autosh-0.0.4.dist-info/entry_points.txt,sha256=BV7bzUnxG6Z5InEkrfajGCxjooYORC5tZDDZctOPenQ,67
16
+ autosh-0.0.4.dist-info/licenses/LICENSE,sha256=BnLDJsIJe-Dm18unR9DOoSv7QOfAz6LeIQc1yHAjxp0,1066
17
+ autosh-0.0.4.dist-info/RECORD,,
autosh/md/__init__.py DELETED
@@ -1,8 +0,0 @@
1
- from typing import AsyncGenerator
2
-
3
- from autosh.md.printer import StreamedMarkdownPrinter
4
-
5
-
6
- async def stream_md(stream: AsyncGenerator[str, None]):
7
- mp = StreamedMarkdownPrinter(stream)
8
- await mp.parse_doc()
autosh/md/inline_text.py DELETED
@@ -1,214 +0,0 @@
1
- from dataclasses import dataclass, field
2
- from autosh.md.printer import StreamedMarkdownPrinter
3
- from autosh.md.state import State
4
-
5
-
6
- @dataclass
7
- class Keyword:
8
- token: str
9
-
10
- def is_bold(self) -> bool:
11
- return self.token[0] in ["*", "_"] and len(self.token) == 2
12
-
13
- def is_italic(self) -> bool:
14
- return self.token[0] in ["*", "_"] and len(self.token) == 1
15
-
16
- def is_bold_and_italic(self) -> bool:
17
- return self.token[0] in ["*", "_"] and len(self.token) > 2
18
-
19
- def is_bold_or_italic(self) -> bool:
20
- return self.token[0] in ["*", "_"]
21
-
22
-
23
- class InvalidState(Exception):
24
- def __init__(self, token: str):
25
- super().__init__(token)
26
- self.token = token
27
-
28
-
29
- @dataclass
30
- class InlineScope:
31
- state: State
32
-
33
- stack: list[tuple[Keyword, State]] = field(default_factory=list)
34
-
35
- @property
36
- def last(self) -> str | None:
37
- return self.stack[-1][0].token if len(self.stack) > 0 else None
38
-
39
- @property
40
- def strike(self) -> bool:
41
- return self.last == "~~"
42
-
43
- @property
44
- def code(self) -> bool:
45
- return self.last == "`"
46
-
47
- def enter(self, kind: Keyword):
48
- match kind:
49
- case "`":
50
- state = self.state._enter_scope(dim=True)
51
- case "~~":
52
- state = self.state._enter_scope(strike=True)
53
- case _ if kind.is_bold():
54
- state = self.state._enter_scope(bold=True)
55
- case _ if kind.is_italic():
56
- state = self.state._enter_scope(italic=True)
57
- case _ if kind.is_bold_and_italic():
58
- state = self.state._enter_scope(bold=True, italic=True)
59
- case _:
60
- state = self.state._enter_scope()
61
- self.stack.append((kind, state))
62
-
63
- def exit(self):
64
- t = self.stack.pop()
65
- state = t[1]
66
- self.state._exit_scope(state)
67
-
68
-
69
- class InlineTextPrinter:
70
- def __init__(self, p: StreamedMarkdownPrinter, table: bool = False):
71
- self.p = p
72
- self.terminator = ["\n", None]
73
- if table:
74
- self.terminator.append("|")
75
-
76
- def emit(self, s: str):
77
- self.p.emit(s)
78
-
79
- def peek(self):
80
- return self.p.peek()
81
-
82
- async def consume(self, n: int = 1):
83
- return await self.p.consume(n)
84
-
85
- async def check(self, s: str, eof: bool | None = None) -> bool:
86
- return await self.p.check(s, eof)
87
-
88
- async def parse_inline_unformatted(self):
89
- # Parse until another "`" or a newline, or EOF
90
- while not self.peek() in self.terminator:
91
- c = await self.p.consume()
92
- self.emit(c)
93
-
94
- async def parse_inline_code(self):
95
- self.emit("`")
96
- # Parse until another "`" or a newline, or EOF
97
- while not await self.check("`"):
98
- c = await self.p.consume()
99
- if c in self.terminator:
100
- return
101
- self.emit(c)
102
- if c == "\\":
103
- c = await self.p.consume()
104
- if c in self.terminator:
105
- return
106
- self.emit(c)
107
- self.emit("`")
108
- await self.p.consume()
109
-
110
- async def get_next_token(self) -> str | Keyword | None:
111
- c = self.peek()
112
- if c in self.terminator:
113
- return None
114
- if c == "`":
115
- await self.consume()
116
- return Keyword("`")
117
- if c == "*":
118
- s = ""
119
- while self.peek() == "*":
120
- s += c
121
- await self.consume()
122
- return Keyword(s)
123
- if c == "_":
124
- s = ""
125
- while self.peek() == "_":
126
- s += c
127
- await self.consume()
128
- return Keyword(s)
129
- if c == "~" and await self.check("~~"):
130
- s = ""
131
- while self.peek() == "~":
132
- s += c
133
- await self.consume()
134
- return Keyword(s)
135
- if c == " " or c == "\t":
136
- s = ""
137
- while self.peek() == " " or self.peek() == "\t":
138
- s += c
139
- await self.consume()
140
- return s
141
- s = c or ""
142
- if c == "\\":
143
- await self.consume()
144
- c = self.peek()
145
- if c in self.terminator:
146
- return None if len(s) == 0 else s
147
- s += c or ""
148
- await self.consume()
149
- else:
150
- await self.consume()
151
- if len(s) == 0:
152
- return None
153
- return s
154
-
155
- async def parse_inline(self, consume_trailing_newline: bool = True):
156
- last_is_space = False
157
- start = True
158
- scope = InlineScope(self.p.state)
159
-
160
- while True:
161
- t = await self.get_next_token()
162
- curr_is_space = False
163
-
164
- if t is None:
165
- if consume_trailing_newline:
166
- if self.peek() == "\n":
167
- await self.consume()
168
- self.emit("\n")
169
- return
170
- elif isinstance(t, str):
171
- # Space or normal text
172
- if t[0] == " ":
173
- curr_is_space = True
174
- self.emit(" ")
175
- else:
176
- self.emit(t)
177
- elif t.token == "`":
178
- # Inline code
179
- scope.enter(t)
180
- with self.p.state.style(dim=True):
181
- await self.parse_inline_code()
182
- scope.exit()
183
- elif t.token == "~~":
184
- # Strike through
185
- if not scope.strike:
186
- scope.enter(t)
187
- self.emit("~~")
188
- else:
189
- self.emit("~~")
190
- scope.exit()
191
- elif (
192
- t.is_bold_or_italic()
193
- and (last_is_space or start)
194
- and scope.last != t.token
195
- ):
196
- # Start bold or italics
197
- scope.enter(t)
198
- self.emit(t.token)
199
- elif (
200
- t.is_bold_or_italic()
201
- and self.peek() in [" ", "\t", *self.terminator]
202
- and scope.last == t.token
203
- ):
204
- # End bold or italics
205
- self.emit(t.token)
206
- scope.exit()
207
- else:
208
- # print(
209
- # "Invalid token:", t.token, f"[{self.peek()}]", scope.last == t.token
210
- # )
211
- raise InvalidState(t.token)
212
-
213
- last_is_space = curr_is_space
214
- start = False