gink-ai 0.1.0__tar.gz

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.
gink_ai-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: gink-ai
3
+ Version: 0.1.0
4
+ Summary: Gink in your terminal: ask, chat, search the web and make images with your Gink API key.
5
+ Author: Gink AI
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://amgib00.github.io/GinkAssistant/
8
+ Project-URL: Source, https://github.com/Amgib00/GinkAssistant
9
+ Keywords: gink,ai,assistant,cli,chat
10
+ Classifier: Environment :: Console
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: httpx>=0.25
16
+
17
+ # Gink in your terminal
18
+
19
+ ```bash
20
+ pip install gink-ai
21
+ gink login # paste your API key (Gink > Settings > Developers)
22
+ gink "What's the weather in Dakar?"
23
+ gink # chat: /new, /model NAME, /models, /search on|off, /exit
24
+ cat notes.txt | gink "summarise this"
25
+ gink -s "latest news in Senegal" # search the web (with sources)
26
+ gink --image "a baobab at sunset" -o tree.png
27
+ gink models
28
+ ```
29
+
30
+ Your key can also come from `GINK_API_KEY`; another server from `GINK_BASE_URL`
31
+ (default `https://gink-server.onrender.com/v1`). `gink logout` forgets the saved key.
@@ -0,0 +1,15 @@
1
+ # Gink in your terminal
2
+
3
+ ```bash
4
+ pip install gink-ai
5
+ gink login # paste your API key (Gink > Settings > Developers)
6
+ gink "What's the weather in Dakar?"
7
+ gink # chat: /new, /model NAME, /models, /search on|off, /exit
8
+ cat notes.txt | gink "summarise this"
9
+ gink -s "latest news in Senegal" # search the web (with sources)
10
+ gink --image "a baobab at sunset" -o tree.png
11
+ gink models
12
+ ```
13
+
14
+ Your key can also come from `GINK_API_KEY`; another server from `GINK_BASE_URL`
15
+ (default `https://gink-server.onrender.com/v1`). `gink logout` forgets the saved key.
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: gink-ai
3
+ Version: 0.1.0
4
+ Summary: Gink in your terminal: ask, chat, search the web and make images with your Gink API key.
5
+ Author: Gink AI
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://amgib00.github.io/GinkAssistant/
8
+ Project-URL: Source, https://github.com/Amgib00/GinkAssistant
9
+ Keywords: gink,ai,assistant,cli,chat
10
+ Classifier: Environment :: Console
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: httpx>=0.25
16
+
17
+ # Gink in your terminal
18
+
19
+ ```bash
20
+ pip install gink-ai
21
+ gink login # paste your API key (Gink > Settings > Developers)
22
+ gink "What's the weather in Dakar?"
23
+ gink # chat: /new, /model NAME, /models, /search on|off, /exit
24
+ cat notes.txt | gink "summarise this"
25
+ gink -s "latest news in Senegal" # search the web (with sources)
26
+ gink --image "a baobab at sunset" -o tree.png
27
+ gink models
28
+ ```
29
+
30
+ Your key can also come from `GINK_API_KEY`; another server from `GINK_BASE_URL`
31
+ (default `https://gink-server.onrender.com/v1`). `gink logout` forgets the saved key.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ gink_ai.egg-info/PKG-INFO
4
+ gink_ai.egg-info/SOURCES.txt
5
+ gink_ai.egg-info/dependency_links.txt
6
+ gink_ai.egg-info/entry_points.txt
7
+ gink_ai.egg-info/requires.txt
8
+ gink_ai.egg-info/top_level.txt
9
+ gink_cli/__init__.py
10
+ gink_cli/__main__.py
11
+ gink_cli/main.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gink = gink_cli.main:main
@@ -0,0 +1 @@
1
+ httpx>=0.25
@@ -0,0 +1 @@
1
+ gink_cli
@@ -0,0 +1,2 @@
1
+ """Gink in your terminal."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,294 @@
1
+ """
2
+ gink -- Gink in your terminal (the Gink Developer API, OpenAI-compatible).
3
+
4
+ gink "question" one answer, streamed
5
+ gink a chat (type /help inside)
6
+ cat file | gink "summarise" send a file's text with the question
7
+ gink -s "question" search the web (answers list their sources)
8
+ gink --image "prompt" -o x.png make an image
9
+ gink login | logout | models | version
10
+ """
11
+ import argparse
12
+ import base64
13
+ import getpass
14
+ import json
15
+ import os
16
+ import sys
17
+ import time
18
+
19
+ from . import __version__
20
+
21
+ DEFAULT_BASE = "https://gink-server.onrender.com/v1"
22
+ MAX_PIPED_CHARS = 200_000
23
+
24
+
25
+ class GinkError(Exception):
26
+ pass
27
+
28
+
29
+ # ---------------------------------------------------------------- settings
30
+ def config_path() -> str:
31
+ if os.name == "nt":
32
+ root = os.environ.get("APPDATA") or os.path.expanduser("~")
33
+ else:
34
+ root = os.environ.get("XDG_CONFIG_HOME") or os.path.join(os.path.expanduser("~"), ".config")
35
+ return os.path.join(root, "gink", "config.json")
36
+
37
+
38
+ def load_config() -> dict:
39
+ try:
40
+ with open(config_path(), encoding="utf-8") as f:
41
+ return json.load(f)
42
+ except (OSError, ValueError):
43
+ return {}
44
+
45
+
46
+ def save_config(cfg: dict) -> None:
47
+ path = config_path()
48
+ os.makedirs(os.path.dirname(path), exist_ok=True)
49
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) # only you can read your key
50
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
51
+ json.dump(cfg, f, indent=1)
52
+ try:
53
+ os.chmod(path, 0o600)
54
+ except OSError:
55
+ pass
56
+
57
+
58
+ def settings():
59
+ cfg = load_config()
60
+ key = os.environ.get("GINK_API_KEY") or cfg.get("api_key") or ""
61
+ base = (os.environ.get("GINK_BASE_URL") or cfg.get("base_url") or DEFAULT_BASE).rstrip("/")
62
+ return key, base
63
+
64
+
65
+ # ---------------------------------------------------------------- talking to Gink
66
+ def _client(key: str, timeout: float = 120):
67
+ import httpx
68
+ return httpx.Client(timeout=httpx.Timeout(timeout, connect=30),
69
+ headers={"Authorization": f"Bearer {key}", "User-Agent": f"gink-cli/{__version__}"})
70
+
71
+
72
+ def _explain(r) -> GinkError:
73
+ try:
74
+ body = r.json()
75
+ msg = (body.get("error") or {}).get("message") if isinstance(body.get("error"), dict) else body.get("detail")
76
+ except Exception:
77
+ msg = ""
78
+ if r.status_code == 401:
79
+ return GinkError("Your API key isn't valid (or was revoked). Run: gink login")
80
+ if r.status_code == 429:
81
+ wait = r.headers.get("retry-after")
82
+ return GinkError((msg or "Limit reached.") + (f" Try again in {wait} seconds." if wait else ""))
83
+ return GinkError(msg or f"The Gink server answered {r.status_code}.")
84
+
85
+
86
+ def _network(exc) -> GinkError:
87
+ import httpx
88
+ if isinstance(exc, httpx.TimeoutException):
89
+ return GinkError("The Gink server didn't answer in time. If it was asleep it can take up to a minute "
90
+ "to wake up \u2014 please try again.")
91
+ return GinkError(f"Can't reach the Gink server ({type(exc).__name__}). Check your connection.")
92
+
93
+
94
+ def need_key(key: str):
95
+ if not key:
96
+ raise GinkError("No API key yet. Create one in Gink (Settings > Developers), then run: gink login")
97
+
98
+
99
+ def list_models(key: str, base: str) -> list:
100
+ need_key(key)
101
+ import httpx
102
+ try:
103
+ with _client(key, 90) as c:
104
+ r = c.get(base + "/models")
105
+ except httpx.HTTPError as exc:
106
+ raise _network(exc)
107
+ if r.status_code != 200:
108
+ raise _explain(r)
109
+ return [m["id"] for m in r.json().get("data", [])]
110
+
111
+
112
+ def stream_answer(key: str, base: str, messages: list, model: str = "", out=None) -> str:
113
+ """Send the chat; print the answer as it arrives; return the full text."""
114
+ need_key(key)
115
+ import httpx
116
+ out = out or sys.stdout
117
+ body = {"messages": messages, "stream": True}
118
+ if model:
119
+ body["model"] = model
120
+ text = []
121
+ try:
122
+ with _client(key) as c, c.stream("POST", base + "/chat/completions", json=body) as r:
123
+ if r.status_code != 200:
124
+ r.read()
125
+ raise _explain(r)
126
+ for line in r.iter_lines():
127
+ if not line.startswith("data:"):
128
+ continue
129
+ data = line[5:].strip()
130
+ if data == "[DONE]":
131
+ break
132
+ try:
133
+ chunk = json.loads(data)
134
+ except ValueError:
135
+ continue
136
+ if chunk.get("error"):
137
+ raise GinkError(chunk["error"].get("message") or "The AI service had a problem.")
138
+ piece = ((chunk.get("choices") or [{}])[0].get("delta") or {}).get("content") or ""
139
+ if piece:
140
+ text.append(piece)
141
+ out.write(piece)
142
+ out.flush()
143
+ except httpx.HTTPError as exc:
144
+ raise _network(exc)
145
+ out.write("\n")
146
+ out.flush()
147
+ return "".join(text)
148
+
149
+
150
+ def make_image(key: str, base: str, prompt: str, path: str) -> str:
151
+ need_key(key)
152
+ import httpx
153
+ try:
154
+ with _client(key, 180) as c:
155
+ r = c.post(base + "/images/generations", json={"prompt": prompt})
156
+ except httpx.HTTPError as exc:
157
+ raise _network(exc)
158
+ if r.status_code != 200:
159
+ raise _explain(r)
160
+ data = r.json()["data"][0]["b64_json"]
161
+ with open(path, "wb") as f:
162
+ f.write(base64.b64decode(data))
163
+ return path
164
+
165
+
166
+ # ---------------------------------------------------------------- the commands
167
+ def with_piped_text(question: str, piped: str) -> str:
168
+ if not piped:
169
+ return question
170
+ if len(piped) > MAX_PIPED_CHARS:
171
+ piped = piped[:MAX_PIPED_CHARS] + "\n[... cut: only the first 200,000 characters were sent]"
172
+ return (f'<attached_file name="input">\n{piped}\n</attached_file>\n\n'
173
+ + (question or "Please read this and summarise the important points."))
174
+
175
+
176
+ def chat(key: str, base: str, model: str, system: str = "") -> int:
177
+ """An interactive chat. /help for commands; Ctrl+C stops an answer, Ctrl+D (or /exit) leaves."""
178
+ need_key(key)
179
+ color = sys.stdout.isatty() and not os.environ.get("NO_COLOR")
180
+ you = "\033[1;32myou \u203a\033[0m " if color else "you > "
181
+ history = [{"role": "system", "content": system}] if system else []
182
+ print(f"Gink chat \u00b7 {model or 'default model'} \u00b7 /help for commands, /exit to leave")
183
+ while True:
184
+ try:
185
+ line = input(you).strip()
186
+ except EOFError:
187
+ print()
188
+ return 0
189
+ except KeyboardInterrupt:
190
+ print()
191
+ return 0
192
+ if not line:
193
+ continue
194
+ if line.startswith("/"):
195
+ cmd, _, arg = line[1:].partition(" ")
196
+ if cmd in ("exit", "quit", "q"):
197
+ return 0
198
+ if cmd == "new":
199
+ history = history[:1] if system else []
200
+ print("New chat.")
201
+ elif cmd == "model":
202
+ model = arg.strip()
203
+ print(f"Model: {model or 'default'}")
204
+ elif cmd == "search":
205
+ model = "gink-search" if arg.strip() != "off" else ""
206
+ print("Web search on." if model == "gink-search" else "Web search off.")
207
+ elif cmd == "models":
208
+ try:
209
+ print("\n".join(list_models(key, base)))
210
+ except GinkError as exc:
211
+ print(f"gink: {exc}")
212
+ else:
213
+ print("/new start over \u00b7 /model NAME \u00b7 /models \u00b7 /search on|off \u00b7 /exit")
214
+ continue
215
+ history.append({"role": "user", "content": line})
216
+ try:
217
+ answer = stream_answer(key, base, history[-30:], model)
218
+ history.append({"role": "assistant", "content": answer})
219
+ except KeyboardInterrupt:
220
+ print("\n(stopped)")
221
+ history.pop()
222
+ except GinkError as exc:
223
+ print(f"gink: {exc}")
224
+ history.pop()
225
+
226
+
227
+ def login(base: str) -> int:
228
+ print("Paste your Gink API key (Gink > Settings > Developers > Create key). It starts with gink_live_")
229
+ key = getpass.getpass("API key: ").strip()
230
+ if not key.startswith("gink_live_"):
231
+ print("gink: that doesn't look like a Gink API key (it should start with gink_live_).")
232
+ return 1
233
+ models = list_models(key, base) # check it works before saving it
234
+ cfg = load_config()
235
+ cfg["api_key"] = key
236
+ if base != DEFAULT_BASE:
237
+ cfg["base_url"] = base
238
+ save_config(cfg)
239
+ print(f"Signed in. {len(models)} models available. Try: gink \"hello\"")
240
+ return 0
241
+
242
+
243
+ def main(argv=None) -> int:
244
+ argv = list(sys.argv[1:] if argv is None else argv)
245
+ key, base = settings()
246
+ try:
247
+ if argv[:1] == ["login"]:
248
+ return login(base)
249
+ if argv[:1] == ["logout"]:
250
+ cfg = load_config()
251
+ cfg.pop("api_key", None)
252
+ save_config(cfg)
253
+ print("Signed out: your API key was removed from this computer.")
254
+ return 0
255
+ if argv[:1] == ["models"]:
256
+ print("\n".join(list_models(key, base)))
257
+ return 0
258
+ if argv[:1] in (["version"], ["--version"]):
259
+ print(f"gink {__version__}")
260
+ return 0
261
+
262
+ ap = argparse.ArgumentParser(prog="gink", description="Gink in your terminal.",
263
+ epilog="Also: gink login | logout | models | version")
264
+ ap.add_argument("question", nargs="*", help="what to ask (leave out for a chat)")
265
+ ap.add_argument("-m", "--model", default="", help="a model from 'gink models' (default: your account's)")
266
+ ap.add_argument("-s", "--search", action="store_true", help="search the web; answers list their sources")
267
+ ap.add_argument("--system", default="", help="instructions for Gink in this conversation")
268
+ ap.add_argument("--image", metavar="PROMPT", help="make an image instead of answering")
269
+ ap.add_argument("-o", "--output", default="", help="where to save the image (default gink-image-<time>.png)")
270
+ a = ap.parse_args(argv)
271
+ model = "gink-search" if a.search else a.model
272
+
273
+ if a.image:
274
+ path = make_image(key, base, a.image, a.output or f"gink-image-{int(time.time())}.png")
275
+ print(f"Saved {path}")
276
+ return 0
277
+ question = " ".join(a.question).strip()
278
+ piped = "" if sys.stdin is None or sys.stdin.isatty() else sys.stdin.read()
279
+ if not question and not piped:
280
+ return chat(key, base, model, a.system)
281
+ messages = ([{"role": "system", "content": a.system}] if a.system else []) + \
282
+ [{"role": "user", "content": with_piped_text(question, piped)}]
283
+ stream_answer(key, base, messages, model)
284
+ return 0
285
+ except GinkError as exc:
286
+ print(f"gink: {exc}", file=sys.stderr)
287
+ return 1
288
+ except KeyboardInterrupt:
289
+ print("\n(stopped)", file=sys.stderr)
290
+ return 130
291
+
292
+
293
+ if __name__ == "__main__":
294
+ raise SystemExit(main())
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gink-ai"
7
+ version = "0.1.0"
8
+ description = "Gink in your terminal: ask, chat, search the web and make images with your Gink API key."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{ name = "Gink AI" }]
13
+ dependencies = ["httpx>=0.25"]
14
+ keywords = ["gink", "ai", "assistant", "cli", "chat"]
15
+ classifiers = ["Environment :: Console", "Programming Language :: Python :: 3", "Operating System :: OS Independent"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://amgib00.github.io/GinkAssistant/"
19
+ Source = "https://github.com/Amgib00/GinkAssistant"
20
+
21
+ [project.scripts]
22
+ gink = "gink_cli.main:main"
23
+
24
+ [tool.setuptools]
25
+ packages = ["gink_cli"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+