p-ai.token 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.
- p_ai_token-0.1.0/MANIFEST.in +5 -0
- p_ai_token-0.1.0/PKG-INFO +28 -0
- p_ai_token-0.1.0/README.md +19 -0
- p_ai_token-0.1.0/p_ai.token.egg-info/PKG-INFO +28 -0
- p_ai_token-0.1.0/p_ai.token.egg-info/SOURCES.txt +12 -0
- p_ai_token-0.1.0/p_ai.token.egg-info/dependency_links.txt +1 -0
- p_ai_token-0.1.0/p_ai.token.egg-info/requires.txt +1 -0
- p_ai_token-0.1.0/p_ai.token.egg-info/top_level.txt +1 -0
- p_ai_token-0.1.0/pyproject.toml +23 -0
- p_ai_token-0.1.0/setup.cfg +4 -0
- p_ai_token-0.1.0/thonnycontrib/api.txt +7 -0
- p_ai_token-0.1.0/thonnycontrib/api.txt.example +1 -0
- p_ai_token-0.1.0/thonnycontrib/backend/ai_request_backend.py +107 -0
- p_ai_token-0.1.0/thonnycontrib/thonny_ai_request.py +118 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: p-ai.token
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Experimental AI request plugin for Thonny
|
|
5
|
+
Keywords: thonny,plugin,ai
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: thonny<6.0,>=4.0
|
|
9
|
+
|
|
10
|
+
# Thonny AI Request — FIX 7
|
|
11
|
+
|
|
12
|
+
## Cài đặt
|
|
13
|
+
1. Đóng hoàn toàn Thonny.
|
|
14
|
+
2. Giải nén ZIP.
|
|
15
|
+
3. (Tuỳ chọn) Thêm API key vào `thonnycontrib/api.txt`, mỗi key một dòng.
|
|
16
|
+
4. Chạy `install.cmd`.
|
|
17
|
+
5. Mở lại Thonny.
|
|
18
|
+
|
|
19
|
+
Installer dọn bản cũ của riêng extension rồi cài bản mới. Nếu `api.txt` trong ZIP có key, file đó được chép vào thư mục cài đặt. Nếu file chỉ có chú thích, key đã cài trên máy sẽ được giữ nguyên; nếu chưa có file thì template được chép vào. Đọc thông báo của installer để biết đường dẫn cấu hình thực tế.
|
|
20
|
+
|
|
21
|
+
Thử chạy trong một file Python:
|
|
22
|
+
```python
|
|
23
|
+
request("Xin chào! Trả lời bằng tiếng Việt.")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Khi thành công, Shell chỉ hiển thị link `Click here` màu `#F2F2F2`, gạch chân. Nhấn link để chép câu trả lời vào clipboard. Câu trả lời không được in trực tiếp ra Shell.
|
|
27
|
+
|
|
28
|
+
Ghi chú: đây là prototype; cần xác nhận tương thích với phiên bản Thonny đang cài.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Thonny AI Request — FIX 7
|
|
2
|
+
|
|
3
|
+
## Cài đặt
|
|
4
|
+
1. Đóng hoàn toàn Thonny.
|
|
5
|
+
2. Giải nén ZIP.
|
|
6
|
+
3. (Tuỳ chọn) Thêm API key vào `thonnycontrib/api.txt`, mỗi key một dòng.
|
|
7
|
+
4. Chạy `install.cmd`.
|
|
8
|
+
5. Mở lại Thonny.
|
|
9
|
+
|
|
10
|
+
Installer dọn bản cũ của riêng extension rồi cài bản mới. Nếu `api.txt` trong ZIP có key, file đó được chép vào thư mục cài đặt. Nếu file chỉ có chú thích, key đã cài trên máy sẽ được giữ nguyên; nếu chưa có file thì template được chép vào. Đọc thông báo của installer để biết đường dẫn cấu hình thực tế.
|
|
11
|
+
|
|
12
|
+
Thử chạy trong một file Python:
|
|
13
|
+
```python
|
|
14
|
+
request("Xin chào! Trả lời bằng tiếng Việt.")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Khi thành công, Shell chỉ hiển thị link `Click here` màu `#F2F2F2`, gạch chân. Nhấn link để chép câu trả lời vào clipboard. Câu trả lời không được in trực tiếp ra Shell.
|
|
18
|
+
|
|
19
|
+
Ghi chú: đây là prototype; cần xác nhận tương thích với phiên bản Thonny đang cài.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: p-ai.token
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Experimental AI request plugin for Thonny
|
|
5
|
+
Keywords: thonny,plugin,ai
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: thonny<6.0,>=4.0
|
|
9
|
+
|
|
10
|
+
# Thonny AI Request — FIX 7
|
|
11
|
+
|
|
12
|
+
## Cài đặt
|
|
13
|
+
1. Đóng hoàn toàn Thonny.
|
|
14
|
+
2. Giải nén ZIP.
|
|
15
|
+
3. (Tuỳ chọn) Thêm API key vào `thonnycontrib/api.txt`, mỗi key một dòng.
|
|
16
|
+
4. Chạy `install.cmd`.
|
|
17
|
+
5. Mở lại Thonny.
|
|
18
|
+
|
|
19
|
+
Installer dọn bản cũ của riêng extension rồi cài bản mới. Nếu `api.txt` trong ZIP có key, file đó được chép vào thư mục cài đặt. Nếu file chỉ có chú thích, key đã cài trên máy sẽ được giữ nguyên; nếu chưa có file thì template được chép vào. Đọc thông báo của installer để biết đường dẫn cấu hình thực tế.
|
|
20
|
+
|
|
21
|
+
Thử chạy trong một file Python:
|
|
22
|
+
```python
|
|
23
|
+
request("Xin chào! Trả lời bằng tiếng Việt.")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Khi thành công, Shell chỉ hiển thị link `Click here` màu `#F2F2F2`, gạch chân. Nhấn link để chép câu trả lời vào clipboard. Câu trả lời không được in trực tiếp ra Shell.
|
|
27
|
+
|
|
28
|
+
Ghi chú: đây là prototype; cần xác nhận tương thích với phiên bản Thonny đang cài.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
p_ai.token.egg-info/PKG-INFO
|
|
5
|
+
p_ai.token.egg-info/SOURCES.txt
|
|
6
|
+
p_ai.token.egg-info/dependency_links.txt
|
|
7
|
+
p_ai.token.egg-info/requires.txt
|
|
8
|
+
p_ai.token.egg-info/top_level.txt
|
|
9
|
+
thonnycontrib/api.txt
|
|
10
|
+
thonnycontrib/api.txt.example
|
|
11
|
+
thonnycontrib/thonny_ai_request.py
|
|
12
|
+
thonnycontrib/backend/ai_request_backend.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
thonny<6.0,>=4.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
thonnycontrib
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "p-ai.token"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Experimental AI request plugin for Thonny"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = ["thonny>=4.0,<6.0"]
|
|
12
|
+
keywords = ["thonny", "plugin", "ai"]
|
|
13
|
+
|
|
14
|
+
[tool.setuptools]
|
|
15
|
+
include-package-data = true
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["."]
|
|
19
|
+
include = ["thonnycontrib*"]
|
|
20
|
+
namespaces = true
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.package-data]
|
|
23
|
+
thonnycontrib = ["api.txt", "api.txt.example"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Add one RevidAPI key per line to a local api.txt file after installation.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Backend plugin implementing request(question) for Thonny scripts."""
|
|
2
|
+
import base64
|
|
3
|
+
import builtins
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import urllib.request
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
MODEL = "claude-sonnet-5"
|
|
10
|
+
API_URL = "https://revidapi.com/v1/chat/completions"
|
|
11
|
+
MAX_TOKENS = 1200
|
|
12
|
+
TIMEOUT_SECONDS = 45
|
|
13
|
+
START = "__THONNY_AI_REQUEST_BEGIN__"
|
|
14
|
+
END = "__THONNY_AI_REQUEST_END__"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _read_keys():
|
|
18
|
+
key_file = Path(__file__).resolve().parents[1] / "api.txt"
|
|
19
|
+
try:
|
|
20
|
+
lines = key_file.read_text(encoding="utf-8-sig").splitlines()
|
|
21
|
+
except Exception:
|
|
22
|
+
return []
|
|
23
|
+
return [line.strip() for line in lines if line.strip() and not line.lstrip().startswith("#")]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _extract_text(data):
|
|
27
|
+
try:
|
|
28
|
+
content = data["choices"][0]["message"]["content"]
|
|
29
|
+
except (KeyError, IndexError, TypeError):
|
|
30
|
+
return ""
|
|
31
|
+
if isinstance(content, str):
|
|
32
|
+
return content.strip()
|
|
33
|
+
if isinstance(content, list):
|
|
34
|
+
parts = []
|
|
35
|
+
for block in content:
|
|
36
|
+
if isinstance(block, dict) and isinstance(block.get("text"), str):
|
|
37
|
+
parts.append(block["text"])
|
|
38
|
+
return "".join(parts).strip()
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ask_api(question, keys):
|
|
43
|
+
payload = json.dumps(
|
|
44
|
+
{
|
|
45
|
+
"model": MODEL,
|
|
46
|
+
"messages": [
|
|
47
|
+
{"role": "system", "content": "Answer clearly and concisely in the language used by the user."},
|
|
48
|
+
{"role": "user", "content": question},
|
|
49
|
+
],
|
|
50
|
+
"max_tokens": MAX_TOKENS,
|
|
51
|
+
"temperature": 0.3,
|
|
52
|
+
},
|
|
53
|
+
ensure_ascii=False,
|
|
54
|
+
).encode("utf-8")
|
|
55
|
+
|
|
56
|
+
for key in keys:
|
|
57
|
+
try:
|
|
58
|
+
req = urllib.request.Request(
|
|
59
|
+
API_URL,
|
|
60
|
+
data=payload,
|
|
61
|
+
method="POST",
|
|
62
|
+
headers={
|
|
63
|
+
"Authorization": "Bearer " + key,
|
|
64
|
+
"X-API-Key": key,
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS) as response:
|
|
69
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
70
|
+
answer = _extract_text(data)
|
|
71
|
+
if answer:
|
|
72
|
+
return answer
|
|
73
|
+
except Exception:
|
|
74
|
+
# As requested, silently try the next API key.
|
|
75
|
+
continue
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def request(question):
|
|
80
|
+
"""Ask RevidAPI, print the answer and an in-Shell copy link."""
|
|
81
|
+
try:
|
|
82
|
+
prompt = question if isinstance(question, str) else str(question)
|
|
83
|
+
keys = _read_keys()
|
|
84
|
+
if not keys:
|
|
85
|
+
print("[AI Request] Chưa có API key. Thêm key vào thonnycontrib/api.txt.")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
answer = _ask_api(prompt, keys)
|
|
89
|
+
if not answer:
|
|
90
|
+
print("[AI Request] Không nhận được câu trả lời. Hãy kiểm tra API key, credit và model trong api.txt.")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
encoded = base64.b64encode(answer.encode("utf-8")).decode("ascii")
|
|
94
|
+
# Only emit the private marker. The frontend turns it into the
|
|
95
|
+
# clickable "Click here" link; never print the natural-language answer
|
|
96
|
+
# into Shell. Returning None also avoids leaking the answer if a user
|
|
97
|
+
# calls request() directly in the interactive Python Shell.
|
|
98
|
+
sys.stdout.write(START + encoded + END + "\n")
|
|
99
|
+
sys.stdout.flush()
|
|
100
|
+
return None
|
|
101
|
+
except Exception:
|
|
102
|
+
print("[AI Request] Yêu cầu thất bại. Kiểm tra kết nối và api.txt.")
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_plugin(_backend=None):
|
|
107
|
+
builtins.request = request
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Frontend for Thonny AI Request.
|
|
2
|
+
|
|
3
|
+
Only reacts to Shell text insertion events that contain our private marker.
|
|
4
|
+
It deliberately does not poll the whole Shell in a recurring timer.
|
|
5
|
+
"""
|
|
6
|
+
import base64
|
|
7
|
+
import itertools
|
|
8
|
+
|
|
9
|
+
from thonny import get_workbench
|
|
10
|
+
|
|
11
|
+
START = "__THONNY_AI_REQUEST_BEGIN__"
|
|
12
|
+
END = "__THONNY_AI_REQUEST_END__"
|
|
13
|
+
LINK_COLOR = "#F2F2F2"
|
|
14
|
+
_TAG_PREFIX = "thonny_ai_answer_"
|
|
15
|
+
_tag_counter = itertools.count(1)
|
|
16
|
+
_pending_widgets = set()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _widget_exists(widget):
|
|
20
|
+
try:
|
|
21
|
+
return bool(widget.winfo_exists())
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _copy_to_clipboard(widget, answer):
|
|
27
|
+
try:
|
|
28
|
+
widget.clipboard_clear()
|
|
29
|
+
widget.clipboard_append(answer)
|
|
30
|
+
widget.update_idletasks()
|
|
31
|
+
except Exception:
|
|
32
|
+
# Clipboard problems must not break the Shell.
|
|
33
|
+
pass
|
|
34
|
+
return "break"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _replace_markers(widget):
|
|
38
|
+
"""Replace complete answer markers using Thonny's direct text API.
|
|
39
|
+
|
|
40
|
+
Returns True only when a begin marker exists but its end marker has not
|
|
41
|
+
arrived yet, so the caller should retry briefly. No background polling.
|
|
42
|
+
"""
|
|
43
|
+
if not _widget_exists(widget):
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
while True:
|
|
48
|
+
start_index = widget.search(START, "1.0", "end-1c")
|
|
49
|
+
if not start_index:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
payload_index = widget.index(f"{start_index} + {len(START)} chars")
|
|
53
|
+
end_index = widget.search(END, payload_index, "end-1c")
|
|
54
|
+
if not end_index:
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
encoded = widget.get(payload_index, end_index)
|
|
58
|
+
after_end = widget.index(f"{end_index} + {len(END)} chars")
|
|
59
|
+
try:
|
|
60
|
+
answer = base64.b64decode(encoded.encode("ascii"), validate=True).decode("utf-8")
|
|
61
|
+
except Exception:
|
|
62
|
+
# Remove malformed internal marker rather than leaving noisy data.
|
|
63
|
+
widget.direct_delete(start_index, after_end)
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
tag = _TAG_PREFIX + str(next(_tag_counter))
|
|
67
|
+
widget.tag_configure(tag, foreground=LINK_COLOR, underline=True)
|
|
68
|
+
widget.tag_raise(tag)
|
|
69
|
+
widget.tag_bind(tag, "<Enter>", lambda _event, w=widget: w.configure(cursor="hand2"))
|
|
70
|
+
widget.tag_bind(tag, "<Leave>", lambda _event, w=widget: w.configure(cursor=""))
|
|
71
|
+
widget.tag_bind(
|
|
72
|
+
tag,
|
|
73
|
+
"<Button-1>",
|
|
74
|
+
lambda _event, w=widget, a=answer: _copy_to_clipboard(w, a),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# The ordinary Tk Text delete/insert methods are blocked by
|
|
78
|
+
# Thonny's read-only Shell. direct_delete/direct_insert are the
|
|
79
|
+
# intended escape hatch for programmatic Shell rendering.
|
|
80
|
+
widget.direct_delete(start_index, after_end)
|
|
81
|
+
widget.direct_insert(start_index, "Click here", ("io", "stdout", tag))
|
|
82
|
+
except Exception:
|
|
83
|
+
# Keep the IDE usable even if a future Thonny version changes internals.
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _process_widget(widget, attempt=0):
|
|
88
|
+
retry = _replace_markers(widget)
|
|
89
|
+
if retry and attempt < 20 and _widget_exists(widget):
|
|
90
|
+
try:
|
|
91
|
+
widget.after(50, lambda w=widget, n=attempt + 1: _process_widget(w, n))
|
|
92
|
+
return
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
_pending_widgets.discard(widget)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _on_text_insert(event):
|
|
99
|
+
inserted = getattr(event, "text", "")
|
|
100
|
+
if START not in inserted and END not in inserted:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
widget = getattr(event, "text_widget", None)
|
|
104
|
+
if widget is None or widget in _pending_widgets:
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
_pending_widgets.add(widget)
|
|
108
|
+
try:
|
|
109
|
+
widget.after_idle(lambda w=widget: _process_widget(w))
|
|
110
|
+
except Exception:
|
|
111
|
+
_pending_widgets.discard(widget)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_plugin():
|
|
115
|
+
# Thonny emits TextInsert from its EnhancedText direct insertion method.
|
|
116
|
+
# The handler is filtered before doing any search, so normal Shell output
|
|
117
|
+
# incurs no repeated scans or timer callbacks.
|
|
118
|
+
get_workbench().bind("TextInsert", _on_text_insert, True)
|