ommlds 0.0.0.dev503__py3-none-any.whl → 0.0.0.dev505__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.
- ommlds/.omlish-manifests.json +5 -5
- ommlds/__about__.py +1 -1
- ommlds/backends/anthropic/protocol/_dataclasses.py +16 -16
- ommlds/backends/cerebras/_dataclasses.py +42 -42
- ommlds/backends/google/protocol/_dataclasses.py +64 -64
- ommlds/backends/groq/_dataclasses.py +36 -36
- ommlds/backends/ollama/_dataclasses.py +28 -28
- ommlds/backends/openai/protocol/_dataclasses.py +88 -88
- ommlds/backends/tavily/_dataclasses.py +16 -16
- ommlds/cli/_dataclasses.py +212 -43
- ommlds/cli/sessions/chat/interfaces/textual/app.py +34 -0
- ommlds/cli/sessions/chat/interfaces/textual/configs.py +1 -1
- ommlds/cli/sessions/chat/interfaces/textual/inject.py +14 -0
- ommlds/cli/sessions/chat/interfaces/textual/inputhistory.py +174 -0
- ommlds/cli/sessions/chat/interfaces/textual/widgets/input.py +42 -8
- ommlds/minichain/_dataclasses.py +361 -343
- ommlds/minichain/backends/impls/cerebras/stream.py +39 -52
- ommlds/minichain/backends/impls/google/chat.py +11 -82
- ommlds/minichain/backends/impls/google/protocol.py +105 -0
- ommlds/minichain/backends/impls/google/stream.py +49 -132
- ommlds/minichain/backends/impls/groq/stream.py +40 -53
- ommlds/minichain/backends/impls/openai/stream.py +40 -87
- ommlds/minichain/http/__init__.py +0 -0
- ommlds/minichain/http/stream.py +195 -0
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/METADATA +6 -6
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/RECORD +30 -26
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/WHEEL +0 -0
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/entry_points.txt +0 -0
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/licenses/LICENSE +0 -0
- {ommlds-0.0.0.dev503.dist-info → ommlds-0.0.0.dev505.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import typing as ta
|
|
5
|
+
|
|
6
|
+
from omlish import lang
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InputHistoryStorage(lang.Abstract):
|
|
13
|
+
@abc.abstractmethod
|
|
14
|
+
def load(self) -> list[str]:
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
@abc.abstractmethod
|
|
18
|
+
def save(self, entries: ta.Sequence[str]) -> None:
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InMemoryInputHistoryStorage(InputHistoryStorage):
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
|
|
26
|
+
self._entries: list[str] = []
|
|
27
|
+
|
|
28
|
+
def load(self) -> list[str]:
|
|
29
|
+
return list(self._entries)
|
|
30
|
+
|
|
31
|
+
def save(self, entries: ta.Sequence[str]) -> None:
|
|
32
|
+
self._entries = list(entries)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FileInputHistoryStorage(InputHistoryStorage):
|
|
36
|
+
def __init__(self, *, path: str) -> None:
|
|
37
|
+
super().__init__()
|
|
38
|
+
|
|
39
|
+
self._path = path
|
|
40
|
+
|
|
41
|
+
def load(self) -> list[str]:
|
|
42
|
+
if not os.path.exists(self._path):
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
with open(self._path) as f:
|
|
47
|
+
data = json.load(f)
|
|
48
|
+
if isinstance(data, list) and all(isinstance(e, str) for e in data):
|
|
49
|
+
return data
|
|
50
|
+
return []
|
|
51
|
+
except (json.JSONDecodeError, OSError):
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
def save(self, entries: ta.Sequence[str]) -> None:
|
|
55
|
+
try:
|
|
56
|
+
dir_path = os.path.dirname(self._path)
|
|
57
|
+
if dir_path:
|
|
58
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
59
|
+
with open(self._path, 'w') as f:
|
|
60
|
+
json.dump(list(entries), f, indent=2)
|
|
61
|
+
except OSError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class InputHistoryManager:
|
|
69
|
+
"""
|
|
70
|
+
Manages input history with readline-style navigation.
|
|
71
|
+
|
|
72
|
+
History position semantics:
|
|
73
|
+
- Position starts at len(history) (one past the end)
|
|
74
|
+
- Moving 'previous' decrements position
|
|
75
|
+
- Moving 'next' increments position
|
|
76
|
+
- Position is clamped to [0, len(history)]
|
|
77
|
+
- Position len(history) represents 'no history item selected'
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
storage: InputHistoryStorage,
|
|
84
|
+
max_entries: int = 1000,
|
|
85
|
+
) -> None:
|
|
86
|
+
super().__init__()
|
|
87
|
+
|
|
88
|
+
self._storage = storage
|
|
89
|
+
self._max_entries = max_entries
|
|
90
|
+
|
|
91
|
+
self._entries: list[str] = self._storage.load()
|
|
92
|
+
self._position: int = len(self._entries)
|
|
93
|
+
self._current_draft: str = ''
|
|
94
|
+
|
|
95
|
+
def add(self, text: str) -> None:
|
|
96
|
+
"""Add a new history entry and reset position."""
|
|
97
|
+
|
|
98
|
+
if not text.strip():
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# Don't add duplicate consecutive entries
|
|
102
|
+
if self._entries and self._entries[-1] == text:
|
|
103
|
+
self.reset_position()
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
self._entries.append(text)
|
|
107
|
+
|
|
108
|
+
# Trim to max size
|
|
109
|
+
if len(self._entries) > self._max_entries:
|
|
110
|
+
self._entries = self._entries[-self._max_entries:]
|
|
111
|
+
|
|
112
|
+
self._storage.save(self._entries)
|
|
113
|
+
self.reset_position()
|
|
114
|
+
|
|
115
|
+
def get_previous(self, text: str | None = None) -> str | None:
|
|
116
|
+
"""
|
|
117
|
+
Navigate to previous (older) history entry.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
text: Current text in the input field (saved as draft when at end of history)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The previous history entry, or None if at the beginning
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
if not self._entries:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# Save current draft if we're at the end
|
|
130
|
+
if self._position == len(self._entries) and text is not None:
|
|
131
|
+
self._current_draft = text
|
|
132
|
+
|
|
133
|
+
# Move to previous entry
|
|
134
|
+
if self._position > 0:
|
|
135
|
+
self._position -= 1
|
|
136
|
+
return self._entries[self._position]
|
|
137
|
+
|
|
138
|
+
# Already at oldest entry
|
|
139
|
+
return self._entries[0] if self._entries else None
|
|
140
|
+
|
|
141
|
+
def get_next(self, text: str | None = None) -> str | None:
|
|
142
|
+
"""
|
|
143
|
+
Navigate to next (newer) history entry.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
text: Current text in the input field (unused for now)
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The next history entry, the saved draft if moving past the end, or None
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
if not self._entries:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
# Move to next entry
|
|
156
|
+
if self._position < len(self._entries):
|
|
157
|
+
self._position += 1
|
|
158
|
+
|
|
159
|
+
# If we moved past the end, return the draft
|
|
160
|
+
if self._position == len(self._entries):
|
|
161
|
+
draft = self._current_draft
|
|
162
|
+
self._current_draft = ''
|
|
163
|
+
return draft
|
|
164
|
+
|
|
165
|
+
return self._entries[self._position]
|
|
166
|
+
|
|
167
|
+
# Already at newest position
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def reset_position(self) -> None:
|
|
171
|
+
"""Reset history position to the end (no history item selected)."""
|
|
172
|
+
|
|
173
|
+
self._position = len(self._entries)
|
|
174
|
+
self._current_draft = ''
|
|
@@ -12,19 +12,53 @@ class InputTextArea(tx.TextArea):
|
|
|
12
12
|
class Submitted(tx.Message):
|
|
13
13
|
text: str
|
|
14
14
|
|
|
15
|
+
@dc.dataclass()
|
|
16
|
+
class HistoryPrevious(tx.Message):
|
|
17
|
+
text: str
|
|
18
|
+
|
|
19
|
+
@dc.dataclass()
|
|
20
|
+
class HistoryNext(tx.Message):
|
|
21
|
+
text: str
|
|
22
|
+
|
|
23
|
+
@dc.dataclass()
|
|
24
|
+
class HistoryReset(tx.Message):
|
|
25
|
+
pass
|
|
26
|
+
|
|
15
27
|
def __init__(self, **kwargs: ta.Any) -> None:
|
|
16
28
|
super().__init__(**kwargs)
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
BINDINGS: ta.ClassVar[ta.Sequence[tx.Binding]] = [ # type: ignore[assignment]
|
|
31
|
+
tx.Binding(
|
|
32
|
+
'enter',
|
|
33
|
+
'submit',
|
|
34
|
+
priority=True,
|
|
35
|
+
),
|
|
36
|
+
tx.Binding(
|
|
37
|
+
'ctrl+p',
|
|
38
|
+
'history_previous',
|
|
39
|
+
),
|
|
40
|
+
tx.Binding(
|
|
41
|
+
'ctrl+n',
|
|
42
|
+
'history_next',
|
|
43
|
+
),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def action_submit(self) -> None:
|
|
47
|
+
if text := self.text.strip():
|
|
48
|
+
self.post_message(self.Submitted(text))
|
|
49
|
+
|
|
50
|
+
def action_cursor_up(self, select: bool = False) -> None:
|
|
51
|
+
# FIXME: if empty -> history_previous
|
|
52
|
+
super().action_cursor_up(select=select)
|
|
53
|
+
|
|
54
|
+
def action_cursor_down(self, select: bool = False) -> None:
|
|
55
|
+
super().action_cursor_down(select=select)
|
|
22
56
|
|
|
23
|
-
|
|
24
|
-
|
|
57
|
+
def action_history_previous(self) -> None:
|
|
58
|
+
self.post_message(self.HistoryPrevious(self.text))
|
|
25
59
|
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
def action_history_next(self) -> None:
|
|
61
|
+
self.post_message(self.HistoryNext(self.text))
|
|
28
62
|
|
|
29
63
|
|
|
30
64
|
class InputOuter(tx.Static):
|