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 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 (Button, Footer, Header, Input, Label, ListItem,
11
- ListView, LoadingIndicator, RichLog, Static)
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
- self.sessions, message = self.app.client.get_my_sessions()
170
-
171
- if message != "Success":
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"{message}"), id="no-sessions")
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
- session_id, message = self.app.client.create_session()
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
- if session_id:
199
- self.app.current_session = session_id
200
- self.app.uninstall_screen("chat")
201
- self.app.install_screen(ChatScreen(), "chat")
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
- self.conversation, message = self.app.client.get_session_history(
393
- self.app.current_session
394
- )
395
- if message != "Success":
396
- self.query_one("#chat-log").write(f"[red]Error: {message}[/red]")
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
- conversation_history, message = await asyncio.to_thread(
427
- self.app.client.chat, self.app.current_session, message
428
- )
429
-
430
- if message != "Success":
431
- self.query_one("#chat-log").write(f"[red]Error: {message}[/red]")
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__(self, base_url: str, username: str, password: str):
6
- self.session = requests.Session()
7
- self.base_url = base_url
8
- self.username: str = username
9
- self.password: str = password
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
- login, message = self._login()
13
- if not login:
14
- raise ValueError(f"Failed to login. {message}")
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
- try:
24
- response = self.session.post(
25
- f"{self.base_url}/user/login", data=login_data, headers=headers
26
- )
27
- if response.status_code == 200:
28
- token_data = response.json()
29
- self.access_token = token_data["access_token"]
30
- return True, "Success"
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
- try:
46
- response = self.session.get(
47
- f"{self.base_url}/sessions/{session_id}/status",
48
- headers=self._get_auth_headers(),
49
- )
50
- success = response.status_code == 200
51
- if success:
52
- return response.json(), "Success"
53
- else:
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
- if not self.access_token:
60
- raise ValueError("Not logged in")
61
- try:
62
- response = self.session.post(
63
- f"{self.base_url}/sessions/create", headers=self._get_auth_headers()
64
- )
65
- if response.status_code == 200:
66
- response_json = response.json()
67
- return response_json["session_id"], "Success"
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
- if not self.access_token:
75
- raise ValueError("Not logged in")
76
- try:
77
- response = self.session.delete(
78
- f"{self.base_url}/sessions/{session_id}",
79
- headers=self._get_auth_headers(),
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
- if not self.access_token:
90
- raise ValueError("Not logged in")
91
- try:
92
- response = self.session.get(
93
- f"{self.base_url}/sessions/my_sessions",
94
- headers=self._get_auth_headers(),
95
- )
96
- if response.status_code == 200:
97
- sessions = response.json()
98
- self.active_sessions = sessions
99
- return [
100
- {
101
- "session_id": session["session_id"],
102
- "title": session["environment_info"].get("title", None),
103
- }
104
- for session in sessions
105
- ], "Success"
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, message = self._get_session_status(session_id)
113
- if not session:
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
- if not self.access_token:
120
- raise ValueError("Not logged in")
121
- data = {
122
- "user_input": prompt,
123
- "session_id": session_id,
124
- }
125
- try:
126
- response = self.session.post(
127
- f"{self.base_url}/chat",
128
- json=data,
129
- headers=self._get_auth_headers(),
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,,