librecoding 0.6.0__tar.gz → 1.0.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.
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: librecoding
3
- Version: 0.6.0
4
- Summary: Agent library: prompt -> tools -> results in files2/
3
+ Version: 1.0.0
4
+ Summary: Minimal prompt/response library: prompt in console, answer saved to N.txt
5
5
  Author: ar
6
6
  License-Expression: MIT
7
- Keywords: agent,ai,librecoding
7
+ Keywords: ai,librecoding,openrouter
8
8
  Classifier: Operating System :: OS Independent
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Requires-Python: >=3.10
11
- Requires-Dist: openpyxl>=3.1
12
11
  Description-Content-Type: text/markdown
13
12
 
14
13
  # librecoding
@@ -4,18 +4,18 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "librecoding"
7
- version = "0.6.0"
8
- description = "Agent library: prompt -> tools -> results in files2/"
7
+ version = "1.0.0"
8
+ description = "Minimal prompt/response library: prompt in console, answer saved to N.txt"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "MIT"
12
12
  authors = [{ name = "ar" }]
13
- keywords = ["librecoding", "ai", "agent"]
13
+ keywords = ["librecoding", "ai", "openrouter"]
14
14
  classifiers = [
15
15
  "Programming Language :: Python :: 3",
16
16
  "Operating System :: OS Independent",
17
17
  ]
18
- dependencies = ["openpyxl>=3.1"]
18
+ dependencies = []
19
19
 
20
20
  [tool.hatch.build.targets.wheel]
21
21
  packages = ["src/librecoding"]
@@ -0,0 +1,187 @@
1
+ """librecoding 1.0.0 - только запрос и ответ.
2
+
3
+ Промпт -> ответ в консоль и в 1.txt, 2.txt, 3.txt рядом с запущенным скриптом.
4
+ Никаких папок files/, files2/ и никаких других создаваемых файлов.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ import urllib.error
13
+ import urllib.request
14
+ from pathlib import Path
15
+ from types import ModuleType
16
+
17
+ __version__ = "1.0.0"
18
+
19
+ API_URL = "https://openrouter.ai/api/v1/chat/completions"
20
+ DEFAULT_MODEL = "deepseek/deepseek-v4.1-flash"
21
+ KEY_FILE = Path.home() / ".config" / "librecoding" / "api_key"
22
+ PROMPT = "Введи промпт: "
23
+ EXIT_WORDS = {"exit", "quit", "q", "выход", "стоп"}
24
+ SYSTEM_PROMPT = "Ты полезный ассистент. Отвечай по-русски, кратко и по делу."
25
+
26
+ _state = {"key": None, "model": DEFAULT_MODEL}
27
+
28
+
29
+ def set_api_key(value):
30
+ """Задать ключ OpenRouter на время работы процесса."""
31
+ _state["key"] = str(value).strip()
32
+
33
+
34
+ def set_model(value):
35
+ """Сменить модель, например deepseek/deepseek-chat."""
36
+ _state["model"] = str(value)
37
+
38
+
39
+ def get_model():
40
+ return _state["model"]
41
+
42
+
43
+ def _api_key():
44
+ """Ключ: set_api_key() -> OPENROUTER_API_KEY -> ~/.config/librecoding/api_key."""
45
+ if _state["key"]:
46
+ return _state["key"]
47
+ env = os.environ.get("OPENROUTER_API_KEY", "").strip()
48
+ if env:
49
+ return env
50
+ if KEY_FILE.is_file():
51
+ for line in KEY_FILE.read_text(encoding="utf-8").splitlines():
52
+ line = line.strip()
53
+ if line and not line.startswith("#"):
54
+ return line
55
+ raise RuntimeError("нет ключа OpenRouter, положи его в " + str(KEY_FILE))
56
+
57
+
58
+ def caller_dir():
59
+ """Папка запущенного скрипта, а не библиотеки."""
60
+ path = getattr(sys.modules.get("__main__"), "__file__", None)
61
+ if path:
62
+ return Path(path).resolve().parent
63
+ arg = sys.argv[0] if sys.argv else ""
64
+ if arg and arg != "-c" and not arg.startswith("-"):
65
+ p = Path(arg).resolve()
66
+ if p.parent.is_dir():
67
+ return p.parent
68
+ return Path.cwd()
69
+
70
+
71
+ def next_filename(directory=None):
72
+ """Следующее имя 1.txt, 2.txt, 3.txt ..."""
73
+ base = Path(directory) if directory else caller_dir()
74
+ nums = [int(p.stem) for p in base.glob("*.txt") if p.stem.isdigit()]
75
+ return base / (str(max(nums, default=0) + 1) + ".txt")
76
+
77
+
78
+ def ask(prompt, model=None, temperature=0.7, timeout=300):
79
+ """Отправить промпт и вернуть текст. Ничего не печатает и не сохраняет."""
80
+ body = {
81
+ "model": model or _state["model"],
82
+ "messages": [
83
+ {"role": "system", "content": SYSTEM_PROMPT},
84
+ {"role": "user", "content": str(prompt)},
85
+ ],
86
+ "temperature": temperature,
87
+ }
88
+ req = urllib.request.Request(
89
+ API_URL,
90
+ data=json.dumps(body).encode("utf-8"),
91
+ headers={
92
+ "Authorization": "Bearer " + _api_key(),
93
+ "Content-Type": "application/json",
94
+ "HTTP-Referer": "https://pypi.org/project/librecoding/",
95
+ "X-Title": "librecoding",
96
+ },
97
+ method="POST",
98
+ )
99
+ try:
100
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
101
+ data = json.loads(resp.read().decode("utf-8"))
102
+ except urllib.error.HTTPError as exc:
103
+ detail = exc.read().decode("utf-8", "replace")[:300]
104
+ raise RuntimeError("OpenRouter HTTP " + str(exc.code) + ": " + detail) from None
105
+ except urllib.error.URLError as exc:
106
+ raise RuntimeError("нет сети или таймаут: " + str(exc.reason)) from None
107
+ try:
108
+ return data["choices"][0]["message"]["content"] or ""
109
+ except (KeyError, IndexError, TypeError):
110
+ raise RuntimeError("неожиданный ответ: " + str(data)[:300]) from None
111
+
112
+
113
+ def save(text, filename=None):
114
+ """Сохранить текст в N.txt (или в filename) рядом со скриптом."""
115
+ base = caller_dir()
116
+ path = Path(filename) if filename else next_filename(base)
117
+ if not path.is_absolute():
118
+ path = base / path
119
+ path.write_text(str(text).rstrip() + "\n", encoding="utf-8")
120
+ return path
121
+
122
+
123
+ def chat(prompt, model=None, save_answer=True, filename=None):
124
+ """Один промпт: напечатать ответ и сохранить его в N.txt."""
125
+ text = str(prompt).strip()
126
+ if not text:
127
+ return None
128
+ print("думаю...")
129
+ answer = (ask(text, model=model) or "").strip()
130
+ print(answer or "(пустой ответ)")
131
+ if save_answer and answer:
132
+ path = save(answer, filename)
133
+ print("сохранено: " + path.name)
134
+ return path
135
+ return None
136
+
137
+
138
+ def interactive(prompt=None, model=None, save_answer=True, filename=None):
139
+ """Без аргумента цикл промпт-ответ. С аргументом один ответ.
140
+
141
+ Выход: пустой Enter, выход, exit, q, Ctrl+C.
142
+ """
143
+ if prompt is not None:
144
+ return chat(prompt, model=model, save_answer=save_answer, filename=filename)
145
+
146
+ last = None
147
+ while True:
148
+ try:
149
+ line = input(PROMPT)
150
+ except (EOFError, KeyboardInterrupt):
151
+ print()
152
+ break
153
+ line = line.strip()
154
+ if not line or line.lower() in EXIT_WORDS:
155
+ break
156
+ print()
157
+ try:
158
+ last = chat(line, model=model, save_answer=save_answer, filename=filename)
159
+ except Exception as exc:
160
+ print("Ошибка: " + str(exc), file=sys.stderr)
161
+ print()
162
+ return last
163
+
164
+
165
+ class _LibreCodingModule(ModuleType):
166
+ """Модуль, который можно вызвать как функцию: librecoding()."""
167
+
168
+ def __call__(self, obj=None, *args, **kwargs):
169
+ if callable(obj) and not args and not kwargs:
170
+ return obj
171
+ try:
172
+ if isinstance(obj, str):
173
+ kwargs["prompt"] = obj
174
+ return interactive(*args, **kwargs)
175
+ except Exception as exc:
176
+ print("Ошибка: " + str(exc), file=sys.stderr)
177
+ if os.environ.get("LIBRECODING_DEBUG"):
178
+ raise
179
+ return None
180
+
181
+
182
+ sys.modules[__name__].__class__ = _LibreCodingModule
183
+
184
+ __all__ = [
185
+ "ask", "chat", "save", "interactive", "caller_dir", "next_filename",
186
+ "set_api_key", "set_model", "get_model",
187
+ ]
@@ -1,418 +0,0 @@
1
- """librecoding - агент: промпт -> инструменты -> результат в files2/."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
- import subprocess
8
- import sys
9
- import urllib.error
10
- import urllib.request
11
- from pathlib import Path
12
- from types import ModuleType
13
-
14
- from .core import greet
15
-
16
- __version__ = "0.6.0"
17
-
18
- API_URL = "https://openrouter.ai/api/v1/chat/completions"
19
- DEFAULT_MODEL = "deepseek/deepseek-v4.1-flash"
20
- KEY_FILE = Path.home() / ".config" / "librecoding" / "api_key"
21
- IN_DIR = "files"
22
- OUT_DIR = "files2"
23
- PROMPT = "Введи промпт: "
24
- PAUSE_TEXT = "Нажми Enter, чтобы закрыть..."
25
-
26
- MAX_STEPS = 12
27
- TOOL_LIMIT = 12000
28
-
29
- SYSTEM_PROMPT = f"""Ты — автономный агент библиотеки librecoding.
30
- Рабочая папка — та, где лежит запущенный скрипт.
31
-
32
- Структура:
33
- {IN_DIR}/ — входные файлы пользователя, только чтение
34
- {OUT_DIR}/ — сюда складывай всё, что создаёшь
35
-
36
- Инструменты:
37
- list_files() — что есть в {IN_DIR}/
38
- read_file(name) — текст или предпросмотр xlsx/csv
39
- write_file(name, content) — записать текстовый файл в {OUT_DIR}/
40
- run_python(code) — выполнить Python-код, cwd = рабочая папка,
41
- openpyxl уже установлен
42
-
43
- Правила:
44
- 1. Сначала list_files(), если точные имена файлов неизвестны.
45
- 2. Для xlsx/xls/csv НИКОГДА не выдумывай содержимое — только run_python.
46
- 3. Результат сохраняй в {OUT_DIR}/ с тем же именем, что исходник, если не сказано иначе.
47
- 4. Файлы в {IN_DIR}/ не перезаписывай и не удаляй.
48
- 5. Не задавай уточняющих вопросов — делай разумное предположение и выполняй.
49
- 6. Финальный ответ — коротко по-русски: что сделал, какие файлы создал. Без кода."""
50
-
51
- TOOLS = [
52
- {
53
- "type": "function",
54
- "function": {
55
- "name": "list_files",
56
- "description": f"Показать файлы во входной папке {IN_DIR}/.",
57
- "parameters": {"type": "object", "properties": {}, "required": []},
58
- },
59
- },
60
- {
61
- "type": "function",
62
- "function": {
63
- "name": "read_file",
64
- "description": f"Прочитать файл из {IN_DIR}/ (текст или предпросмотр таблицы).",
65
- "parameters": {
66
- "type": "object",
67
- "properties": {"name": {"type": "string", "description": "имя файла в files/"}},
68
- "required": ["name"],
69
- },
70
- },
71
- },
72
- {
73
- "type": "function",
74
- "function": {
75
- "name": "write_file",
76
- "description": f"Записать текстовый файл в {OUT_DIR}/.",
77
- "parameters": {
78
- "type": "object",
79
- "properties": {
80
- "name": {"type": "string"},
81
- "content": {"type": "string"},
82
- },
83
- "required": ["name", "content"],
84
- },
85
- },
86
- },
87
- {
88
- "type": "function",
89
- "function": {
90
- "name": "run_python",
91
- "description": (
92
- "Выполнить Python-код в рабочей папке (cwd). Относительные пути: "
93
- f"'{IN_DIR}/...' и '{OUT_DIR}/...'. openpyxl установлен. "
94
- f"Результаты сохраняй в {OUT_DIR}/. Возвращает stdout/stderr."
95
- ),
96
- "parameters": {
97
- "type": "object",
98
- "properties": {"code": {"type": "string"}},
99
- "required": ["code"],
100
- },
101
- },
102
- },
103
- ]
104
-
105
- _state = {"key": None, "model": DEFAULT_MODEL}
106
-
107
-
108
- # --- настройки ----------------------------------------------------------
109
-
110
- def set_api_key(key: str) -> None:
111
- """Задать ключ OpenRouter на время работы процесса."""
112
- _state["key"] = key.strip()
113
-
114
-
115
- def set_model(model: str) -> None:
116
- """Сменить модель, например 'deepseek/deepseek-chat'."""
117
- _state["model"] = model
118
-
119
-
120
- def get_model() -> str:
121
- return _state["model"]
122
-
123
-
124
- def _api_key() -> str:
125
- """Ключ: set_api_key() -> OPENROUTER_API_KEY -> ~/.config/librecoding/api_key."""
126
- if _state["key"]:
127
- return _state["key"]
128
- env = os.environ.get("OPENROUTER_API_KEY", "").strip()
129
- if env:
130
- return env
131
- if KEY_FILE.is_file():
132
- for line in KEY_FILE.read_text(encoding="utf-8").splitlines():
133
- line = line.strip()
134
- if line and not line.startswith("#"):
135
- return line
136
- raise RuntimeError(
137
- "API-ключ OpenRouter не найден. Задай его так:\n"
138
- f" echo 'sk-or-v1-...' > {KEY_FILE}\n"
139
- f" chmod 600 {KEY_FILE}\n"
140
- " либо: export OPENROUTER_API_KEY=sk-or-v1-...\n"
141
- " либо: librecoding.set_api_key('sk-or-v1-...')"
142
- )
143
-
144
-
145
- # --- пути ---------------------------------------------------------------
146
-
147
- def _caller_dir() -> Path:
148
- """Папка запущенного скрипта (test.py), а не библиотеки."""
149
- path = getattr(sys.modules.get("__main__"), "__file__", None)
150
- if path:
151
- return Path(path).resolve().parent
152
- if sys.argv and sys.argv[0]:
153
- return Path(sys.argv[0]).resolve().parent
154
- return Path.cwd()
155
-
156
-
157
- def ensure_dirs() -> tuple[Path, Path]:
158
- """Создать files/ и files2/ рядом со скриптом. Вернуть (in_dir, out_dir)."""
159
- base = _caller_dir()
160
- in_dir, out_dir = base / IN_DIR, base / OUT_DIR
161
- in_dir.mkdir(parents=True, exist_ok=True)
162
- out_dir.mkdir(parents=True, exist_ok=True)
163
- return in_dir, out_dir
164
-
165
-
166
- def _safe(base: Path, name: str) -> Path:
167
- """Не дать выйти за пределы папки через ../ или абсолютный путь."""
168
- base = base.resolve()
169
- p = (base / name).resolve()
170
- if base != p and base not in p.parents:
171
- raise ValueError(f"путь вне папки: {name}")
172
- return p
173
-
174
-
175
- def next_filename(directory=None) -> Path:
176
- """Следующее имя 1.txt, 2.txt, 3.txt ... (максимум + 1)."""
177
- base = Path(directory) if directory else ensure_dirs()[1]
178
- nums = [int(p.stem) for p in base.glob("*.txt") if p.stem.isdigit()]
179
- return base / f"{max(nums, default=0) + 1}.txt"
180
-
181
-
182
- # --- сеть ---------------------------------------------------------------
183
-
184
- def _api(payload: dict, timeout: int = 300) -> dict:
185
- req = urllib.request.Request(
186
- API_URL,
187
- data=json.dumps(payload).encode("utf-8"),
188
- headers={
189
- "Authorization": f"Bearer {_api_key()}",
190
- "Content-Type": "application/json",
191
- "HTTP-Referer": "https://pypi.org/project/librecoding/",
192
- "X-Title": "librecoding",
193
- },
194
- method="POST",
195
- )
196
- try:
197
- with urllib.request.urlopen(req, timeout=timeout) as resp:
198
- return json.loads(resp.read().decode("utf-8"))
199
- except urllib.error.HTTPError as exc:
200
- body = exc.read().decode("utf-8", "replace")[:400]
201
- raise RuntimeError(f"OpenRouter HTTP {exc.code}: {body}") from None
202
- except urllib.error.URLError as exc:
203
- raise RuntimeError(f"нет сети / таймаут: {exc.reason}") from None
204
-
205
-
206
- def ask(prompt, model=None, temperature=0.7, timeout=300) -> str:
207
- """Один запрос без инструментов. Возвращает текст ответа."""
208
- data = _api(
209
- {
210
- "model": model or _state["model"],
211
- "messages": [{"role": "user", "content": prompt}],
212
- "temperature": temperature,
213
- },
214
- timeout=timeout,
215
- )
216
- try:
217
- return data["choices"][0]["message"]["content"] or ""
218
- except (KeyError, IndexError, TypeError):
219
- raise RuntimeError(f"неожиданный ответ: {str(data)[:400]}") from None
220
-
221
-
222
- # --- инструменты --------------------------------------------------------
223
-
224
- def _tool_list() -> str:
225
- in_dir, _ = ensure_dirs()
226
- files = sorted(p for p in in_dir.iterdir() if p.is_file())
227
- if not files:
228
- return f"{IN_DIR}/ пуста"
229
- return f"файлы в {IN_DIR}/:\n" + "\n".join(
230
- f" {p.name} ({p.stat().st_size} байт)" for p in files
231
- )
232
-
233
-
234
- def _tool_read(name: str) -> str:
235
- in_dir, _ = ensure_dirs()
236
- p = _safe(in_dir, name)
237
- if not p.is_file():
238
- return f"нет файла {name}. Список: {[q.name for q in in_dir.iterdir() if q.is_file()]}"
239
-
240
- suffix = p.suffix.lower()
241
- if suffix in {".xlsx", ".xlsm"}:
242
- try:
243
- from openpyxl import load_workbook
244
- except ImportError:
245
- return "openpyxl недоступен — используй run_python"
246
- wb = load_workbook(p, read_only=True, data_only=True)
247
- parts = []
248
- for ws in wb.worksheets[:3]:
249
- parts.append(f"--- лист '{ws.title}' ---")
250
- for row in ws.iter_rows(min_row=1, max_row=min(ws.max_row or 0, 15), values_only=True):
251
- parts.append(" | ".join("" if c is None else str(c) for c in row))
252
- wb.close()
253
- return "\n".join(parts)[:TOOL_LIMIT]
254
-
255
- if suffix in {".csv", ".tsv", ".txt", ".md", ".json", ".py", ".log", ".xml", ".yml", ".yaml"}:
256
- text = p.read_text(encoding="utf-8", errors="replace")
257
- if len(text) > TOOL_LIMIT:
258
- return text[:TOOL_LIMIT] + "\n...[обрезано]"
259
- return text
260
-
261
- return f"бинарный файл ({suffix or 'без расширения'}) — работай с ним через run_python"
262
-
263
-
264
- def _tool_write(name: str, content: str) -> str:
265
- _, out_dir = ensure_dirs()
266
- p = _safe(out_dir, name)
267
- p.parent.mkdir(parents=True, exist_ok=True)
268
- p.write_text(content, encoding="utf-8")
269
- return f"записано: {OUT_DIR}/{p.relative_to(out_dir)}"
270
-
271
-
272
- def _tool_python(code: str, timeout: int = 180) -> str:
273
- try:
274
- proc = subprocess.run(
275
- [sys.executable, "-c", code],
276
- cwd=_caller_dir(),
277
- capture_output=True,
278
- text=True,
279
- timeout=timeout,
280
- )
281
- except subprocess.TimeoutExpired:
282
- return f"таймаут {timeout}s — код выполнялся слишком долго"
283
- out = (proc.stdout or "")[-TOOL_LIMIT:]
284
- err = (proc.stderr or "")[-4000:]
285
- res = f"exit={proc.returncode}\nstdout:\n{out or '(пусто)'}"
286
- if err.strip():
287
- res += f"\nstderr:\n{err}"
288
- return res
289
-
290
-
291
- def _dispatch(name: str, args: dict) -> str:
292
- try:
293
- if name == "list_files":
294
- return _tool_list()
295
- if name == "read_file":
296
- return _tool_read(str(args.get("name", "")))
297
- if name == "write_file":
298
- return _tool_write(str(args.get("name", "")), str(args.get("content", "")))
299
- if name == "run_python":
300
- return _tool_python(str(args.get("code", "")))
301
- return f"неизвестный инструмент: {name}"
302
- except Exception as exc:
303
- return f"ОШИБКА: {type(exc).__name__}: {exc}"
304
-
305
-
306
- # --- агент --------------------------------------------------------------
307
-
308
- def run_agent(prompt: str, model=None, max_steps=MAX_STEPS, verbose=True) -> str:
309
- """Цикл: модель -> инструменты -> ответ. Возвращает финальный текст."""
310
- mdl = model or _state["model"]
311
- messages = [
312
- {"role": "system", "content": SYSTEM_PROMPT},
313
- {"role": "user", "content": prompt},
314
- ]
315
-
316
- for step in range(1, max_steps + 1):
317
- data = _api({"model": mdl, "messages": messages, "tools": TOOLS})
318
- try:
319
- msg = data["choices"][0]["message"]
320
- except (KeyError, IndexError, TypeError):
321
- raise RuntimeError(f"неожиданный ответ: {str(data)[:400]}") from None
322
-
323
- calls = msg.get("tool_calls") or []
324
- if not calls:
325
- return (msg.get("content") or "").strip()
326
-
327
- assistant = {"role": "assistant", "content": msg.get("content") or ""}
328
- assistant["tool_calls"] = calls
329
- messages.append(assistant)
330
-
331
- for tc in calls:
332
- fn = (tc.get("function") or {}).get("name", "?")
333
- raw = (tc.get("function") or {}).get("arguments") or "{}"
334
- try:
335
- args = json.loads(raw)
336
- except json.JSONDecodeError:
337
- args = {}
338
- if verbose:
339
- hint = args.get("name") or str(args.get("code", ""))[:45].replace("\n", " ")
340
- print(f" [{step}] {fn}({hint})")
341
- result = _dispatch(fn, args)
342
- messages.append(
343
- {
344
- "role": "tool",
345
- "tool_call_id": tc.get("id", ""),
346
- "content": result[:TOOL_LIMIT],
347
- }
348
- )
349
-
350
- return f"агент не уложился в {max_steps} шагов"
351
-
352
-
353
- # --- ввод/вывод ---------------------------------------------------------
354
-
355
- def pause(prompt: str = PAUSE_TEXT) -> None:
356
- """Ждёт Enter. Больше не вызывается автоматом, оставлена для ручного использования."""
357
- if not sys.stdin.isatty():
358
- return
359
- try:
360
- input(prompt)
361
- except (EOFError, KeyboardInterrupt):
362
- print()
363
-
364
-
365
- def interactive(prompt=None, model=None, filename=None, save_answer=True, verbose=True):
366
- """Создать files/ и files2/, принять промпт, выполнить агента, сохранить ответ в files2/N.txt."""
367
- in_dir, out_dir = ensure_dirs()
368
-
369
- if prompt is None:
370
- if not sys.stdin.isatty():
371
- raise RuntimeError("нужен интерактивный терминал или аргумент prompt")
372
- try:
373
- prompt = input(PROMPT).strip()
374
- except (EOFError, KeyboardInterrupt):
375
- print()
376
- return None
377
-
378
- prompt = str(prompt).strip()
379
- if not prompt:
380
- print("Пустой промпт — выход.")
381
- return None
382
-
383
- print(f"думаю... ({model or _state['model']})")
384
- answer = run_agent(prompt, model=model, verbose=verbose)
385
-
386
- target = None
387
- if save_answer and answer:
388
- target = out_dir / (filename or next_filename(out_dir).name)
389
- target.write_text(answer + "\n", encoding="utf-8")
390
-
391
- produced = sorted(p.name for p in out_dir.iterdir() if p.is_file())
392
- print(f"готово. {OUT_DIR}/: {', '.join(produced) if produced else 'пусто'}")
393
- return target
394
-
395
-
396
- # --- магия: модуль, который можно вызвать -------------------------------
397
-
398
- class _LibreCodingModule(ModuleType):
399
- def __call__(self, obj=None, *args, **kwargs):
400
- if callable(obj) and not args and not kwargs:
401
- return obj # @librecoding -> режим декоратора
402
- try:
403
- if isinstance(obj, str): # librecoding("готовый промпт")
404
- kwargs["prompt"] = obj
405
- return interactive(*args, **kwargs)
406
- except Exception as exc:
407
- print(f"Ошибка: {exc}", file=sys.stderr)
408
- if os.environ.get("LIBRECODING_DEBUG"):
409
- raise
410
- return None
411
-
412
-
413
- sys.modules[__name__].__class__ = _LibreCodingModule
414
-
415
- __all__ = [
416
- "greet", "ask", "run_agent", "interactive", "pause", "ensure_dirs", "next_filename",
417
- "set_api_key", "set_model", "get_model",
418
- ]
@@ -1,5 +0,0 @@
1
- def greet(name: str = "мир") -> str:
2
- """Печатает и возвращает приветствие."""
3
- msg = f"Привет, {name}!"
4
- print(msg)
5
- return msg
File without changes