librecoding 0.3.0__tar.gz → 0.5.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.
- {librecoding-0.3.0 → librecoding-0.5.0}/PKG-INFO +1 -1
- {librecoding-0.3.0 → librecoding-0.5.0}/pyproject.toml +1 -1
- librecoding-0.5.0/src/librecoding/__init__.py +187 -0
- librecoding-0.3.0/src/librecoding/__init__.py +0 -43
- {librecoding-0.3.0 → librecoding-0.5.0}/README.md +0 -0
- {librecoding-0.3.0 → librecoding-0.5.0}/src/librecoding/core.py +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""librecoding - вызываемый модуль: спрашивает промпт, шлёт в OpenRouter, сохраняет ответ в N.txt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.request
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import ModuleType
|
|
12
|
+
|
|
13
|
+
from .core import greet
|
|
14
|
+
|
|
15
|
+
__version__ = "0.5.0"
|
|
16
|
+
|
|
17
|
+
API_URL = "https://openrouter.ai/api/v1/chat/completions"
|
|
18
|
+
DEFAULT_MODEL = "deepseek/deepseek-v4.1-flash"
|
|
19
|
+
KEY_FILE = Path.home() / ".config" / "librecoding" / "api_key"
|
|
20
|
+
PROMPT = "Введи промпт: "
|
|
21
|
+
PAUSE_TEXT = "Нажми Enter, чтобы закрыть..."
|
|
22
|
+
|
|
23
|
+
_state: dict = {"key": None, "model": DEFAULT_MODEL}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# --- настройки ----------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def set_api_key(key: str) -> None:
|
|
29
|
+
"""Задать ключ OpenRouter на время работы процесса."""
|
|
30
|
+
_state["key"] = key.strip()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_model(model: str) -> None:
|
|
34
|
+
"""Сменить модель, например 'deepseek/deepseek-chat'."""
|
|
35
|
+
_state["model"] = model
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_model() -> str:
|
|
39
|
+
return _state["model"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _api_key() -> str:
|
|
43
|
+
"""Ключ: set_api_key() -> OPENROUTER_API_KEY -> ~/.config/librecoding/api_key."""
|
|
44
|
+
if _state["key"]:
|
|
45
|
+
return _state["key"]
|
|
46
|
+
|
|
47
|
+
env = os.environ.get("OPENROUTER_API_KEY", "").strip()
|
|
48
|
+
if env:
|
|
49
|
+
return env
|
|
50
|
+
|
|
51
|
+
if KEY_FILE.is_file():
|
|
52
|
+
for line in KEY_FILE.read_text(encoding="utf-8").splitlines():
|
|
53
|
+
line = line.strip()
|
|
54
|
+
if line and not line.startswith("#"):
|
|
55
|
+
return line
|
|
56
|
+
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
"API-ключ OpenRouter не найден. Задай его так:\n"
|
|
59
|
+
f" echo 'sk-or-v1-...' > {KEY_FILE}\n"
|
|
60
|
+
f" chmod 600 {KEY_FILE}\n"
|
|
61
|
+
" либо: export OPENROUTER_API_KEY=sk-or-v1-...\n"
|
|
62
|
+
" либо: librecoding.set_api_key('sk-or-v1-...')"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --- сеть ---------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def ask(prompt, model=None, temperature=0.7, timeout=180) -> str:
|
|
69
|
+
"""Отправить промпт в OpenRouter, вернуть текст ответа."""
|
|
70
|
+
payload = {
|
|
71
|
+
"model": model or _state["model"],
|
|
72
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
73
|
+
"temperature": temperature,
|
|
74
|
+
}
|
|
75
|
+
req = urllib.request.Request(
|
|
76
|
+
API_URL,
|
|
77
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
78
|
+
headers={
|
|
79
|
+
"Authorization": f"Bearer {_api_key()}",
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"HTTP-Referer": "https://pypi.org/project/librecoding/",
|
|
82
|
+
"X-Title": "librecoding",
|
|
83
|
+
},
|
|
84
|
+
method="POST",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
89
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
90
|
+
except urllib.error.HTTPError as exc:
|
|
91
|
+
body = exc.read().decode("utf-8", "replace")[:400]
|
|
92
|
+
raise RuntimeError(f"OpenRouter HTTP {exc.code}: {body}") from None
|
|
93
|
+
except urllib.error.URLError as exc:
|
|
94
|
+
raise RuntimeError(f"нет сети / таймаут: {exc.reason}") from None
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
return data["choices"][0]["message"]["content"] or ""
|
|
98
|
+
except (KeyError, IndexError, TypeError):
|
|
99
|
+
raise RuntimeError(f"неожиданный ответ: {str(data)[:400]}") from None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# --- ввод/вывод ---------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def pause(prompt: str = PAUSE_TEXT) -> None:
|
|
105
|
+
"""Ждёт Enter, чтобы терминал не закрылся. Молчит без tty."""
|
|
106
|
+
if not sys.stdin.isatty():
|
|
107
|
+
return
|
|
108
|
+
try:
|
|
109
|
+
input(prompt)
|
|
110
|
+
except (EOFError, KeyboardInterrupt):
|
|
111
|
+
print()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _caller_dir() -> Path:
|
|
115
|
+
"""Папка запущенного скрипта (test.py), а не библиотеки."""
|
|
116
|
+
path = getattr(sys.modules.get("__main__"), "__file__", None)
|
|
117
|
+
if path:
|
|
118
|
+
return Path(path).resolve().parent
|
|
119
|
+
if sys.argv and sys.argv[0]:
|
|
120
|
+
return Path(sys.argv[0]).resolve().parent
|
|
121
|
+
return Path.cwd()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def next_filename(directory=None) -> Path:
|
|
125
|
+
"""Следующий номер: 1.txt, 2.txt, 3.txt ... (максимальный + 1)."""
|
|
126
|
+
base = Path(directory) if directory else _caller_dir()
|
|
127
|
+
nums = [int(p.stem) for p in base.glob("*.txt") if p.stem.isdigit()]
|
|
128
|
+
return base / f"{max(nums, default=0) + 1}.txt"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def interactive(prompt=None, filename=None, model=None, directory=None):
|
|
132
|
+
"""Спросить промпт -> OpenRouter -> сохранить чистый ответ в N.txt рядом со скриптом."""
|
|
133
|
+
if not sys.stdin.isatty():
|
|
134
|
+
raise RuntimeError("нужен интерактивный терминал: запусти файл в консоли")
|
|
135
|
+
|
|
136
|
+
if prompt is None:
|
|
137
|
+
try:
|
|
138
|
+
prompt = input(PROMPT).strip()
|
|
139
|
+
except (EOFError, KeyboardInterrupt):
|
|
140
|
+
print()
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
if not prompt:
|
|
144
|
+
print("Пустой промпт — выход.")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
print("думаю...")
|
|
148
|
+
answer = ask(prompt, model=model or _state["model"]).strip()
|
|
149
|
+
print(answer)
|
|
150
|
+
|
|
151
|
+
if filename:
|
|
152
|
+
path = Path(filename)
|
|
153
|
+
if not path.is_absolute():
|
|
154
|
+
path = (Path(directory) if directory else _caller_dir()) / path
|
|
155
|
+
else:
|
|
156
|
+
path = next_filename(directory)
|
|
157
|
+
|
|
158
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
path.write_text(answer + "\n", encoding="utf-8")
|
|
160
|
+
print(f"\nСохранено: {path}")
|
|
161
|
+
return path
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --- магия: модуль, который можно вызвать -------------------------------
|
|
165
|
+
|
|
166
|
+
class _LibreCodingModule(ModuleType):
|
|
167
|
+
def __call__(self, obj=None, *args, **kwargs):
|
|
168
|
+
if callable(obj) and not args and not kwargs:
|
|
169
|
+
return obj # @librecoding -> режим декоратора
|
|
170
|
+
try:
|
|
171
|
+
if isinstance(obj, str): # librecoding("готовый промпт")
|
|
172
|
+
kwargs["prompt"] = obj
|
|
173
|
+
interactive(*args, **kwargs)
|
|
174
|
+
except Exception as exc:
|
|
175
|
+
print(f"Ошибка: {exc}", file=sys.stderr)
|
|
176
|
+
if os.environ.get("LIBRECODING_DEBUG"):
|
|
177
|
+
raise
|
|
178
|
+
pause()
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
sys.modules[__name__].__class__ = _LibreCodingModule
|
|
183
|
+
|
|
184
|
+
__all__ = [
|
|
185
|
+
"greet", "ask", "interactive", "pause", "next_filename",
|
|
186
|
+
"set_api_key", "set_model", "get_model",
|
|
187
|
+
]
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"""librecoding - модуль, который можно вызвать как функцию."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
from types import ModuleType
|
|
5
|
-
|
|
6
|
-
from .core import greet
|
|
7
|
-
|
|
8
|
-
__version__ = "0.3.0"
|
|
9
|
-
|
|
10
|
-
BANNER = "Привет, мир! (из librecoding)"
|
|
11
|
-
PROMPT = "Нажми Enter, чтобы закрыть..."
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def hello() -> str:
|
|
15
|
-
"""Явный способ попросить приветствие."""
|
|
16
|
-
print(BANNER)
|
|
17
|
-
return BANNER
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def pause(prompt: str = PROMPT) -> None:
|
|
21
|
-
"""Ждёт Enter, чтобы терминал не закрылся сразу."""
|
|
22
|
-
if not sys.stdin.isatty():
|
|
23
|
-
return
|
|
24
|
-
try:
|
|
25
|
-
input(prompt)
|
|
26
|
-
except (EOFError, KeyboardInterrupt):
|
|
27
|
-
print()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class _LibreCodingModule(ModuleType):
|
|
31
|
-
"""Модуль, который ведёт себя как функция: librecoding()."""
|
|
32
|
-
|
|
33
|
-
def __call__(self, obj=None, *args, **kwargs):
|
|
34
|
-
print(BANNER)
|
|
35
|
-
if callable(obj) and not args and not kwargs:
|
|
36
|
-
return obj
|
|
37
|
-
pause()
|
|
38
|
-
return BANNER
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
sys.modules[__name__].__class__ = _LibreCodingModule
|
|
42
|
-
|
|
43
|
-
__all__ = ["greet", "hello", "pause", "BANNER"]
|
|
File without changes
|
|
File without changes
|