aicosmos-client 0.0.1__py3-none-any.whl → 0.0.2__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.
- aicosmos_client/cli.py +479 -0
- aicosmos_client/client.py +3 -3
- {aicosmos_client-0.0.1.dist-info → aicosmos_client-0.0.2.dist-info}/METADATA +2 -1
- aicosmos_client-0.0.2.dist-info/RECORD +7 -0
- aicosmos_client-0.0.1.dist-info/RECORD +0 -6
- {aicosmos_client-0.0.1.dist-info → aicosmos_client-0.0.2.dist-info}/WHEEL +0 -0
- {aicosmos_client-0.0.1.dist-info → aicosmos_client-0.0.2.dist-info}/licenses/LICENSE +0 -0
aicosmos_client/cli.py
ADDED
@@ -0,0 +1,479 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Dict
|
3
|
+
|
4
|
+
from textual import on, work
|
5
|
+
from textual.app import App, ComposeResult
|
6
|
+
from textual.containers import Container
|
7
|
+
from textual.reactive import reactive
|
8
|
+
from textual.screen import Screen
|
9
|
+
from textual.widgets import (
|
10
|
+
Button,
|
11
|
+
Footer,
|
12
|
+
Header,
|
13
|
+
Input,
|
14
|
+
Label,
|
15
|
+
ListItem,
|
16
|
+
ListView,
|
17
|
+
LoadingIndicator,
|
18
|
+
RichLog,
|
19
|
+
Static,
|
20
|
+
)
|
21
|
+
from client import AICosmosClient
|
22
|
+
|
23
|
+
|
24
|
+
class LoginScreen(Screen):
|
25
|
+
CSS = """
|
26
|
+
#login-container {
|
27
|
+
width: 100%;
|
28
|
+
height: 100%;
|
29
|
+
border: solid $accent;
|
30
|
+
padding: 1;
|
31
|
+
}
|
32
|
+
#login-title {
|
33
|
+
width: 100%;
|
34
|
+
text-align: center;
|
35
|
+
margin-bottom: 1;
|
36
|
+
text-style: bold;
|
37
|
+
}
|
38
|
+
#login-error {
|
39
|
+
width: 100%;
|
40
|
+
color: red;
|
41
|
+
text-align: center;
|
42
|
+
margin-top: 1;
|
43
|
+
}
|
44
|
+
"""
|
45
|
+
|
46
|
+
def compose(self) -> ComposeResult:
|
47
|
+
yield Header()
|
48
|
+
with Container(id="login-container"):
|
49
|
+
yield Label("Welcome to AICosmos", id="login-title")
|
50
|
+
yield Input(placeholder="server url", id="server_url")
|
51
|
+
yield Input(placeholder="username", id="username")
|
52
|
+
yield Input(
|
53
|
+
placeholder="password",
|
54
|
+
id="password",
|
55
|
+
password=True,
|
56
|
+
)
|
57
|
+
yield Button("Login", id="login-button", variant="primary")
|
58
|
+
yield Label("", id="login-error")
|
59
|
+
yield Footer()
|
60
|
+
|
61
|
+
@on(Button.Pressed, "#login-button")
|
62
|
+
def on_login(self) -> None:
|
63
|
+
server_url = self.query_one("#server_url", Input).value
|
64
|
+
username = self.query_one("#username", Input).value
|
65
|
+
password = self.query_one("#password", Input).value
|
66
|
+
|
67
|
+
if not server_url:
|
68
|
+
self.query_one("#login-error").update("Server url cannot be empty")
|
69
|
+
return
|
70
|
+
if not username:
|
71
|
+
self.query_one("#login-error").update("User cannot be empty")
|
72
|
+
return
|
73
|
+
if not password:
|
74
|
+
self.query_one("#login-error").update("Password cannot be empty")
|
75
|
+
return
|
76
|
+
|
77
|
+
try:
|
78
|
+
self.app.client = AICosmosClient(
|
79
|
+
base_url=server_url, username=username, password=password
|
80
|
+
)
|
81
|
+
self.app.push_screen("session")
|
82
|
+
except ValueError:
|
83
|
+
self.query_one("#login-error").update(
|
84
|
+
"Login failed, please check your information"
|
85
|
+
)
|
86
|
+
|
87
|
+
|
88
|
+
class SessionScreen(Screen):
|
89
|
+
CSS = """
|
90
|
+
#session-container {
|
91
|
+
width: 100%;
|
92
|
+
height: 100%;
|
93
|
+
border: solid $accent;
|
94
|
+
padding: 1;
|
95
|
+
}
|
96
|
+
#session-header {
|
97
|
+
width: 100%;
|
98
|
+
layout: horizontal;
|
99
|
+
height: auto;
|
100
|
+
margin-bottom: 1;
|
101
|
+
}
|
102
|
+
#session-title {
|
103
|
+
width: 100%;
|
104
|
+
text-align: center;
|
105
|
+
text-style: bold;
|
106
|
+
content-align: center middle;
|
107
|
+
}
|
108
|
+
#session-actions {
|
109
|
+
width: 100%;
|
110
|
+
layout: horizontal;
|
111
|
+
height: auto;
|
112
|
+
margin-bottom: 1;
|
113
|
+
}
|
114
|
+
#create-session {
|
115
|
+
width: 30%;
|
116
|
+
margin-right: 1;
|
117
|
+
}
|
118
|
+
#logout-button {
|
119
|
+
width: 30%;
|
120
|
+
}
|
121
|
+
.spacer {
|
122
|
+
width: 40%;
|
123
|
+
}
|
124
|
+
.subtitle {
|
125
|
+
width: 100%;
|
126
|
+
margin-top: 1;
|
127
|
+
margin-bottom: 1;
|
128
|
+
text-style: underline;
|
129
|
+
}
|
130
|
+
#session-list {
|
131
|
+
width: 100%;
|
132
|
+
height: 60%;
|
133
|
+
border: solid $panel;
|
134
|
+
}
|
135
|
+
#session-error {
|
136
|
+
width: 100%;
|
137
|
+
color: red;
|
138
|
+
text-align: center;
|
139
|
+
margin-top: 1;
|
140
|
+
}
|
141
|
+
"""
|
142
|
+
|
143
|
+
sessions = reactive([])
|
144
|
+
|
145
|
+
def compose(self) -> ComposeResult:
|
146
|
+
yield Header()
|
147
|
+
with Container(id="session-container"):
|
148
|
+
with Container(id="session-header"):
|
149
|
+
yield Label("Select Session", id="session-title")
|
150
|
+
with Container(id="session-actions"):
|
151
|
+
yield Button("Create Session", id="create-session", variant="primary")
|
152
|
+
yield Static("", classes="spacer")
|
153
|
+
yield Button("Log out", id="logout-button", variant="error")
|
154
|
+
yield Label("Your sessions:", classes="subtitle")
|
155
|
+
yield ListView(id="session-list")
|
156
|
+
yield Label("", id="session-error")
|
157
|
+
yield Footer()
|
158
|
+
|
159
|
+
def sanitize_id(self, session_id: str) -> str:
|
160
|
+
"""Turn session id into valid HTML identifier"""
|
161
|
+
if not isinstance(session_id, str):
|
162
|
+
session_id = str(session_id)
|
163
|
+
|
164
|
+
import re
|
165
|
+
|
166
|
+
clean_id = re.sub(r"[^a-zA-Z0-9_-]", "", session_id)
|
167
|
+
|
168
|
+
if clean_id and clean_id[0].isdigit():
|
169
|
+
clean_id = "s_" + clean_id
|
170
|
+
|
171
|
+
return clean_id or "invalid_session"
|
172
|
+
|
173
|
+
def on_mount(self) -> None:
|
174
|
+
self.load_sessions()
|
175
|
+
|
176
|
+
@work(exclusive=True)
|
177
|
+
async def load_sessions(self) -> None:
|
178
|
+
self.query_one("#session-list").clear()
|
179
|
+
self.sessions, message = self.app.client.get_my_sessions()
|
180
|
+
|
181
|
+
if message != "success":
|
182
|
+
self.query_one("#session-list").append(
|
183
|
+
ListItem(Label(f"{message}"), id="no-sessions")
|
184
|
+
)
|
185
|
+
return
|
186
|
+
|
187
|
+
if not self.sessions:
|
188
|
+
self.query_one("#session-list").append(
|
189
|
+
ListItem(Label("No session found"), id="no-sessions")
|
190
|
+
)
|
191
|
+
return
|
192
|
+
|
193
|
+
for session in self.sessions:
|
194
|
+
session_id = session["session_id"]
|
195
|
+
clean_id = self.sanitize_id(session_id)
|
196
|
+
title = session["environment_info"].get(
|
197
|
+
"title", f"Session {session_id[:6]}"
|
198
|
+
)
|
199
|
+
self.query_one("#session-list").append(
|
200
|
+
ListItem(Label(title), id=f"session-{clean_id}")
|
201
|
+
)
|
202
|
+
|
203
|
+
@on(Button.Pressed, "#create-session")
|
204
|
+
@work(exclusive=True)
|
205
|
+
async def create_session(self) -> None:
|
206
|
+
session_id = self.app.client.create_session()
|
207
|
+
|
208
|
+
if session_id:
|
209
|
+
self.app.current_session = session_id
|
210
|
+
self.app.uninstall_screen("chat")
|
211
|
+
self.app.install_screen(ChatScreen(), "chat")
|
212
|
+
self.app.push_screen("chat")
|
213
|
+
else:
|
214
|
+
self.query_one("#session-error").update("Failed to create session")
|
215
|
+
|
216
|
+
@on(ListView.Selected)
|
217
|
+
def session_selected(self, event: ListView.Selected) -> None:
|
218
|
+
"""Handle session selection"""
|
219
|
+
if not event.item or not event.item.id or event.item.id == "no-sessions":
|
220
|
+
return
|
221
|
+
|
222
|
+
selected_index = event.list_view.index
|
223
|
+
if selected_index is None or selected_index >= len(self.sessions):
|
224
|
+
return
|
225
|
+
|
226
|
+
selected_session = self.sessions[selected_index]
|
227
|
+
session_id = selected_session["session_id"]
|
228
|
+
|
229
|
+
if session_id:
|
230
|
+
self.app.current_session = session_id
|
231
|
+
self.app.uninstall_screen("chat")
|
232
|
+
self.app.install_screen(ChatScreen(), "chat")
|
233
|
+
self.app.push_screen("chat")
|
234
|
+
|
235
|
+
@on(Button.Pressed, "#logout-button")
|
236
|
+
def logout(self) -> None:
|
237
|
+
"""Go back to login page"""
|
238
|
+
self.app.pop_screen()
|
239
|
+
self.app.push_screen("login")
|
240
|
+
|
241
|
+
|
242
|
+
class ChatScreen(Screen):
|
243
|
+
CSS = """
|
244
|
+
#chat-container {
|
245
|
+
width: 100%;
|
246
|
+
height: 100%;
|
247
|
+
border: solid $accent;
|
248
|
+
padding: 1;
|
249
|
+
}
|
250
|
+
|
251
|
+
#chat-header {
|
252
|
+
width: 100%;
|
253
|
+
layout: horizontal;
|
254
|
+
height: 3;
|
255
|
+
margin-bottom: 1;
|
256
|
+
}
|
257
|
+
|
258
|
+
#chat-title-container {
|
259
|
+
width: 1fr;
|
260
|
+
height: 100%;
|
261
|
+
align: center middle;
|
262
|
+
}
|
263
|
+
|
264
|
+
#chat-title {
|
265
|
+
text-align: center;
|
266
|
+
text-style: bold;
|
267
|
+
}
|
268
|
+
|
269
|
+
#back-button {
|
270
|
+
width: auto;
|
271
|
+
height: 100%;
|
272
|
+
dock: right;
|
273
|
+
}
|
274
|
+
|
275
|
+
#chat-content {
|
276
|
+
width: 100%;
|
277
|
+
height: 1fr;
|
278
|
+
layout: vertical;
|
279
|
+
}
|
280
|
+
|
281
|
+
#chat-log {
|
282
|
+
width: 100%;
|
283
|
+
height: 1fr;
|
284
|
+
border: solid $panel;
|
285
|
+
padding: 1;
|
286
|
+
overflow-y: auto;
|
287
|
+
scrollbar-size: 1 1;
|
288
|
+
scrollbar-color: $accent;
|
289
|
+
}
|
290
|
+
|
291
|
+
#input-container {
|
292
|
+
width: 100%;
|
293
|
+
height: auto;
|
294
|
+
margin-top: 1;
|
295
|
+
layout: horizontal;
|
296
|
+
}
|
297
|
+
|
298
|
+
#message-input {
|
299
|
+
width: 1fr;
|
300
|
+
}
|
301
|
+
|
302
|
+
#send-button {
|
303
|
+
width: auto;
|
304
|
+
margin-left: 1;
|
305
|
+
}
|
306
|
+
|
307
|
+
#loading-indicator {
|
308
|
+
width: auto;
|
309
|
+
margin-left: 1;
|
310
|
+
}
|
311
|
+
"""
|
312
|
+
|
313
|
+
def compose(self) -> ComposeResult:
|
314
|
+
yield Header()
|
315
|
+
with Container(id="chat-container"):
|
316
|
+
with Container(id="chat-header"):
|
317
|
+
with Container(id="chat-title-container"):
|
318
|
+
yield Label("Session", id="chat-title")
|
319
|
+
yield Button("Back", id="back-button", variant="default")
|
320
|
+
|
321
|
+
with Container(id="chat-content"):
|
322
|
+
yield RichLog(id="chat-log", wrap=True, markup=True, auto_scroll=True)
|
323
|
+
|
324
|
+
with Container(id="input-container"):
|
325
|
+
yield Input(placeholder="Enter your message...", id="message-input")
|
326
|
+
yield Button("Send", id="send-button", variant="primary")
|
327
|
+
yield LoadingIndicator(id="loading-indicator")
|
328
|
+
yield Footer()
|
329
|
+
|
330
|
+
def __init__(self):
|
331
|
+
super().__init__()
|
332
|
+
self.conversation = []
|
333
|
+
|
334
|
+
def on_resize(self) -> None:
|
335
|
+
"""Recalculate message displays when window changes"""
|
336
|
+
self.update_message_display()
|
337
|
+
|
338
|
+
def update_message_display(self) -> None:
|
339
|
+
"""Update message displays when window changes"""
|
340
|
+
log = self.query_one("#chat-log")
|
341
|
+
log.clear()
|
342
|
+
for message in self.conversation:
|
343
|
+
log.write(self.format_message(message))
|
344
|
+
log.scroll_end(animate=False)
|
345
|
+
|
346
|
+
def calculate_max_line_length(self) -> int:
|
347
|
+
"""Max line length for current session"""
|
348
|
+
log_widget = self.query_one("#chat-log")
|
349
|
+
container_width = log_widget.content_size.width
|
350
|
+
return container_width - 1
|
351
|
+
|
352
|
+
def format_message(self, message: Dict) -> str:
|
353
|
+
"""Format each message, with two spaces as indent"""
|
354
|
+
role = message["role"]
|
355
|
+
content = message["content"]
|
356
|
+
max_line_length = self.calculate_max_line_length() - 2
|
357
|
+
|
358
|
+
import textwrap
|
359
|
+
|
360
|
+
wrapped_lines = []
|
361
|
+
for line in content.split("\n"):
|
362
|
+
indented_line = textwrap.fill(
|
363
|
+
line,
|
364
|
+
width=max_line_length,
|
365
|
+
initial_indent=" ",
|
366
|
+
subsequent_indent=" ",
|
367
|
+
replace_whitespace=False,
|
368
|
+
drop_whitespace=False,
|
369
|
+
)
|
370
|
+
wrapped_lines.append(indented_line)
|
371
|
+
|
372
|
+
wrapped_content = "\n".join(wrapped_lines)
|
373
|
+
|
374
|
+
if role == "user":
|
375
|
+
return f"[cyan][b]user[/b][/cyan]\n{wrapped_content}\n"
|
376
|
+
elif role == "assistant":
|
377
|
+
char_name = message.get("character_name", "assistant")
|
378
|
+
return f"[green][b]{char_name}[/b][/green]\n{wrapped_content}\n"
|
379
|
+
else:
|
380
|
+
return f"[red][b]{role}[/b][/red]\n{wrapped_content}\n"
|
381
|
+
|
382
|
+
def add_message(self, message: Dict) -> None:
|
383
|
+
"""Update history"""
|
384
|
+
self.conversation.append(message)
|
385
|
+
self.update_message_display()
|
386
|
+
|
387
|
+
def on_show(self) -> None:
|
388
|
+
"""Hide loading indicator and show history"""
|
389
|
+
self.query_one("#loading-indicator").display = False
|
390
|
+
self.load_conversation()
|
391
|
+
self.query_one("#message-input").focus()
|
392
|
+
|
393
|
+
def on_mount(self) -> None:
|
394
|
+
"""Hide loading indicator and show history"""
|
395
|
+
self.query_one("#loading-indicator").display = False
|
396
|
+
self.load_conversation()
|
397
|
+
self.query_one("#message-input").focus()
|
398
|
+
|
399
|
+
@work(exclusive=True)
|
400
|
+
async def load_conversation(self) -> None:
|
401
|
+
"""Fetch conversation history"""
|
402
|
+
self.conversation, message = self.app.client.get_session_history(
|
403
|
+
self.app.current_session
|
404
|
+
)
|
405
|
+
if message != "success":
|
406
|
+
self.query_one("#chat-log").write(f"[red]Error: {message}[/red]")
|
407
|
+
return
|
408
|
+
self.update_message_display()
|
409
|
+
|
410
|
+
@on(Button.Pressed, "#send-button")
|
411
|
+
@on(Input.Submitted, "#message-input")
|
412
|
+
def send_message(self) -> None:
|
413
|
+
"""Send message to backend"""
|
414
|
+
input_widget = self.query_one("#message-input")
|
415
|
+
message = input_widget.value.strip()
|
416
|
+
|
417
|
+
if not message:
|
418
|
+
return
|
419
|
+
|
420
|
+
input_widget.value = ""
|
421
|
+
self.add_message({"role": "user", "content": message})
|
422
|
+
self.process_response(message)
|
423
|
+
|
424
|
+
@work(exclusive=True)
|
425
|
+
async def process_response(self, message: str) -> None:
|
426
|
+
"""Wait for response from backend"""
|
427
|
+
loading = self.query_one("#loading-indicator", LoadingIndicator)
|
428
|
+
input_widget = self.query_one("#message-input")
|
429
|
+
send_button = self.query_one("#send-button")
|
430
|
+
|
431
|
+
loading.display = True
|
432
|
+
input_widget.disabled = True
|
433
|
+
send_button.disabled = True
|
434
|
+
|
435
|
+
try:
|
436
|
+
conversation_history, message = await asyncio.to_thread(
|
437
|
+
self.app.client.chat, self.app.current_session, message
|
438
|
+
)
|
439
|
+
|
440
|
+
if message != "success":
|
441
|
+
self.query_one("#chat-log").write(f"[red]Error: {message}[/red]")
|
442
|
+
return
|
443
|
+
new_messages = conversation_history[len(self.conversation) :]
|
444
|
+
for msg in new_messages:
|
445
|
+
self.add_message(msg)
|
446
|
+
|
447
|
+
except Exception as e:
|
448
|
+
self.query_one("#chat-log").write(f"[red]Query failed: {str(e)}[/red]")
|
449
|
+
|
450
|
+
finally:
|
451
|
+
loading.display = False
|
452
|
+
input_widget.disabled = False
|
453
|
+
send_button.disabled = False
|
454
|
+
input_widget.focus()
|
455
|
+
|
456
|
+
@on(Button.Pressed, "#back-button")
|
457
|
+
def back_to_sessions(self) -> None:
|
458
|
+
"""Go back to sessions list"""
|
459
|
+
self.app.pop_screen()
|
460
|
+
|
461
|
+
|
462
|
+
class AICosmosCLI(App):
|
463
|
+
CSS = """
|
464
|
+
Screen {
|
465
|
+
align: center middle;
|
466
|
+
}
|
467
|
+
"""
|
468
|
+
|
469
|
+
FULL_SCREEN = True
|
470
|
+
|
471
|
+
SCREENS = {"login": LoginScreen, "session": SessionScreen, "chat": ChatScreen}
|
472
|
+
|
473
|
+
def __init__(self):
|
474
|
+
super().__init__()
|
475
|
+
self.client: AICosmosClient = None
|
476
|
+
self.current_session: str = None
|
477
|
+
|
478
|
+
def on_mount(self) -> None:
|
479
|
+
self.push_screen("login")
|
aicosmos_client/client.py
CHANGED
@@ -111,7 +111,7 @@ class AICosmosClient:
|
|
111
111
|
def get_session_history(self, session_id: str):
|
112
112
|
session, message = self._get_session_status(session_id)
|
113
113
|
if not session:
|
114
|
-
return
|
114
|
+
return [], message
|
115
115
|
else:
|
116
116
|
return session.get("conversation", []), message
|
117
117
|
|
@@ -132,6 +132,6 @@ class AICosmosClient:
|
|
132
132
|
if success:
|
133
133
|
return response.json()["conversation_history"], "Success"
|
134
134
|
else:
|
135
|
-
return
|
135
|
+
return [], f"Status code: {response.status_code}"
|
136
136
|
except Exception as e:
|
137
|
-
return
|
137
|
+
return [], f"Error: {e}"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: aicosmos_client
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.2
|
4
4
|
Summary: client for AICosmos platform
|
5
5
|
Project-URL: Homepage, https://github.com/pypa/sampleproject
|
6
6
|
Project-URL: Issues, https://github.com/pypa/sampleproject/issues
|
@@ -11,6 +11,7 @@ Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
12
12
|
Requires-Python: >=3.9
|
13
13
|
Requires-Dist: requests>=2.25.0
|
14
|
+
Requires-Dist: textual>=5.0.0
|
14
15
|
Description-Content-Type: text/markdown
|
15
16
|
|
16
17
|
# Example Package
|
@@ -0,0 +1,7 @@
|
|
1
|
+
aicosmos_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
aicosmos_client/cli.py,sha256=GtmZYWeCXi_09QmVP9w4gMiIzajCcerhFzW9Fr4Urqc,13938
|
3
|
+
aicosmos_client/client.py,sha256=t3qlfanWG_NngNp9VPwHcJh_JC-2sQR-RRMvXWVQVcM,4875
|
4
|
+
aicosmos_client-0.0.2.dist-info/METADATA,sha256=IzSYeEGkvROSIIAfbD1XcPDYQKE5T95tM8vi4qiWkw4,712
|
5
|
+
aicosmos_client-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
6
|
+
aicosmos_client-0.0.2.dist-info/licenses/LICENSE,sha256=XBdpsYae127l7YQyMSVQwUUo22FPis7sMts7oBjkN_g,1056
|
7
|
+
aicosmos_client-0.0.2.dist-info/RECORD,,
|
@@ -1,6 +0,0 @@
|
|
1
|
-
aicosmos_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
aicosmos_client/client.py,sha256=Rmwcqz2rd8Q894bsQbKc4dMg9RRLJKPEKbCRe8OvOzw,4881
|
3
|
-
aicosmos_client-0.0.1.dist-info/METADATA,sha256=83k1OehHN6N30Y9B1sovoyARq5cmKB7XloUbp81S2ZY,682
|
4
|
-
aicosmos_client-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
-
aicosmos_client-0.0.1.dist-info/licenses/LICENSE,sha256=XBdpsYae127l7YQyMSVQwUUo22FPis7sMts7oBjkN_g,1056
|
6
|
-
aicosmos_client-0.0.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|