Terminal-Assistant-Interface 1.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.
termilib/tools.py ADDED
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env python3
2
+ # Made by @Ericwasepic127
3
+
4
+ import httpx, json, asyncio, platformdirs, os, threading as t, uuid, time, datetime
5
+
6
+ CONFIGS = platformdirs.user_config_dir("TermiAI")
7
+ DATA = platformdirs.user_data_dir("TermiAI")
8
+ JOIN = os.path.join
9
+ LIST = os.listdir
10
+ CHATS = JOIN(CONFIGS, "chat.json")
11
+ APPCONFIG = None
12
+ CHATJSON = None
13
+ URL = None
14
+ BASEAIURL = "https://ai-proxy-%s.onrender.com/"
15
+ VERBOSE = False
16
+ THERROR = """<!doctype html>
17
+ <html lang=en>
18
+ <title>429 Too Many Requests</title>
19
+ <h1>Too Many Requests</h1>
20
+ <p>{&#34;error&#34;: {&#34;message&#34;: &#34;Too many requests!&#34;}}</p>"""
21
+
22
+ os.makedirs(CONFIGS, exist_ok=True)
23
+ os.makedirs(DATA, exist_ok=True)
24
+
25
+ def swrap(f):
26
+ def a(*a, **k):
27
+ res = f(*a, **k)
28
+ if VERBOSE and res:
29
+ try:
30
+ print("---", f.__name__, "---")
31
+ print(a, k)
32
+ print(res, repr(res), dir(res), type(res))
33
+ except:
34
+ ...
35
+ return res
36
+ return a
37
+
38
+ def awrap(f):
39
+ async def a(*a, **k):
40
+ res = await f(*a, **k)
41
+ if VERBOSE and res:
42
+ try:
43
+ print("---", f.__name__, "---")
44
+ print(a, k)
45
+ print(res, repr(res), dir(res), type(res))
46
+ except:
47
+ ...
48
+ return res
49
+ return a
50
+
51
+ @awrap
52
+ async def grab(url=None, method="GET", follow_redirects=True, **kwargs):
53
+ async with httpx.AsyncClient() as client:
54
+ return await getattr(client, method.lower())(url, follow_redirects=follow_redirects, **kwargs)
55
+
56
+ @awrap
57
+ async def ping(url=None):
58
+ if url is not None:
59
+ url = "https://%s-proxy-%s.onrender.com/" % url
60
+ else:
61
+ url = URL or BASEAIURL % "x"
62
+ return await grab(url, timeout=75)
63
+
64
+ @awrap
65
+ async def loop():
66
+ return await ping(), await ping(("search", "x"))
67
+
68
+ @awrap
69
+ async def get_current_time(local=True):
70
+ async with httpx.AsyncClient() as client:
71
+ try:
72
+ (1 / 0) if local else None
73
+ r = await client.get("https://currentdatetimepy.onrender.com/", timeout=10)
74
+ return r.text
75
+ except:
76
+ import datetime
77
+ return datetime.datetime.now(datetime.timezone.utc).ctime()
78
+
79
+ @awrap
80
+ async def run_command(command="", at="", cmd=""):
81
+ try:
82
+ process = await asyncio.create_subprocess_shell(
83
+ command or cmd,
84
+ stdout=asyncio.subprocess.PIPE,
85
+ stderr=asyncio.subprocess.PIPE,
86
+ stdin=asyncio.subprocess.DEVNULL,
87
+ cwd=at or None
88
+ )
89
+ stdout, stderr = await process.communicate()
90
+ return {"stdout": stdout.decode("utf-8", errors="replace"), "stderr": stderr.decode("utf-8", errors="replace"), "returncode": process.returncode}
91
+ except Exception as e:
92
+ return {"stdout": "", "stderr": getattr(type(e), "__name__", "<Unknown error>") + ": " + str(e), "returncode": 1}
93
+
94
+ @swrap
95
+ def createChatList():
96
+ if CHATJSON is None:
97
+ return []
98
+ give = []
99
+ for idcard, detail in CHATJSON.items():
100
+ give.append()
101
+
102
+ @swrap
103
+ def createChatName(default=None):
104
+ val = str(uuid.uuid4())
105
+ if CHATJSON is None:
106
+ return val
107
+ obj = JOIN(DATA, val + ".json")
108
+ CHATJSON[val] = ["New Chat", time.time()]
109
+ return val, LiveJson(obj, default_data=default)
110
+
111
+ @swrap
112
+ def loadChat(name):
113
+ return LiveJson(JOIN(DATA, name))
114
+
115
+ def append_message(role, message, var, raw=False):
116
+ var.append(message) if raw else var.append({"role": role, "content": message})
117
+
118
+ @awrap
119
+ async def post(data):
120
+ async with httpx.AsyncClient() as client:
121
+ try:
122
+ r = await client.post(URL or "https://ai-proxy-x.onrender.com/", content=json.dumps(data), headers={"Content-Type": "application/json"}, timeout=120)
123
+ m = r.json()
124
+ except json.JSONDecodeError as e:
125
+ try:
126
+ if ("<!doctype html>" in r.text and "<html>" in r.text and r.status_code == 429 and "error" in r.text) or THERROR in r.text.strip().rstrip():
127
+ m = {"error": {"message": "Too many requests per minute. Please wait about 1 minute"}}
128
+ else:
129
+ m = {"error": {"message": "DecodeError: " + r.text}}
130
+ except:
131
+ m = {"error": {"message": e.__class__.__name__ + ": " + str(e)}}
132
+ except (BaseException, Exception) as e:
133
+ m = {"error": {"message": e.__class__.__name__ + ": " + str(e)}}
134
+ return m
135
+
136
+ @awrap
137
+ async def instantSearch(**kwargs):
138
+ data = kwargs.get("q") or kwargs.get("query") or kwargs.get("prompt")
139
+ if not data:
140
+ return {"error": "No search query provided"}
141
+ async with httpx.AsyncClient() as client:
142
+ try:
143
+ r = await client.get("https://api.duckduckgo.com/", headers={"Content-Type": "application/json"}, timeout=120, params={"format": "json", "q": data})
144
+ m = r.json()
145
+ except json.JSONDecodeError:
146
+ m = {"error": {"message": "Decode failure on response JSON: " + r.text}}
147
+ except (BaseException, Exception) as e:
148
+ m = {"error": {"message": e.__class__.__name__ + ": " + str(e)}}
149
+ return m
150
+
151
+ @awrap
152
+ async def search(noredirectinsta=False, **kwargs):
153
+ data = kwargs.get("q") or kwargs.get("query") or kwargs.get("prompt")
154
+ count = int(kwargs.get("count", 5))
155
+ if not data:
156
+ return {"error": "No search query provided"}
157
+ if INSTANTSEARCH and not noredirectinsta:
158
+ return await instantSearch(q=data)
159
+ async with httpx.AsyncClient() as client:
160
+ try:
161
+ r = await client.post("https://search-proxy-x.onrender.com/", content=json.dumps({"query": data, "contents": {"text": True},"freshness": "noLimit", "count": count}), headers={"Content-Type": "application/json"}, timeout=120)
162
+ m = r.json()
163
+ except json.JSONDecodeError:
164
+ m = {"error": {"message": "Decode failure on response JSON: " + r.text}}
165
+ except (BaseException, Exception) as e:
166
+ m = {"error": {"message": e.__class__.__name__ + ": " + str(e)}}
167
+ return m
168
+
169
+ async def deepsearch(**kwargs):
170
+ return await search(**kwargs, noredirectinsta=True)
171
+
172
+ @swrap
173
+ def buildict(p, tools=None):
174
+ m = []
175
+ append_message("system", p, m)
176
+ r = {"messages": m}
177
+ if tools:
178
+ r["tools"] = tools
179
+ print(r)
180
+ return r
181
+
182
+ def deleteChat(name):
183
+ if name in CHATJSON or CHATJSON.get(name) is not None:
184
+ del CHATJSON[name]
185
+ file_path = JOIN(DATA, name + ".json")
186
+ if os.path.exists(file_path):
187
+ os.remove(file_path)
188
+
189
+ class LiveJson(dict):
190
+ def __init__(self, file_path="data.json", default_data=None):
191
+ self.file_path = file_path
192
+ self.lock = t.Lock()
193
+
194
+ initial_data = {}
195
+ if os.path.exists(self.file_path):
196
+ try:
197
+ with open(self.file_path, "r") as f:
198
+ initial_data = json.load(f)
199
+ except Exception:
200
+ initial_data = default_data or {}
201
+ else:
202
+ initial_data = default_data or {}
203
+ self._save_to_disk(initial_data)
204
+
205
+ super().__init__(initial_data)
206
+
207
+ def _save_to_disk(self, data):
208
+ try:
209
+ with open(self.file_path, "w") as f:
210
+ json.dump(data, f, indent=4)
211
+ except IOError:
212
+ pass
213
+
214
+ def __getitem__(self, key):
215
+ with self.lock:
216
+ return super().__getitem__(key)
217
+
218
+ def __setitem__(self, key, value):
219
+ with self.lock:
220
+ super().__setitem__(key, value)
221
+ current_state = dict(self.items())
222
+ self._save_to_disk(current_state)
223
+
224
+ def __delitem__(self, key):
225
+ with self.lock:
226
+ super().__delitem__(key)
227
+ current_state = dict(self.items())
228
+ self._save_to_disk(current_state)
229
+
230
+ def update(self, other=None, **kwargs):
231
+ with self.lock:
232
+ if other is not None:
233
+ if isinstance(other, dict):
234
+ for k, v in other.items():
235
+ if k in self and isinstance(self[k], dict) and isinstance(v, dict):
236
+ self[k].update(v)
237
+ else:
238
+ super().__setitem__(k, v)
239
+ else:
240
+ super().update(other)
241
+ if kwargs:
242
+ super().update(kwargs)
243
+
244
+ current_state = dict(self.items())
245
+ self._save_to_disk(current_state)
246
+
247
+ def reload(self):
248
+ with self.lock:
249
+ if os.path.exists(self.file_path):
250
+ try:
251
+ with open(self.file_path, "r") as f:
252
+ data = json.load(f)
253
+ super().clear()
254
+ super().update(data)
255
+ except Exception:
256
+ pass
257
+ def clone(self):
258
+ cloned = {}
259
+ for name, item in self.items():
260
+ cloned[name] = item
261
+ return cloned
262
+
263
+ class MemJson(dict):
264
+ def clone(self):
265
+ return MemJson(self)
266
+
267
+ class AsyncAI:
268
+ def __init__(self, jsonobj: LiveJson, sysprompt):
269
+ self.data = jsonobj
270
+ self.post = post
271
+ self.sysprompt = sysprompt
272
+ self.maxLimit = APPCONFIG.get("maxToolLoop", 4)
273
+ self.funcs = {}
274
+ self.tools = []
275
+
276
+ def removeTool(self, name):
277
+ if name in self.funcs:
278
+ self.tools = [t for t in self.tools if t["function"]["name"] != name]
279
+ del self.funcs[name]
280
+
281
+ def assignTool(self, asyncfunc, name, desc, params, needs=[]):
282
+ build = {"type": "function","function": {"parameters": {"type": "object","required": []}}}
283
+ build["function"]["name"] = name
284
+ build["function"]["description"] = desc
285
+ build["function"]["properties"] = params
286
+ build["function"]["required"] = needs
287
+ self.funcs[name] = asyncfunc
288
+ self.tools.append(build)
289
+
290
+ @awrap
291
+ async def send(self, m):
292
+ currentLoop = 0
293
+ dclon = {**self.data.clone(), "tools": self.tools} if self.tools else self.data.clone()
294
+ clone = [{"role": "system", "content": self.sysprompt}] + dclon.get("messages", [])
295
+ append_message("user", m, clone)
296
+ dclon["messages"] = clone
297
+ returned = await self.post(dclon)
298
+ c = returned.get("choices")
299
+ if not c:
300
+ append_message("assistant", returned.get("error").get("message"), clone)
301
+ self.data["messages"] = [c for c in clone if c.get("role") != "system"]
302
+ return returned.get("error").get("message")
303
+ airesponse = c[0]["message"]
304
+ append_message("", airesponse, clone, raw=True)
305
+ tool = c[0].get("finish_reason", "stop")
306
+ calls = airesponse.get("tool_calls", [])
307
+ while tool != "stop" and self.maxLimit > currentLoop and calls:
308
+ replica, coros = [], []
309
+ for call in calls:
310
+ fd = call.get("function")
311
+ func = self.funcs.get(fd.get("name"))
312
+ nm = fd.get("name")
313
+ arg = json.loads(fd.get("arguments"))
314
+ print(fd.get("name") + ":", arg) if VERBOSE else None
315
+ coros.append(func(**arg))
316
+ replica.append({"role": "tool", "tool_call_id": call.get("id")})
317
+ for index, res in enumerate(await asyncio.gather(*coros)):
318
+ replica[index]["content"] = res if isinstance(res, str) else (json.dumps(res) if isinstance(res, dict) else str(res))
319
+ for x in replica:
320
+ clone.append(x)
321
+ dclon["messages"] = clone
322
+ returned = await self.post(dclon)
323
+ c = returned.get("choices")
324
+ if not c:
325
+ append_message("assistant", returned.get("error").get("message"), clone)
326
+ self.data["messages"] = [c for c in clone if c.get("role") != "system"]
327
+ return returned.get("error").get("message")
328
+ airesponse = c[0]["message"]
329
+ append_message("", airesponse, clone, raw=True)
330
+ tool = c[0].get("finish_reason", "stop")
331
+ calls = airesponse.get("tool_calls", [])
332
+ currentLoop += 1
333
+ if tool == "tool_calls":
334
+ for call in calls:
335
+ clone.append({"role": "tool", "tool_call_id": call.get("id"), "content": "Too much tool calls: try again in next request - you cannot run any tool calls because you exceeding tool limit per one message"})
336
+ dclon["messages"] = clone
337
+ returned = await post(dclon)
338
+ c = returned.get("choices")
339
+ if not c:
340
+ tools.append_message("assistant", returned.get("error").get("message"), clone)
341
+ self.data["messages"] = [c for c in clone if c.get("role") != "system"]
342
+ return returned.get("error").get("message")
343
+ airesponse = c[0]["message"]
344
+ append_message("", airesponse, clone, raw=True)
345
+ self.data["messages"] = [c for c in clone if c.get("role") != "system"]
346
+ return airesponse.get("content") or airesponse
347
+
348
+ DEFAULTDATA = {
349
+ "dark": False,
350
+ "api": "x",
351
+ "search": True,
352
+ "custom": False,
353
+ "instantSearch": True,
354
+ "maxToolLoop": 4,
355
+ }
356
+
357
+ CHATJSON = LiveJson(CHATS)
358
+ APPCONFIG = LiveJson(JOIN(CONFIGS, "appcfg.json"), default_data=DEFAULTDATA)
359
+ URL = APPCONFIG.get("api", BASEAIURL % "x") if APPCONFIG.get("custom") else BASEAIURL % APPCONFIG.get("api", "x")
360
+ INSTANTSEARCH = APPCONFIG.get("instantSearch", True)
361
+
362
+ if __name__ == '__main__':
363
+ VERBOSE = True
364
+ asyncio.run(loop())
365
+ asyncio.run(deepsearch(q="What is capital of france"))
366
+ print(asyncio.run(post({"messages": [{"role": "user", "content": "Hi"}]})))
367
+ aichat = AsyncAI(LiveJson("test.json"), "Helpful AI Assistant called TermiAI")
368
+ aichat.assignTool(instantSearch, "ddgsearch", "Instant search API by duckduckgo, for quick lookups.", {"q": {"type": "string", "description": "Search query (no encode like test+example needed)"}}, ["q"])
369
+ aichat.assignTool(deepsearch, "deepsearch", "Deep search API by LangSearch, for deep and unfindable questions.", {"q": {"type": "string", "description": "Search query (no encode like test+example needed)"}, "count": {"type": "integer", "description": "Total counts"}}, ["q"])
370
+ aichat.assignTool(get_current_time, "currentutctime", "Get latest time", {"local": {"type": "boolean", "description": "Only set to False if you think local time is wrong (mostly no, so use true)"}}, ["local"])
371
+ aichat.assignTool(grab, "fetch", "Fetch things from given URL", {"url": {"type": "string", "description": "URL to fetch"}, "params": {"type": "object", "description": "Parameters"}}, ["url"])
372
+ print(asyncio.run(aichat.send("Hi! Can you get my local time clock in UTC please? And then grab latest Github News by that clock so u can fetch absalute latest!")))
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: Terminal-Assistant-Interface
3
+ Version: 1.0.6
4
+ Summary: Your personal AI assistant to help terminal
5
+ Author-email: Ericwasepic127 <ericwasepic127@outlook.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ericwasepic127/termiai
8
+ Project-URL: Issues, https://github.com/ericwasepic127/termiai/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: requests
15
+ Requires-Dist: platformdirs
16
+ Requires-Dist: httpx
17
+ Requires-Dist: flet[all]
18
+ Dynamic: license-file
19
+
20
+ ### TermiAI
21
+ An AI client do terminal job just for you :)
22
+ ### If you scared to type terminal commmand ...
23
+ Use This!! An AI will run commands just for you
@@ -0,0 +1,16 @@
1
+ termiai_main.py,sha256=VNIbUwpmv0BgmvRPwvw6OUThUtiZxDnuPRTKUCXAMso,134
2
+ termiai/__init__.py,sha256=y557UOxAN4RUClRn94sqV4VYcltthT7E8Ci8vGJ3c7U,52925
3
+ termiai/__main__.py,sha256=lTCaIM2LoD9rP9wlh5Z2HkCql5FYSYu72pfNKA0hWRo,132
4
+ termiai/clean.py,sha256=K89yLRHkeneyT4LxJhdkeIRDjhLFAclUoHpU-ave5aU,885
5
+ termilib/__init__.py,sha256=01Gl-B6Si0Gnexl0qIdfH8gmpOSZ5kjOw3kBLVxO2uQ,8609
6
+ termilib/__main__.py,sha256=gpQBHb5hd-1tOArTqbQ3MF7qmrsrymjEAbdD1hkNTHM,69
7
+ termilib/ftapi.py,sha256=_xMgbcNGmWp4MpPmC42Ty3AysuKo92L9SOSgY1pQ82s,8000
8
+ termilib/icon_data.py,sha256=nWzCHrg3OuWsbznwcJFmWrCubehTHwgDXgqgI3AxgMc,43911
9
+ termilib/test.py,sha256=oCIO1ln8GM0SI2dUHht_es6fl8cLSf1UcU1yMuh-Gew,4269
10
+ termilib/tools.py,sha256=z0lDPK9_ZKeWC8iyI7RslukPG6kAsSY8sEZWNvatcqg,14758
11
+ terminal_assistant_interface-1.0.6.dist-info/licenses/LICENSE,sha256=0Bl4Tc3Bc9GA-XopN9pIcteabWfnanUg2bWkc4LUS1o,1065
12
+ terminal_assistant_interface-1.0.6.dist-info/METADATA,sha256=sv3G30Xkoo1Ubbf-Xsb-apZ7uF1Pb1NSjuczkZwYHxA,797
13
+ terminal_assistant_interface-1.0.6.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
14
+ terminal_assistant_interface-1.0.6.dist-info/entry_points.txt,sha256=wKq2ohatv6trd8d3ycDq1lHt4qt5zgXn2OL4faT1crY,86
15
+ terminal_assistant_interface-1.0.6.dist-info/top_level.txt,sha256=X_BjXD8xIcYZ3sQudKvUb49g2T5gTxNjg6oL9Tzd5bo,30
16
+ terminal_assistant_interface-1.0.6.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ termiai = termiai:ftmain
3
+ termiai-factory-reset = termiai.clean:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erkhemee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ termiai
2
+ termiai_main
3
+ termilib