aicosmos-client 0.0.4__py3-none-any.whl → 0.0.6__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 +37 -25
- aicosmos_client/client.py +144 -103
- aicosmos_client-0.0.6.dist-info/METADATA +70 -0
- aicosmos_client-0.0.6.dist-info/RECORD +7 -0
- aicosmos_client-0.0.4.dist-info/METADATA +0 -21
- aicosmos_client-0.0.4.dist-info/RECORD +0 -7
- {aicosmos_client-0.0.4.dist-info → aicosmos_client-0.0.6.dist-info}/WHEEL +0 -0
- {aicosmos_client-0.0.4.dist-info → aicosmos_client-0.0.6.dist-info}/licenses/LICENSE +0 -0
aicosmos_client/cli.py
CHANGED
@@ -7,8 +7,18 @@ from textual.app import App, ComposeResult
|
|
7
7
|
from textual.containers import Container
|
8
8
|
from textual.reactive import reactive
|
9
9
|
from textual.screen import Screen
|
10
|
-
from textual.widgets import (
|
11
|
-
|
10
|
+
from textual.widgets import (
|
11
|
+
Button,
|
12
|
+
Footer,
|
13
|
+
Header,
|
14
|
+
Input,
|
15
|
+
Label,
|
16
|
+
ListItem,
|
17
|
+
ListView,
|
18
|
+
LoadingIndicator,
|
19
|
+
RichLog,
|
20
|
+
Static,
|
21
|
+
)
|
12
22
|
|
13
23
|
|
14
24
|
class LoginScreen(Screen):
|
@@ -166,11 +176,11 @@ class SessionScreen(Screen):
|
|
166
176
|
@work(exclusive=True)
|
167
177
|
async def load_sessions(self) -> None:
|
168
178
|
self.query_one("#session-list").clear()
|
169
|
-
|
170
|
-
|
171
|
-
|
179
|
+
try:
|
180
|
+
self.sessions = self.app.client.get_my_sessions()
|
181
|
+
except Exception as e:
|
172
182
|
self.query_one("#session-list").append(
|
173
|
-
ListItem(Label(f"{
|
183
|
+
ListItem(Label(f"Failed: {e}"), id="no-sessions")
|
174
184
|
)
|
175
185
|
return
|
176
186
|
|
@@ -193,15 +203,16 @@ class SessionScreen(Screen):
|
|
193
203
|
@on(Button.Pressed, "#create-session")
|
194
204
|
@work(exclusive=True)
|
195
205
|
async def create_session(self) -> None:
|
196
|
-
|
206
|
+
try:
|
207
|
+
session_id = self.app.client.create_session()
|
208
|
+
except Exception as e:
|
209
|
+
self.query_one("#session-error").update(f"Failed to create session: {e}")
|
210
|
+
return
|
197
211
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
self.app.push_screen("chat")
|
203
|
-
else:
|
204
|
-
self.query_one("#session-error").update(f"Failed to create session: {message}")
|
212
|
+
self.app.current_session = session_id
|
213
|
+
self.app.uninstall_screen("chat")
|
214
|
+
self.app.install_screen(ChatScreen(), "chat")
|
215
|
+
self.app.push_screen("chat")
|
205
216
|
|
206
217
|
@on(ListView.Selected)
|
207
218
|
def session_selected(self, event: ListView.Selected) -> None:
|
@@ -389,11 +400,12 @@ class ChatScreen(Screen):
|
|
389
400
|
@work(exclusive=True)
|
390
401
|
async def load_conversation(self) -> None:
|
391
402
|
"""Fetch conversation history"""
|
392
|
-
|
393
|
-
self.app.
|
394
|
-
|
395
|
-
|
396
|
-
|
403
|
+
try:
|
404
|
+
self.conversation = self.app.client.get_session_history(
|
405
|
+
self.app.current_session
|
406
|
+
)
|
407
|
+
except Exception as e:
|
408
|
+
self.query_one("#chat-log").write(f"[red]Error: {e}[/red]")
|
397
409
|
return
|
398
410
|
self.update_message_display()
|
399
411
|
|
@@ -423,12 +435,12 @@ class ChatScreen(Screen):
|
|
423
435
|
send_button.disabled = True
|
424
436
|
|
425
437
|
try:
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
self.query_one("#chat-log").write(f"[red]Error: {
|
438
|
+
try:
|
439
|
+
conversation_history = await asyncio.to_thread(
|
440
|
+
self.app.client.chat, self.app.current_session, message
|
441
|
+
)
|
442
|
+
except Exception as e:
|
443
|
+
self.query_one("#chat-log").write(f"[red]Error: {e}[/red]")
|
432
444
|
return
|
433
445
|
new_messages = conversation_history[len(self.conversation) :]
|
434
446
|
for msg in new_messages:
|
aicosmos_client/client.py
CHANGED
@@ -1,17 +1,90 @@
|
|
1
|
+
import os
|
2
|
+
import socket
|
3
|
+
import ssl
|
4
|
+
from urllib.parse import urlparse
|
5
|
+
|
6
|
+
import certifi
|
1
7
|
import requests
|
8
|
+
from requests.adapters import HTTPAdapter
|
9
|
+
|
10
|
+
|
11
|
+
class SSLAdapter(HTTPAdapter):
|
12
|
+
"""HTTPS adapter that allows dynamic CA bundle injection."""
|
13
|
+
|
14
|
+
def __init__(self, cafile=None, *args, **kwargs):
|
15
|
+
self.cafile = cafile
|
16
|
+
super().__init__(*args, **kwargs)
|
17
|
+
|
18
|
+
def init_poolmanager(self, *args, **kwargs):
|
19
|
+
context = ssl.create_default_context()
|
20
|
+
context.load_verify_locations(cafile=certifi.where())
|
21
|
+
if self.cafile and os.path.exists(self.cafile):
|
22
|
+
context.load_verify_locations(cafile=self.cafile)
|
23
|
+
kwargs["ssl_context"] = context
|
24
|
+
return super().init_poolmanager(*args, **kwargs)
|
2
25
|
|
3
26
|
|
4
27
|
class AICosmosClient:
|
5
|
-
def __init__(
|
6
|
-
self
|
7
|
-
|
8
|
-
|
9
|
-
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
base_url: str,
|
31
|
+
username: str,
|
32
|
+
password: str,
|
33
|
+
certs_dir: str = None,
|
34
|
+
auto_trust: bool = False,
|
35
|
+
):
|
36
|
+
"""
|
37
|
+
:param base_url: API base URL, e.g. 'https://aicosmos.ai/api'
|
38
|
+
:param username: Username for login
|
39
|
+
:param password: Password for login
|
40
|
+
:param certs_dir: Directory for storing trusted self-signed certs
|
41
|
+
:param auto_trust: If True, will automatically trust self-signed certs
|
42
|
+
"""
|
43
|
+
self.base_url = base_url.rstrip("/")
|
44
|
+
self.username = username
|
45
|
+
self.password = password
|
10
46
|
self.access_token: str = None
|
47
|
+
self.auto_trust = auto_trust
|
48
|
+
|
49
|
+
host = urlparse(self.base_url).hostname
|
50
|
+
self.certs_dir = certs_dir or os.path.join(
|
51
|
+
os.path.expanduser("~"), ".aicosmos", "certs"
|
52
|
+
)
|
53
|
+
os.makedirs(self.certs_dir, exist_ok=True)
|
54
|
+
self.cert_file = os.path.join(self.certs_dir, f"{host}.pem")
|
55
|
+
|
56
|
+
self.session = requests.Session()
|
57
|
+
self.session.mount("https://", SSLAdapter(cafile=self.cert_file))
|
11
58
|
|
12
|
-
|
13
|
-
|
14
|
-
|
59
|
+
self._login()
|
60
|
+
|
61
|
+
def _fetch_server_cert(self, hostname, port=443):
|
62
|
+
"""Download server's SSL certificate and save locally."""
|
63
|
+
pem_path = self.cert_file
|
64
|
+
conn = socket.create_connection((hostname, port))
|
65
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
66
|
+
context.check_hostname = False
|
67
|
+
context.verify_mode = ssl.CERT_NONE
|
68
|
+
with context.wrap_socket(conn, server_hostname=hostname) as sock:
|
69
|
+
der_cert = sock.getpeercert(True)
|
70
|
+
pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)
|
71
|
+
with open(pem_path, "w") as f:
|
72
|
+
f.write(pem_cert)
|
73
|
+
return pem_path
|
74
|
+
|
75
|
+
def _robust_request(self, method, url, **kwargs):
|
76
|
+
try:
|
77
|
+
return self.session.request(method, url, **kwargs)
|
78
|
+
except requests.exceptions.SSLError as e:
|
79
|
+
if not self.auto_trust:
|
80
|
+
raise RuntimeError(
|
81
|
+
f"SSL verification failed for {url}. "
|
82
|
+
f"Set auto_trust=True to accept and store the server's certificate."
|
83
|
+
) from e
|
84
|
+
host = urlparse(self.base_url).hostname
|
85
|
+
self._fetch_server_cert(host)
|
86
|
+
self.session.mount("https://", SSLAdapter(cafile=self.cert_file))
|
87
|
+
return self.session.request(method, url, **kwargs)
|
15
88
|
|
16
89
|
def _login(self):
|
17
90
|
login_data = {
|
@@ -20,18 +93,14 @@ class AICosmosClient:
|
|
20
93
|
"grant_type": "password",
|
21
94
|
}
|
22
95
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
else:
|
32
|
-
return False, f"Status code: {response.status_code}"
|
33
|
-
except Exception as e:
|
34
|
-
return False, f"Error: {e}"
|
96
|
+
response = self._robust_request(
|
97
|
+
"POST", f"{self.base_url}/user/login", data=login_data, headers=headers
|
98
|
+
)
|
99
|
+
if response.status_code == 200:
|
100
|
+
token_data = response.json()
|
101
|
+
self.access_token = token_data["access_token"]
|
102
|
+
else:
|
103
|
+
raise ValueError(f"Login failed: {response.status_code} {response.text}")
|
35
104
|
|
36
105
|
def _get_auth_headers(self):
|
37
106
|
if not self.access_token:
|
@@ -42,96 +111,68 @@ class AICosmosClient:
|
|
42
111
|
}
|
43
112
|
|
44
113
|
def _get_session_status(self, session_id):
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
return None, f"Status code: {response.status_code}"
|
55
|
-
except Exception as e:
|
56
|
-
return None, f"Error: {e}"
|
114
|
+
response = self._robust_request(
|
115
|
+
"GET",
|
116
|
+
f"{self.base_url}/sessions/{session_id}/status",
|
117
|
+
headers=self._get_auth_headers(),
|
118
|
+
)
|
119
|
+
if response.status_code == 200:
|
120
|
+
return response.json()
|
121
|
+
else:
|
122
|
+
raise ValueError(f"Status code: {response.status_code}")
|
57
123
|
|
58
124
|
def create_session(self):
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
else:
|
69
|
-
return None, f"Status code: {response.status_code}"
|
70
|
-
except Exception as e:
|
71
|
-
return None, f"Error: {e}"
|
125
|
+
response = self._robust_request(
|
126
|
+
"POST",
|
127
|
+
f"{self.base_url}/sessions/create",
|
128
|
+
headers=self._get_auth_headers(),
|
129
|
+
)
|
130
|
+
if response.status_code == 200:
|
131
|
+
return response.json()["session_id"]
|
132
|
+
else:
|
133
|
+
raise ValueError(f"Status code: {response.status_code}")
|
72
134
|
|
73
135
|
def delete_session(self, session_id: str):
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
)
|
81
|
-
if response.status_code == 200:
|
82
|
-
return True, "Success"
|
83
|
-
else:
|
84
|
-
return False, f"Status code: {response.status_code}"
|
85
|
-
except Exception as e:
|
86
|
-
return False, f"Error: {e}"
|
136
|
+
response = self._robust_request(
|
137
|
+
"DELETE",
|
138
|
+
f"{self.base_url}/sessions/{session_id}",
|
139
|
+
headers=self._get_auth_headers(),
|
140
|
+
)
|
141
|
+
if response.status_code != 200:
|
142
|
+
raise ValueError(f"Status code: {response.status_code}")
|
87
143
|
|
88
144
|
def get_my_sessions(self):
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
)
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
else:
|
107
|
-
return None, f"Status code: {response.status_code}"
|
108
|
-
except Exception as e:
|
109
|
-
return None, f"Error: {e}"
|
145
|
+
response = self._robust_request(
|
146
|
+
"GET",
|
147
|
+
f"{self.base_url}/sessions/my_sessions",
|
148
|
+
headers=self._get_auth_headers(),
|
149
|
+
)
|
150
|
+
if response.status_code == 200:
|
151
|
+
sessions = response.json()
|
152
|
+
self.active_sessions = sessions
|
153
|
+
return [
|
154
|
+
{
|
155
|
+
"session_id": s["session_id"],
|
156
|
+
"title": s["environment_info"].get("title"),
|
157
|
+
}
|
158
|
+
for s in sessions
|
159
|
+
]
|
160
|
+
else:
|
161
|
+
raise ValueError(f"Status code: {response.status_code}")
|
110
162
|
|
111
163
|
def get_session_history(self, session_id: str):
|
112
|
-
session
|
113
|
-
|
114
|
-
return [], message
|
115
|
-
else:
|
116
|
-
return session.get("conversation", []), message
|
164
|
+
session = self._get_session_status(session_id)
|
165
|
+
return session.get("conversation", [])
|
117
166
|
|
118
167
|
def chat(self, session_id: str, prompt: str):
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
"
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
)
|
131
|
-
success = response.status_code == 200
|
132
|
-
if success:
|
133
|
-
return response.json()["conversation_history"], "Success"
|
134
|
-
else:
|
135
|
-
return [], f"Status code: {response.status_code}"
|
136
|
-
except Exception as e:
|
137
|
-
return [], f"Error: {e}"
|
168
|
+
data = {"user_input": prompt, "session_id": session_id}
|
169
|
+
response = self._robust_request(
|
170
|
+
"POST",
|
171
|
+
f"{self.base_url}/chat",
|
172
|
+
json=data,
|
173
|
+
headers=self._get_auth_headers(),
|
174
|
+
)
|
175
|
+
if response.status_code == 200:
|
176
|
+
return response.json()["conversation_history"]
|
177
|
+
else:
|
178
|
+
raise ValueError(f"Status code: {response.status_code}")
|
@@ -0,0 +1,70 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: aicosmos_client
|
3
|
+
Version: 0.0.6
|
4
|
+
Summary: client for AICosmos platform
|
5
|
+
Project-URL: Homepage, https://github.com/pypa/sampleproject
|
6
|
+
Project-URL: Issues, https://github.com/pypa/sampleproject/issues
|
7
|
+
Author-email: Example Author <author@example.com>
|
8
|
+
License-Expression: MIT
|
9
|
+
License-File: LICENSE
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
|
+
Requires-Python: >=3.9
|
13
|
+
Requires-Dist: requests>=2.25.0
|
14
|
+
Requires-Dist: textual>=5.0.0
|
15
|
+
Description-Content-Type: text/markdown
|
16
|
+
|
17
|
+
# Client for AICosmos
|
18
|
+
|
19
|
+
This package implements the client for AICosmos. Before using this package, please make sure that you have a valid account for AICosmos.
|
20
|
+
|
21
|
+
### AICosmosClient
|
22
|
+
By using this client, you can chat with our backend in "base" mode. To login, you will need the server's address, your username and your password. You can either start a new session, or use an existing one.
|
23
|
+
|
24
|
+
Our framework is a little bit different from "chat completions", where you give an llm the conversation history. Instead, your conversation history, along with other tool execution results, are stored in our database. This gives your a clean and simple interface to use, without worrying about constructing complicated contexts.
|
25
|
+
|
26
|
+
```Python
|
27
|
+
from aicosmos_client.client import AICosmosClient
|
28
|
+
|
29
|
+
# login
|
30
|
+
client = AICosmosClient(
|
31
|
+
base_url="https://aicosmos.ai/api",
|
32
|
+
username="xxx",
|
33
|
+
password="xxx",
|
34
|
+
auto_trust=True,
|
35
|
+
)
|
36
|
+
|
37
|
+
# create a new session
|
38
|
+
try:
|
39
|
+
new_session_id = client.create_session()
|
40
|
+
except Exception as e:
|
41
|
+
print(f"Error creating new session: {e}")
|
42
|
+
exit(0)
|
43
|
+
|
44
|
+
# lookup all the sessions
|
45
|
+
try:
|
46
|
+
my_sessions = client.get_my_sessions()
|
47
|
+
except Exception as e:
|
48
|
+
print(f"Error getting my sessions: {e}")
|
49
|
+
exit(0)
|
50
|
+
# [{"session_id", "title"}, ...]
|
51
|
+
print(my_sessions)
|
52
|
+
|
53
|
+
# enjoy the conversation
|
54
|
+
try:
|
55
|
+
conversation_history = client.chat(new_session_id, "Hello")
|
56
|
+
except Exception as e:
|
57
|
+
print(f"Error chatting: {e}")
|
58
|
+
exit(0)
|
59
|
+
print(conversation_history)
|
60
|
+
```
|
61
|
+
|
62
|
+
## AICosmosCLI
|
63
|
+
To show that the client is enough to build an application, we offer you an command-line interface!
|
64
|
+
|
65
|
+
```Python
|
66
|
+
from aicosmos_client.cli import AICosmosCLI
|
67
|
+
|
68
|
+
# url: https://aicosmos.ai/api
|
69
|
+
AICosmosCLI().run()
|
70
|
+
```
|
@@ -0,0 +1,7 @@
|
|
1
|
+
aicosmos_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
aicosmos_client/cli.py,sha256=V6fewmdU2Im3NNbVw0OrVbxd1mkTGYKTdfgNKJl0CwY,13979
|
3
|
+
aicosmos_client/client.py,sha256=dn58ztTJVSOP9munQcYVB8AAhUiTsJoNnPvngQceSGc,6388
|
4
|
+
aicosmos_client-0.0.6.dist-info/METADATA,sha256=EsoT3FQDaqbmGH2bEdQCCcCAUkKQKz8e4xxH74d4Sj0,2242
|
5
|
+
aicosmos_client-0.0.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
6
|
+
aicosmos_client-0.0.6.dist-info/licenses/LICENSE,sha256=XBdpsYae127l7YQyMSVQwUUo22FPis7sMts7oBjkN_g,1056
|
7
|
+
aicosmos_client-0.0.6.dist-info/RECORD,,
|
@@ -1,21 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: aicosmos_client
|
3
|
-
Version: 0.0.4
|
4
|
-
Summary: client for AICosmos platform
|
5
|
-
Project-URL: Homepage, https://github.com/pypa/sampleproject
|
6
|
-
Project-URL: Issues, https://github.com/pypa/sampleproject/issues
|
7
|
-
Author-email: Example Author <author@example.com>
|
8
|
-
License-Expression: MIT
|
9
|
-
License-File: LICENSE
|
10
|
-
Classifier: Operating System :: OS Independent
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
12
|
-
Requires-Python: >=3.9
|
13
|
-
Requires-Dist: requests>=2.25.0
|
14
|
-
Requires-Dist: textual>=5.0.0
|
15
|
-
Description-Content-Type: text/markdown
|
16
|
-
|
17
|
-
# Example Package
|
18
|
-
|
19
|
-
This is a simple example package. You can use
|
20
|
-
[GitHub-flavored Markdown](https://guides.github.com/features/mastering-markdown/)
|
21
|
-
to write your content.
|
@@ -1,7 +0,0 @@
|
|
1
|
-
aicosmos_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
aicosmos_client/cli.py,sha256=Dv9uNOcc0rMXjCVfaElDxS8p1mnDwFfTt_vnpAwLUuI,13940
|
3
|
-
aicosmos_client/client.py,sha256=SSR7THQDvSj9Bnm2ytdJ5Twe3Vy8YdGslbmx71UgLw4,4870
|
4
|
-
aicosmos_client-0.0.4.dist-info/METADATA,sha256=a85Gf6H1HTMtUY-YvMuzm_QFnDKT48L8Ngwp_1iwq64,712
|
5
|
-
aicosmos_client-0.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
6
|
-
aicosmos_client-0.0.4.dist-info/licenses/LICENSE,sha256=XBdpsYae127l7YQyMSVQwUUo22FPis7sMts7oBjkN_g,1056
|
7
|
-
aicosmos_client-0.0.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|