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