termchat-ai 0.1.0__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.
- aiterminal/__init__.py +4 -0
- aiterminal/client.py +81 -0
- aiterminal/main.py +64 -0
- aiterminal/terminal.py +15 -0
- termchat_ai-0.1.0.dist-info/METADATA +48 -0
- termchat_ai-0.1.0.dist-info/RECORD +8 -0
- termchat_ai-0.1.0.dist-info/WHEEL +4 -0
- termchat_ai-0.1.0.dist-info/entry_points.txt +2 -0
aiterminal/__init__.py
ADDED
aiterminal/client.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""HTTP client for the AITerminal Netlify Functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
DEFAULT_API_URL = "https://aiterminal-cloud.netlify.app"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AITerminalError(Exception):
|
|
14
|
+
"""Base exception for user-safe service errors."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ServiceError(AITerminalError):
|
|
18
|
+
"""The backend returned a safe, user-facing error."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AITerminalClient:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
base_url: str | None = None,
|
|
25
|
+
*,
|
|
26
|
+
transport: httpx.BaseTransport | None = None,
|
|
27
|
+
timeout: float = 35.0,
|
|
28
|
+
) -> None:
|
|
29
|
+
configured_url = base_url or os.getenv("AIT_TERMINAL_API_URL") or DEFAULT_API_URL
|
|
30
|
+
self.base_url = configured_url.rstrip("/")
|
|
31
|
+
parsed = urlparse(self.base_url)
|
|
32
|
+
if parsed.scheme != "https" and parsed.hostname not in {"localhost", "127.0.0.1", "::1"}:
|
|
33
|
+
raise ValueError("AIT_TERMINAL_API_URL must use HTTPS (HTTP is allowed only for localhost).")
|
|
34
|
+
if not parsed.netloc:
|
|
35
|
+
raise ValueError("AIT_TERMINAL_API_URL must be an absolute URL.")
|
|
36
|
+
self._http = httpx.Client(base_url=self.base_url, timeout=timeout, transport=transport)
|
|
37
|
+
|
|
38
|
+
def ask(self, message: str, history: list[dict[str, str]]) -> str:
|
|
39
|
+
try:
|
|
40
|
+
result = self._http.post(
|
|
41
|
+
"/.netlify/functions/chat",
|
|
42
|
+
json={"message": message, "history": history},
|
|
43
|
+
)
|
|
44
|
+
except httpx.TimeoutException as exc:
|
|
45
|
+
raise ServiceError("The request timed out. Please try again.") from exc
|
|
46
|
+
except httpx.RequestError as exc:
|
|
47
|
+
raise ServiceError("Could not connect to AITerminal. Check your internet connection and try again.") from exc
|
|
48
|
+
if result.status_code == 401:
|
|
49
|
+
raise ServiceError("The request was not accepted.")
|
|
50
|
+
if result.status_code == 429:
|
|
51
|
+
raise ServiceError(self._error_message(result, "Too many requests. Please wait and try again."))
|
|
52
|
+
if result.status_code >= 500:
|
|
53
|
+
raise ServiceError(self._error_message(result, "AITerminal is temporarily unavailable."))
|
|
54
|
+
if result.status_code != 200:
|
|
55
|
+
raise ServiceError(self._error_message(result, "The request was rejected."))
|
|
56
|
+
try:
|
|
57
|
+
payload = result.json()
|
|
58
|
+
except ValueError as exc:
|
|
59
|
+
raise ServiceError("AITerminal returned an invalid response.") from exc
|
|
60
|
+
answer = payload.get("answer") if isinstance(payload, dict) else None
|
|
61
|
+
if not isinstance(answer, str) or not answer.strip():
|
|
62
|
+
raise ServiceError("AITerminal returned an invalid response.")
|
|
63
|
+
return answer
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _error_message(result: httpx.Response, fallback: str) -> str:
|
|
67
|
+
try:
|
|
68
|
+
payload = result.json()
|
|
69
|
+
except ValueError:
|
|
70
|
+
return fallback
|
|
71
|
+
message = payload.get("error") if isinstance(payload, dict) else None
|
|
72
|
+
return message if isinstance(message, str) and message else fallback
|
|
73
|
+
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
self._http.close()
|
|
76
|
+
|
|
77
|
+
def __enter__(self) -> AITerminalClient:
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __exit__(self, *_: object) -> None:
|
|
81
|
+
self.close()
|
aiterminal/main.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Interactive AITerminal CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .client import AITerminalClient, AITerminalError
|
|
8
|
+
from .terminal import ask_question, console, show_answer
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_conversation(client: AITerminalClient) -> None:
|
|
14
|
+
history: list[dict[str, str]] = []
|
|
15
|
+
while True:
|
|
16
|
+
try:
|
|
17
|
+
question = ask_question().strip()
|
|
18
|
+
except (EOFError, KeyboardInterrupt):
|
|
19
|
+
console.print("\nGoodbye!")
|
|
20
|
+
return
|
|
21
|
+
command = question.lower()
|
|
22
|
+
if command in {"exit", "quit"}:
|
|
23
|
+
console.print("Goodbye!")
|
|
24
|
+
return
|
|
25
|
+
if command in {"clear", "/clear"}:
|
|
26
|
+
history.clear()
|
|
27
|
+
console.clear()
|
|
28
|
+
console.print("[bold]AITerminal ready![/bold]")
|
|
29
|
+
continue
|
|
30
|
+
if not question:
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
with console.status("Thinking...", spinner="dots"):
|
|
35
|
+
answer = client.ask(question, history)
|
|
36
|
+
except AITerminalError as exc:
|
|
37
|
+
console.print(f"[bold red]{exc}[/bold red]")
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
show_answer(answer)
|
|
41
|
+
history.extend(({"role": "user", "content": question}, {"role": "assistant", "content": answer}))
|
|
42
|
+
del history[:-20]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.callback(invoke_without_command=True)
|
|
46
|
+
def main() -> None:
|
|
47
|
+
"""Ask questions of the AITerminal cloud service."""
|
|
48
|
+
console.print("Connecting to AITerminal Cloud...")
|
|
49
|
+
try:
|
|
50
|
+
with AITerminalClient() as client:
|
|
51
|
+
console.print("[bold]AITerminal ready![/bold]")
|
|
52
|
+
run_conversation(client)
|
|
53
|
+
except KeyboardInterrupt:
|
|
54
|
+
console.print("\nGoodbye!")
|
|
55
|
+
except ValueError as exc:
|
|
56
|
+
console.print(f"[bold red]{exc}[/bold red]")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run() -> None:
|
|
60
|
+
app()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
run()
|
aiterminal/terminal.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Small Rich helpers for the interactive terminal."""
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.markdown import Markdown
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ask_question() -> str:
|
|
10
|
+
return console.input("[bold cyan]You> [/bold cyan]")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def show_answer(answer: str) -> None:
|
|
14
|
+
console.print("[bold green]AI>[/bold green]")
|
|
15
|
+
console.print(Markdown(answer))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: termchat-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A no-login terminal client for the AITerminal cloud AI service
|
|
5
|
+
Author: AITerminal contributors
|
|
6
|
+
Keywords: ai,cli,groq,netlify,terminal
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: httpx<1,>=0.27
|
|
15
|
+
Requires-Dist: rich<15,>=13.7
|
|
16
|
+
Requires-Dist: typer<1,>=0.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# AITerminal CLI
|
|
20
|
+
|
|
21
|
+
This package provides the `aiterminal` terminal command. It connects directly to the public AITerminal chat function; it does not ask users to log in and does not contain a Groq API key.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
The package is named `termchat-ai` and is not published yet. After publication:
|
|
26
|
+
|
|
27
|
+
```powershell
|
|
28
|
+
py -m pip install termchat-ai
|
|
29
|
+
aiterminal
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Linux and macOS:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
python -m pip install termchat-ai
|
|
36
|
+
aiterminal
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For development from a checkout:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
python -m pip install ./cli
|
|
43
|
+
aiterminal
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The client uses `https://aiterminal-cloud.netlify.app` by default. Override with `AIT_TERMINAL_API_URL` if needed.
|
|
47
|
+
|
|
48
|
+
Use `clear` or `/clear` to clear the screen and temporary conversation context; use `exit` or `quit` to end the session. Context is memory-only and is discarded when the process exits.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
aiterminal/__init__.py,sha256=CLC7Fx1FlEGxFn-3Jr3LtxuhNV5qdEWdnjKDd-THCWQ,66
|
|
2
|
+
aiterminal/client.py,sha256=VUjr5OLViClDc_lyH6uGYQS98szSlDf15n5_0U1AphE,3269
|
|
3
|
+
aiterminal/main.py,sha256=4NaeO9zhHxC_JMITGz8-Iz00PAHgWyA2QKMvlT-vn8c,1924
|
|
4
|
+
aiterminal/terminal.py,sha256=MlkQx6zEq4Akn01X7dl_KdA3S7qcmseFj9YQHmE72WM,372
|
|
5
|
+
termchat_ai-0.1.0.dist-info/METADATA,sha256=UMSfCjbUZ4o8w0juc3Sozzb9a2BLwpfGjPt3j7SAGcw,1467
|
|
6
|
+
termchat_ai-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
7
|
+
termchat_ai-0.1.0.dist-info/entry_points.txt,sha256=RHuE5f3T4cILXBMlGSVKe9CtfvazOPZSU8MDzP6ZvS4,51
|
|
8
|
+
termchat_ai-0.1.0.dist-info/RECORD,,
|