magic-answer 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.
- magic_answer/__init__.py +13 -0
- magic_answer/_ask.py +84 -0
- magic_answer/api.py +9 -0
- magic_answer/assets/supabase.html +179 -0
- magic_answer/config.py +3 -0
- magic_answer/models.py +5 -0
- magic_answer/supabase_manager.py +8 -0
- magic_answer-0.1.0.dist-info/METADATA +120 -0
- magic_answer-0.1.0.dist-info/RECORD +12 -0
- magic_answer-0.1.0.dist-info/WHEEL +5 -0
- magic_answer-0.1.0.dist-info/licenses/LICENSE +21 -0
- magic_answer-0.1.0.dist-info/top_level.txt +1 -0
magic_answer/__init__.py
ADDED
magic_answer/_ask.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import openai
|
|
2
|
+
from openai import OpenAI
|
|
3
|
+
import requests
|
|
4
|
+
import pyperclip
|
|
5
|
+
|
|
6
|
+
from magic_answer.config import SUPABASE_URL, SUPABASE_KEY, CUSTOM_INSTRUCTION
|
|
7
|
+
from magic_answer.models import models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def v(k):
|
|
11
|
+
url = f"{SUPABASE_URL}/rest/v1/kv"
|
|
12
|
+
|
|
13
|
+
params = {
|
|
14
|
+
"key": f"eq.{k}"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
headers = {
|
|
18
|
+
"apikey": SUPABASE_KEY,
|
|
19
|
+
"Authorization": f"Bearer {SUPABASE_KEY}"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
res = requests.get(url, params=params, headers=headers)
|
|
23
|
+
data = res.json()
|
|
24
|
+
|
|
25
|
+
if len(data) == 0:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
return data[0]["value"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def clip_input(_):
|
|
32
|
+
return pyperclip.paste()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def clip_print(text):
|
|
36
|
+
pyperclip.copy(text)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ask(key, use_clipboard=False):
|
|
40
|
+
read = clip_input if use_clipboard else input
|
|
41
|
+
write = clip_print if use_clipboard else print
|
|
42
|
+
try:
|
|
43
|
+
client = OpenAI(
|
|
44
|
+
api_key=v(key),
|
|
45
|
+
base_url="https://openrouter.ai/api/v1"
|
|
46
|
+
)
|
|
47
|
+
except openai.OpenAIError:
|
|
48
|
+
print(
|
|
49
|
+
"ОШИБКА: API-ключ по вашему ключу не найден. Проверьте правильность введённого ключа при вызове функции `answer`.")
|
|
50
|
+
quit()
|
|
51
|
+
|
|
52
|
+
content = read("Enter request: ")
|
|
53
|
+
content += CUSTOM_INSTRUCTION
|
|
54
|
+
|
|
55
|
+
# noinspection PyTypeChecker
|
|
56
|
+
def response(model):
|
|
57
|
+
try:
|
|
58
|
+
r = client.chat.completions.create(
|
|
59
|
+
model=model,
|
|
60
|
+
messages=[
|
|
61
|
+
{"role": "user", "content": content}
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# check if response is valid
|
|
66
|
+
if not r or not hasattr(r, "choices") or len(r.choices) == 0:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
return r
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f"[ERROR] model {model} failed:", e)
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
result = None
|
|
76
|
+
|
|
77
|
+
for m in models:
|
|
78
|
+
res = response(m)
|
|
79
|
+
if res is not None:
|
|
80
|
+
# noinspection PyTypeChecker
|
|
81
|
+
result = res.choices[0].message.content
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
write(result if result else "All models failed")
|
magic_answer/api.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>KV Store</title>
|
|
6
|
+
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
margin: 0;
|
|
10
|
+
font-family: Arial, sans-serif;
|
|
11
|
+
background: #0f172a;
|
|
12
|
+
color: #e2e8f0;
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
align-items: center;
|
|
16
|
+
height: 100vh;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.card {
|
|
20
|
+
background: #1e293b;
|
|
21
|
+
padding: 25px;
|
|
22
|
+
border-radius: 12px;
|
|
23
|
+
width: 320px;
|
|
24
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
h2 {
|
|
28
|
+
margin-top: 0;
|
|
29
|
+
text-align: center;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
input {
|
|
33
|
+
width: calc(100% - 20px);
|
|
34
|
+
padding: 10px;
|
|
35
|
+
margin: 8px 0;
|
|
36
|
+
border: none;
|
|
37
|
+
border-radius: 6px;
|
|
38
|
+
background: #334155;
|
|
39
|
+
color: white;
|
|
40
|
+
outline: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
button {
|
|
44
|
+
width: 100%;
|
|
45
|
+
padding: 10px;
|
|
46
|
+
margin-top: 10px;
|
|
47
|
+
border: none;
|
|
48
|
+
border-radius: 6px;
|
|
49
|
+
background: #3b82f6;
|
|
50
|
+
color: white;
|
|
51
|
+
font-weight: bold;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
button:hover {
|
|
56
|
+
background: #2563eb;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.msg {
|
|
60
|
+
margin-top: 12px;
|
|
61
|
+
text-align: center;
|
|
62
|
+
font-size: 14px;
|
|
63
|
+
min-height: 20px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ok { color: #22c55e; }
|
|
67
|
+
.err { color: #ef4444; }
|
|
68
|
+
</style>
|
|
69
|
+
</head>
|
|
70
|
+
|
|
71
|
+
<body>
|
|
72
|
+
<div class="card">
|
|
73
|
+
<h2>KV Store</h2>
|
|
74
|
+
|
|
75
|
+
<!-- WRITE -->
|
|
76
|
+
<label for="key"></label><input id="key" placeholder="key (e.g. Lastname Name)">
|
|
77
|
+
<label for="value"></label><input id="value" placeholder="value (e.g. API_KEY)">
|
|
78
|
+
<button id="btnSave">Save</button>
|
|
79
|
+
|
|
80
|
+
<hr style="margin: 15px 0; border-color:#334155">
|
|
81
|
+
|
|
82
|
+
<!-- READ -->
|
|
83
|
+
<label for="lookupKey"></label><input id="lookupKey" placeholder="check key exists...">
|
|
84
|
+
<button id="btnCheck">Check</button>
|
|
85
|
+
|
|
86
|
+
<div id="msg" class="msg"></div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<script>
|
|
90
|
+
const SUPABASE_URL = "https://gxralozqwlifabqykgle.supabase.co";
|
|
91
|
+
const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd4cmFsb3pxd2xpZmFicXlrZ2xlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODI3MzY1OTQsImV4cCI6MjA5ODMxMjU5NH0.cxrJxRoDiRCjCLrFmzIUy9lL4fPO-XZ0JqjeHHoJC8c";
|
|
92
|
+
|
|
93
|
+
const msg = document.getElementById("msg");
|
|
94
|
+
|
|
95
|
+
function setMsg(text, ok=true) {
|
|
96
|
+
msg.textContent = text;
|
|
97
|
+
msg.className = "msg " + (ok ? "ok" : "err");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =========================
|
|
101
|
+
// SAVE (INSERT / UPDATE)
|
|
102
|
+
// =========================
|
|
103
|
+
async function send() {
|
|
104
|
+
const key = document.getElementById("key").value.trim();
|
|
105
|
+
const value = document.getElementById("value").value.trim();
|
|
106
|
+
|
|
107
|
+
if (!key || !value) {
|
|
108
|
+
setMsg("Please fill both fields", false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setMsg("Saving...");
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(`${SUPABASE_URL}/rest/v1/kv?on_conflict=key`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
"apikey": SUPABASE_KEY,
|
|
120
|
+
"Authorization": "Bearer " + SUPABASE_KEY,
|
|
121
|
+
"Prefer": "resolution=merge-duplicates"
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({ key, value })
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (res.ok) {
|
|
127
|
+
setMsg("Saved successfully ✔");
|
|
128
|
+
} else {
|
|
129
|
+
setMsg("Error: " + await res.text(), false);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
} catch (e) {
|
|
133
|
+
setMsg("Network error", false);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// =========================
|
|
138
|
+
// LOOKUP (GET VALUE)
|
|
139
|
+
// =========================
|
|
140
|
+
async function check() {
|
|
141
|
+
const key = document.getElementById("lookupKey").value.trim();
|
|
142
|
+
|
|
143
|
+
if (!key) {
|
|
144
|
+
setMsg("Enter a key to check", false);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setMsg("Checking...");
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch(
|
|
152
|
+
`${SUPABASE_URL}/rest/v1/kv?key=eq.${encodeURIComponent(key)}`,
|
|
153
|
+
{
|
|
154
|
+
method: "GET",
|
|
155
|
+
headers: {
|
|
156
|
+
"apikey": SUPABASE_KEY,
|
|
157
|
+
"Authorization": "Bearer " + SUPABASE_KEY
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const data = await res.json();
|
|
163
|
+
|
|
164
|
+
if (data.length > 0) {
|
|
165
|
+
setMsg(`Found ✔ Value: ${data[0].value}`);
|
|
166
|
+
} else {
|
|
167
|
+
setMsg("Not found ❌", false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
} catch (e) {
|
|
171
|
+
setMsg("Network error", false);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
document.getElementById("btnSave").addEventListener("click", send);
|
|
176
|
+
document.getElementById("btnCheck").addEventListener("click", check);
|
|
177
|
+
</script>
|
|
178
|
+
</body>
|
|
179
|
+
</html>
|
magic_answer/config.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
SUPABASE_URL = "https://gxralozqwlifabqykgle.supabase.co"
|
|
2
|
+
SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd4cmFsb3pxd2xpZmFicXlrZ2xlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODI3MzY1OTQsImV4cCI6MjA5ODMxMjU5NH0.cxrJxRoDiRCjCLrFmzIUy9lL4fPO-XZ0JqjeHHoJC8c"
|
|
3
|
+
CUSTOM_INSTRUCTION = ". Отвечай по делу, не используй MD форматирование. Отвечай только на заданные вопросы или выполняй заданные инструкции, если это практическое задание, не преподноси лишнюю информацию. Формулируй ответ, как будто это ответ на экзаменационный билет"
|
magic_answer/models.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: magic-answer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight OpenRouter client with Supabase-based remote API key storage.
|
|
5
|
+
Author: Melkii_Mel
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: openai
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Requires-Dist: pyperclip
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# Magic Answer
|
|
16
|
+
|
|
17
|
+
A lightweight Python library for querying OpenRouter LLMs using API keys stored in a Supabase key-value database.
|
|
18
|
+
|
|
19
|
+
Instead of embedding an OpenRouter API key into your application, **Magic Answer** retrieves it from a Supabase key-value store at runtime using a lookup key. This allows API keys to be updated or rotated without modifying or redistributing client applications.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
* Query any model available through OpenRouter
|
|
24
|
+
* Automatic fallback between multiple models
|
|
25
|
+
* Retrieve OpenRouter API keys from a Supabase key-value store
|
|
26
|
+
* Exceptionally simple Python API
|
|
27
|
+
* Optional clipboard integration
|
|
28
|
+
* Built-in key manager for maintaining the Supabase database
|
|
29
|
+
* Easily customizable through configuration
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install magic-answer
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
Query using console input:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import magic_answer
|
|
43
|
+
|
|
44
|
+
magic_answer.ask("your_lookup_key")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or use the clipboard:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import magic_answer
|
|
51
|
+
|
|
52
|
+
magic_answer.ask_clipboard("your_lookup_key")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Managing API Keys
|
|
56
|
+
|
|
57
|
+
Magic Answer includes a simple graphical key manager for maintaining your Supabase key-value store.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import magic_answer
|
|
61
|
+
|
|
62
|
+
magic_answer.open_key_manager()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The key manager allows you to:
|
|
66
|
+
|
|
67
|
+
* Add or update API keys
|
|
68
|
+
* Check whether a lookup key exists
|
|
69
|
+
* View the stored value associated with a lookup key
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
Override the default Supabase configuration:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from magic_answer import config
|
|
77
|
+
|
|
78
|
+
config.SUPABASE_URL = "https://your-project.supabase.co"
|
|
79
|
+
config.SUPABASE_KEY = "your_supabase_anon_key"
|
|
80
|
+
config.CUSTOM_INSTRUCTION = "Your custom instruction"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Configure fallback models:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from magic_answer import models
|
|
87
|
+
|
|
88
|
+
models.models = [
|
|
89
|
+
"openai/gpt-oss-20b:free",
|
|
90
|
+
"qwen/qwen3-235b-a22b:free",
|
|
91
|
+
"deepseek/deepseek-r1:free",
|
|
92
|
+
]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The library will attempt each model in order until one successfully returns a response.
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
### `ask(lookup_key)`
|
|
100
|
+
|
|
101
|
+
Prompts the user for a question, retrieves the corresponding OpenRouter API key from Supabase using `lookup_key`, queries OpenRouter, and prints the model's response.
|
|
102
|
+
|
|
103
|
+
### `ask_clipboard(lookup_key)`
|
|
104
|
+
|
|
105
|
+
Reads the prompt from the clipboard, retrieves the corresponding OpenRouter API key from Supabase, queries OpenRouter, and copies the model's response back to the clipboard.
|
|
106
|
+
|
|
107
|
+
### `open_key_manager()`
|
|
108
|
+
|
|
109
|
+
Opens the built-in HTML key manager in the default web browser, allowing you to add, update, and inspect entries in your Supabase key-value store.
|
|
110
|
+
|
|
111
|
+
## Requirements
|
|
112
|
+
|
|
113
|
+
* Python 3.10+
|
|
114
|
+
* Internet connection
|
|
115
|
+
* A Supabase project containing the key-value table
|
|
116
|
+
* An OpenRouter API key stored in that table
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
magic_answer/__init__.py,sha256=qRqPiVMd-CRmtLloSrf1tkieKqLzO1YNmgNdZf6FqYk,251
|
|
2
|
+
magic_answer/_ask.py,sha256=sx4mX7izvKRPE82Vf3jaBDRxcJo9auSO3_CWeAHXX10,2147
|
|
3
|
+
magic_answer/api.py,sha256=6WYhbAQX7Ud4FsTyS3Zs40ZHqtW9DkdlctSDySW8gaQ,123
|
|
4
|
+
magic_answer/config.py,sha256=fGiLAwJUQrKw7Kh7Mu9qpABurUKcyGXtPJoJ-XaBmLs,762
|
|
5
|
+
magic_answer/models.py,sha256=F3tsOzeQl8Hlvl5xJ5XNlSxPc6cCFkGVq7e9dyJfgLQ,129
|
|
6
|
+
magic_answer/supabase_manager.py,sha256=XfrCkVjBV79H31GgkdgoDxhMuduyaEyE-z88UAroRQA,236
|
|
7
|
+
magic_answer/assets/supabase.html,sha256=8vrfGLg-DbRknKvegcgtjlLrJIwS-J_7Krs7EbpUK8w,4511
|
|
8
|
+
magic_answer-0.1.0.dist-info/licenses/LICENSE,sha256=dffn-bw-FwKJ4O4cfMqB32LETA9j96F3l-sRE71IR2w,1091
|
|
9
|
+
magic_answer-0.1.0.dist-info/METADATA,sha256=bCGe-76a-U6qW_CCF8PA-MPapoydX1goTawqcHWXjJ8,3126
|
|
10
|
+
magic_answer-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
magic_answer-0.1.0.dist-info/top_level.txt,sha256=osk9HpwAt_bhbr3KM2Tn3g0Y7QmqF03mAtVj8JL5r7M,13
|
|
12
|
+
magic_answer-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [year] [fullname]
|
|
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 @@
|
|
|
1
|
+
magic_answer
|