emberio-labs-ember-agent 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.
- emberio_labs_ember_agent-0.1.0/LICENSE +21 -0
- emberio_labs_ember_agent-0.1.0/PKG-INFO +170 -0
- emberio_labs_ember_agent-0.1.0/README.md +148 -0
- emberio_labs_ember_agent-0.1.0/ember_agent/__init__.py +18 -0
- emberio_labs_ember_agent-0.1.0/ember_agent/__main__.py +8 -0
- emberio_labs_ember_agent-0.1.0/ember_agent/cli.py +103 -0
- emberio_labs_ember_agent-0.1.0/ember_agent/config.py +185 -0
- emberio_labs_ember_agent-0.1.0/ember_agent/factory.py +98 -0
- emberio_labs_ember_agent-0.1.0/pyproject.toml +81 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 emberio-labs
|
|
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,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: emberio-labs-ember-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Готовый к запуску агент на базе ember: настройка в TOML, запуск одной командой.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: agent,llm,ai,cli,ember,mcp
|
|
8
|
+
Author: Emberio Labs
|
|
9
|
+
Author-email: dev@emberio.labs
|
|
10
|
+
Requires-Python: >=3.12,<4.0
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Dist: emberio-labs-ember[mcp,openai] (>=0.3.0,<1.0)
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# ember-agent
|
|
23
|
+
|
|
24
|
+
Готовый к запуску агент на базе [`ember`](https://github.com/emberio-labs/ember):
|
|
25
|
+
настройка в TOML — запуск одной командой.
|
|
26
|
+
|
|
27
|
+
- **CLI-команда:** `ember-agent` (короткая, удобная в терминале)
|
|
28
|
+
- **На PyPI пакет публикуется как:** `emberio-labs-ember-agent`
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
$ ember-agent --version
|
|
32
|
+
ember-agent 0.1.0
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Возможности
|
|
36
|
+
|
|
37
|
+
- Конфигурация в одном TOML-файле: системный промпт, провайдер LLM (ключ, модель, base_url), MCP-инструменты.
|
|
38
|
+
- Интерактивный диалог в терминале или разовый запрос (`--message`) для скриптов.
|
|
39
|
+
- Провайдеры: `mock` (без сети и ключей, для экспериментов и тестов) и `openai`
|
|
40
|
+
(OpenAI и любые OpenAI-совместимые API: OpenRouter, Groq, vLLM, LM Studio и т.п.).
|
|
41
|
+
- Модель задаётся провайдеру (`[provider] model`) — как часть «подключения к LLM».
|
|
42
|
+
- Инструменты по [MCP](https://modelcontextprotocol.io): `stdio`-процессы и streamable HTTP-серверы.
|
|
43
|
+
- Работает на Python 3.12+.
|
|
44
|
+
|
|
45
|
+
## Установка
|
|
46
|
+
|
|
47
|
+
Из PyPI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install emberio-labs-ember-agent
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Из исходников (Poetry):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git clone https://github.com/emberio-labs/ember-agent.git
|
|
57
|
+
cd ember-agent
|
|
58
|
+
poetry install
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Настройка
|
|
62
|
+
|
|
63
|
+
Скопируйте пример конфигурации и отредактируйте под себя:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cp config.example.toml config.toml
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Все секции конфигурации опциональны: пустой файл создаст агента на `MockProvider`,
|
|
70
|
+
который работает без сети и API-ключей.
|
|
71
|
+
|
|
72
|
+
```toml
|
|
73
|
+
[agent]
|
|
74
|
+
system_prompt = "Ты полезный и краткий помощник." # как агент себя ведёт
|
|
75
|
+
|
|
76
|
+
[provider]
|
|
77
|
+
type = "mock" # "mock" | "openai"
|
|
78
|
+
model = "gpt-4o-mini" # модель подключения LLM (для type = "openai")
|
|
79
|
+
api_key_env = "OPENAI_API_KEY" # откуда брать ключ (для type = "openai")
|
|
80
|
+
|
|
81
|
+
[[mcp.servers]] # внешние инструменты (опционально)
|
|
82
|
+
transport = "stdio" # "stdio" | "http"
|
|
83
|
+
command = "python"
|
|
84
|
+
args = ["path/to/server.py"]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Полный пример с комментариями — в [`config.example.toml`](config.example.toml).
|
|
88
|
+
Для реального провайдера задайте переменную окружения с ключом, например `OPENAI_API_KEY`.
|
|
89
|
+
Если модель не указана, провайдер берёт свою по умолчанию (`gpt-4o-mini` у OpenAI).
|
|
90
|
+
|
|
91
|
+
## Запуск
|
|
92
|
+
|
|
93
|
+
Интерактивный диалог (выход: `exit`, `quit`, `выход` или Ctrl+D/Ctrl+C):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
ember-agent run
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Разовый запрос — без диалога, удобно для скриптов:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
ember-agent run -m "Привет! Коротко: кто ты?"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Явно указать файл конфигурации:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
ember-agent run -c path/to/config.toml
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Альтернативный запуск через Python (без установки в окружение):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python -m ember_agent run -m "Привет"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Разработка
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
poetry install # установка зависимостей (включая dev)
|
|
121
|
+
poetry run pytest # тесты
|
|
122
|
+
poetry run ruff check ember_agent tests # линтер
|
|
123
|
+
poetry run mypy # статическая типизация
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Код в `ember_agent/`, тесты — в `tests/`. Формат конфигурации описан в
|
|
127
|
+
[`config.example.toml`](config.example.toml).
|
|
128
|
+
|
|
129
|
+
## Релиз
|
|
130
|
+
|
|
131
|
+
Публикация новой версии на PyPI автоматизирована через GitHub Actions
|
|
132
|
+
(workflow `.github/workflows/publish.yml`):
|
|
133
|
+
|
|
134
|
+
1. Поднимите версию в `pyproject.toml` (`version = "0.1.0"`) и закоммитьте
|
|
135
|
+
изменение, например: `chore: bump version to 0.1.0`.
|
|
136
|
+
2. Создайте и запушьте git-тег, совпадающий с версией:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
git tag v0.1.0
|
|
140
|
+
git push origin v0.1.0
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
3. Workflow соберёт wheel и sdist (`poetry build`) и опубликует их на PyPI.
|
|
144
|
+
Ветка `main` при этом не нужна — достаточно тега.
|
|
145
|
+
|
|
146
|
+
Публикация использует Trusted Publishing (OIDC): секреты в GitHub не хранятся.
|
|
147
|
+
Для этого владельцу нужно один раз настроить publisher на PyPI
|
|
148
|
+
(и, опционально, на TestPyPI для проверок):
|
|
149
|
+
|
|
150
|
+
- **PyPI:** https://pypi.org/manage/account/publishing/
|
|
151
|
+
- **TestPyPI:** https://test.pypi.org/manage/account/publishing/
|
|
152
|
+
|
|
153
|
+
Поля формы одинаковы для PyPI и TestPyPI:
|
|
154
|
+
|
|
155
|
+
| Поле | Значение |
|
|
156
|
+
|---|---|
|
|
157
|
+
| Project name | `emberio-labs-ember-agent` |
|
|
158
|
+
| GitHub owner | `emberio-labs` |
|
|
159
|
+
| GitHub repository | `ember-agent` |
|
|
160
|
+
| Workflow name | `publish.yml` |
|
|
161
|
+
| Environment | *(пусто)* |
|
|
162
|
+
|
|
163
|
+
После настройки публикацию можно проверить вручную на TestPyPI:
|
|
164
|
+
GitHub → Actions → Publish → Run workflow. На боевой PyPI пакет уходит
|
|
165
|
+
только по git-тегу `v*`.
|
|
166
|
+
|
|
167
|
+
## Лицензия
|
|
168
|
+
|
|
169
|
+
MIT — см. [LICENSE](LICENSE).
|
|
170
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# ember-agent
|
|
2
|
+
|
|
3
|
+
Готовый к запуску агент на базе [`ember`](https://github.com/emberio-labs/ember):
|
|
4
|
+
настройка в TOML — запуск одной командой.
|
|
5
|
+
|
|
6
|
+
- **CLI-команда:** `ember-agent` (короткая, удобная в терминале)
|
|
7
|
+
- **На PyPI пакет публикуется как:** `emberio-labs-ember-agent`
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
$ ember-agent --version
|
|
11
|
+
ember-agent 0.1.0
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Возможности
|
|
15
|
+
|
|
16
|
+
- Конфигурация в одном TOML-файле: системный промпт, провайдер LLM (ключ, модель, base_url), MCP-инструменты.
|
|
17
|
+
- Интерактивный диалог в терминале или разовый запрос (`--message`) для скриптов.
|
|
18
|
+
- Провайдеры: `mock` (без сети и ключей, для экспериментов и тестов) и `openai`
|
|
19
|
+
(OpenAI и любые OpenAI-совместимые API: OpenRouter, Groq, vLLM, LM Studio и т.п.).
|
|
20
|
+
- Модель задаётся провайдеру (`[provider] model`) — как часть «подключения к LLM».
|
|
21
|
+
- Инструменты по [MCP](https://modelcontextprotocol.io): `stdio`-процессы и streamable HTTP-серверы.
|
|
22
|
+
- Работает на Python 3.12+.
|
|
23
|
+
|
|
24
|
+
## Установка
|
|
25
|
+
|
|
26
|
+
Из PyPI:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install emberio-labs-ember-agent
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Из исходников (Poetry):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/emberio-labs/ember-agent.git
|
|
36
|
+
cd ember-agent
|
|
37
|
+
poetry install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Настройка
|
|
41
|
+
|
|
42
|
+
Скопируйте пример конфигурации и отредактируйте под себя:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cp config.example.toml config.toml
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Все секции конфигурации опциональны: пустой файл создаст агента на `MockProvider`,
|
|
49
|
+
который работает без сети и API-ключей.
|
|
50
|
+
|
|
51
|
+
```toml
|
|
52
|
+
[agent]
|
|
53
|
+
system_prompt = "Ты полезный и краткий помощник." # как агент себя ведёт
|
|
54
|
+
|
|
55
|
+
[provider]
|
|
56
|
+
type = "mock" # "mock" | "openai"
|
|
57
|
+
model = "gpt-4o-mini" # модель подключения LLM (для type = "openai")
|
|
58
|
+
api_key_env = "OPENAI_API_KEY" # откуда брать ключ (для type = "openai")
|
|
59
|
+
|
|
60
|
+
[[mcp.servers]] # внешние инструменты (опционально)
|
|
61
|
+
transport = "stdio" # "stdio" | "http"
|
|
62
|
+
command = "python"
|
|
63
|
+
args = ["path/to/server.py"]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Полный пример с комментариями — в [`config.example.toml`](config.example.toml).
|
|
67
|
+
Для реального провайдера задайте переменную окружения с ключом, например `OPENAI_API_KEY`.
|
|
68
|
+
Если модель не указана, провайдер берёт свою по умолчанию (`gpt-4o-mini` у OpenAI).
|
|
69
|
+
|
|
70
|
+
## Запуск
|
|
71
|
+
|
|
72
|
+
Интерактивный диалог (выход: `exit`, `quit`, `выход` или Ctrl+D/Ctrl+C):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
ember-agent run
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Разовый запрос — без диалога, удобно для скриптов:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
ember-agent run -m "Привет! Коротко: кто ты?"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Явно указать файл конфигурации:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
ember-agent run -c path/to/config.toml
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Альтернативный запуск через Python (без установки в окружение):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python -m ember_agent run -m "Привет"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Разработка
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
poetry install # установка зависимостей (включая dev)
|
|
100
|
+
poetry run pytest # тесты
|
|
101
|
+
poetry run ruff check ember_agent tests # линтер
|
|
102
|
+
poetry run mypy # статическая типизация
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Код в `ember_agent/`, тесты — в `tests/`. Формат конфигурации описан в
|
|
106
|
+
[`config.example.toml`](config.example.toml).
|
|
107
|
+
|
|
108
|
+
## Релиз
|
|
109
|
+
|
|
110
|
+
Публикация новой версии на PyPI автоматизирована через GitHub Actions
|
|
111
|
+
(workflow `.github/workflows/publish.yml`):
|
|
112
|
+
|
|
113
|
+
1. Поднимите версию в `pyproject.toml` (`version = "0.1.0"`) и закоммитьте
|
|
114
|
+
изменение, например: `chore: bump version to 0.1.0`.
|
|
115
|
+
2. Создайте и запушьте git-тег, совпадающий с версией:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
git tag v0.1.0
|
|
119
|
+
git push origin v0.1.0
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
3. Workflow соберёт wheel и sdist (`poetry build`) и опубликует их на PyPI.
|
|
123
|
+
Ветка `main` при этом не нужна — достаточно тега.
|
|
124
|
+
|
|
125
|
+
Публикация использует Trusted Publishing (OIDC): секреты в GitHub не хранятся.
|
|
126
|
+
Для этого владельцу нужно один раз настроить publisher на PyPI
|
|
127
|
+
(и, опционально, на TestPyPI для проверок):
|
|
128
|
+
|
|
129
|
+
- **PyPI:** https://pypi.org/manage/account/publishing/
|
|
130
|
+
- **TestPyPI:** https://test.pypi.org/manage/account/publishing/
|
|
131
|
+
|
|
132
|
+
Поля формы одинаковы для PyPI и TestPyPI:
|
|
133
|
+
|
|
134
|
+
| Поле | Значение |
|
|
135
|
+
|---|---|
|
|
136
|
+
| Project name | `emberio-labs-ember-agent` |
|
|
137
|
+
| GitHub owner | `emberio-labs` |
|
|
138
|
+
| GitHub repository | `ember-agent` |
|
|
139
|
+
| Workflow name | `publish.yml` |
|
|
140
|
+
| Environment | *(пусто)* |
|
|
141
|
+
|
|
142
|
+
После настройки публикацию можно проверить вручную на TestPyPI:
|
|
143
|
+
GitHub → Actions → Publish → Run workflow. На боевой PyPI пакет уходит
|
|
144
|
+
только по git-тегу `v*`.
|
|
145
|
+
|
|
146
|
+
## Лицензия
|
|
147
|
+
|
|
148
|
+
MIT — см. [LICENSE](LICENSE).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""emberio-labs-ember-agent — настраиваемый агент на базе ``ember`` (emberio-labs-ember).
|
|
2
|
+
|
|
3
|
+
Пакет даёт:
|
|
4
|
+
- CLI ``ember-agent run`` (команда короткая) с интерактивным диалогом или разовым запросом;
|
|
5
|
+
- загрузку конфигурации агента из TOML-файла;
|
|
6
|
+
- сборку провайдера и MCP-инструментов в готового агента.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__"]
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
__version__ = version("emberio-labs-ember-agent")
|
|
17
|
+
except PackageNotFoundError: # пакет не установлен (запуск из исходников)
|
|
18
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Командная строка ember-agent.
|
|
2
|
+
|
|
3
|
+
Команда ``run`` читает TOML-конфигурацию и запускает агента:
|
|
4
|
+
в интерактивном диалоге (по умолчанию) или с разовым запросом ``--message``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from ember_agent import __version__
|
|
16
|
+
from ember_agent.config import DEFAULT_CONFIG_FILE, ConfigError, load_config
|
|
17
|
+
from ember_agent.factory import build_agent
|
|
18
|
+
|
|
19
|
+
_QUIT_COMMANDS = frozenset({"exit", "quit", "выход"})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
prog="ember-agent",
|
|
25
|
+
description=(
|
|
26
|
+
"Готовый к запуску агент на базе ember: настройка в TOML, запуск одной командой."
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--version",
|
|
31
|
+
action="version",
|
|
32
|
+
version=f"%(prog)s {__version__}",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
subparsers = parser.add_subparsers(dest="command", required=True, metavar="COMMAND")
|
|
36
|
+
|
|
37
|
+
run = subparsers.add_parser("run", help="запустить агента из конфигурации")
|
|
38
|
+
run.add_argument(
|
|
39
|
+
"-c",
|
|
40
|
+
"--config",
|
|
41
|
+
default=DEFAULT_CONFIG_FILE,
|
|
42
|
+
help=f"путь к TOML-конфигурации (по умолчанию: {DEFAULT_CONFIG_FILE})",
|
|
43
|
+
)
|
|
44
|
+
run.add_argument(
|
|
45
|
+
"-m",
|
|
46
|
+
"--message",
|
|
47
|
+
help="разовый запрос без интерактивного диалога",
|
|
48
|
+
)
|
|
49
|
+
return parser
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _print_error(message: str) -> None:
|
|
53
|
+
print(f"ошибка: {message}", file=sys.stderr)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_repl(agent: Any) -> int:
|
|
57
|
+
"""Интерактивный диалог с агентом."""
|
|
58
|
+
print("Диалог с агентом. Выход: 'exit', 'quit', 'выход' или Ctrl+D/Ctrl+C.")
|
|
59
|
+
while True:
|
|
60
|
+
try:
|
|
61
|
+
line = input("\nвы: ").strip()
|
|
62
|
+
except (EOFError, KeyboardInterrupt):
|
|
63
|
+
print()
|
|
64
|
+
return 0
|
|
65
|
+
if line.lower() in _QUIT_COMMANDS:
|
|
66
|
+
return 0
|
|
67
|
+
if not line:
|
|
68
|
+
continue
|
|
69
|
+
try:
|
|
70
|
+
print(f"агент: {agent.run(line)}")
|
|
71
|
+
except Exception as exc: # показываем ошибку и продолжаем диалог
|
|
72
|
+
_print_error(str(exc))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
76
|
+
try:
|
|
77
|
+
config = load_config(Path(args.config))
|
|
78
|
+
except ConfigError as exc:
|
|
79
|
+
_print_error(str(exc))
|
|
80
|
+
return 2
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
with build_agent(config) as agent:
|
|
84
|
+
if args.message:
|
|
85
|
+
print(agent.run(args.message))
|
|
86
|
+
return 0
|
|
87
|
+
return _run_repl(agent)
|
|
88
|
+
except ConfigError as exc:
|
|
89
|
+
_print_error(str(exc))
|
|
90
|
+
return 2
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
_print_error(str(exc))
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
97
|
+
args = _build_parser().parse_args(argv)
|
|
98
|
+
try:
|
|
99
|
+
if args.command == "run":
|
|
100
|
+
return _cmd_run(args)
|
|
101
|
+
except KeyboardInterrupt:
|
|
102
|
+
return 130
|
|
103
|
+
return 0 # недостижимо: subparser обязателен
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Чтение и валидация TOML-конфигурации агента.
|
|
2
|
+
|
|
3
|
+
Формат конфигурации описан в ``config.example.toml`` в корне проекта.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import tomllib
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
#: Имя файла конфигурации, который CLI ищет по умолчанию.
|
|
14
|
+
DEFAULT_CONFIG_FILE = "config.toml"
|
|
15
|
+
|
|
16
|
+
#: Промпт, используемый, если в конфигурации ничего не задано.
|
|
17
|
+
DEFAULT_SYSTEM_PROMPT = "Ты полезный и краткий помощник."
|
|
18
|
+
|
|
19
|
+
PROVIDER_MOCK = "mock"
|
|
20
|
+
PROVIDER_OPENAI = "openai"
|
|
21
|
+
VALID_PROVIDERS: frozenset[str] = frozenset({PROVIDER_MOCK, PROVIDER_OPENAI})
|
|
22
|
+
|
|
23
|
+
TRANSPORT_STDIO = "stdio"
|
|
24
|
+
TRANSPORT_HTTP = "http"
|
|
25
|
+
VALID_TRANSPORTS: frozenset[str] = frozenset({TRANSPORT_STDIO, TRANSPORT_HTTP})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConfigError(ValueError):
|
|
29
|
+
"""Ошибка конфигурации агента (файл, TOML-синтаксис, значения)."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _require_str(data: dict[str, Any], key: str, default: str) -> str:
|
|
33
|
+
value = data.get(key, default)
|
|
34
|
+
if not isinstance(value, str):
|
|
35
|
+
raise ConfigError(f"Поле '{key}' должно быть строкой, получено {type(value).__name__}")
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _optional_str(data: dict[str, Any], key: str) -> str | None:
|
|
40
|
+
value = data.get(key)
|
|
41
|
+
if value is None:
|
|
42
|
+
return None
|
|
43
|
+
if not isinstance(value, str):
|
|
44
|
+
raise ConfigError(f"Поле '{key}' должно быть строкой, получено {type(value).__name__}")
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _str_list(data: dict[str, Any], key: str) -> list[str]:
|
|
49
|
+
value = data.get(key)
|
|
50
|
+
if value is None:
|
|
51
|
+
return []
|
|
52
|
+
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
53
|
+
raise ConfigError(f"Поле '{key}' должно быть списком строк")
|
|
54
|
+
return list(value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _str_dict(data: dict[str, Any], key: str) -> dict[str, str]:
|
|
58
|
+
value = data.get(key)
|
|
59
|
+
if value is None:
|
|
60
|
+
return {}
|
|
61
|
+
if not isinstance(value, dict) or not all(
|
|
62
|
+
isinstance(k, str) and isinstance(v, str) for k, v in value.items()
|
|
63
|
+
):
|
|
64
|
+
raise ConfigError(f"Поле '{key}' должно быть таблицей строк ({{key = 'value'}})")
|
|
65
|
+
return {str(k): str(v) for k, v in value.items()}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class MCPServerConfig:
|
|
70
|
+
"""Описание одного MCP-сервера (stdio-процесс или HTTP-endpoint)."""
|
|
71
|
+
|
|
72
|
+
transport: str = TRANSPORT_STDIO
|
|
73
|
+
#: Для transport = "stdio"
|
|
74
|
+
command: str | None = None
|
|
75
|
+
args: list[str] = field(default_factory=list)
|
|
76
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
77
|
+
#: Для transport = "http"
|
|
78
|
+
url: str | None = None
|
|
79
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class ProviderConfig:
|
|
84
|
+
"""Настройки LLM-провайдера: как и через какую модель агент ходит к LLM."""
|
|
85
|
+
|
|
86
|
+
type: str = PROVIDER_MOCK
|
|
87
|
+
#: Модель подключения LLM. Если не задана, провайдер берёт свою модель
|
|
88
|
+
#: по умолчанию (например, "gpt-4o-mini" у OpenAI).
|
|
89
|
+
model: str | None = None
|
|
90
|
+
#: Переменная окружения с API-ключом (для type = "openai").
|
|
91
|
+
api_key_env: str = "OPENAI_API_KEY"
|
|
92
|
+
#: Полный base_url OpenAI-совместимого API (опционально).
|
|
93
|
+
base_url: str | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class AgentConfig:
|
|
98
|
+
"""Полная конфигурация агента после чтения TOML-файла."""
|
|
99
|
+
|
|
100
|
+
system_prompt: str = DEFAULT_SYSTEM_PROMPT
|
|
101
|
+
provider: ProviderConfig = field(default_factory=ProviderConfig)
|
|
102
|
+
mcp_servers: list[MCPServerConfig] = field(default_factory=list)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_dict(cls, data: dict[str, Any]) -> AgentConfig:
|
|
106
|
+
agent_section = data.get("agent", {})
|
|
107
|
+
if not isinstance(agent_section, dict):
|
|
108
|
+
raise ConfigError("Секция '[agent]' должна быть таблицей")
|
|
109
|
+
|
|
110
|
+
provider_section = data.get("provider", {})
|
|
111
|
+
if not isinstance(provider_section, dict):
|
|
112
|
+
raise ConfigError("Секция '[provider]' должна быть таблицей")
|
|
113
|
+
|
|
114
|
+
return cls(
|
|
115
|
+
system_prompt=_require_str(agent_section, "system_prompt", DEFAULT_SYSTEM_PROMPT),
|
|
116
|
+
provider=_parse_provider(provider_section),
|
|
117
|
+
mcp_servers=_parse_mcp_servers(data.get("mcp")),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _parse_provider(data: dict[str, Any]) -> ProviderConfig:
|
|
122
|
+
provider_type = _require_str(data, "type", PROVIDER_MOCK)
|
|
123
|
+
if provider_type not in VALID_PROVIDERS:
|
|
124
|
+
valid = ", ".join(sorted(VALID_PROVIDERS))
|
|
125
|
+
message = f"Неизвестный тип провайдера {provider_type!r}; ожидается одно из: {valid}"
|
|
126
|
+
raise ConfigError(message)
|
|
127
|
+
|
|
128
|
+
return ProviderConfig(
|
|
129
|
+
type=provider_type,
|
|
130
|
+
model=_optional_str(data, "model"),
|
|
131
|
+
api_key_env=_require_str(data, "api_key_env", "OPENAI_API_KEY"),
|
|
132
|
+
base_url=_optional_str(data, "base_url"),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _parse_mcp_servers(raw: Any) -> list[MCPServerConfig]:
|
|
137
|
+
if raw is None:
|
|
138
|
+
return []
|
|
139
|
+
if not isinstance(raw, dict):
|
|
140
|
+
raise ConfigError("Секция '[mcp]' должна быть таблицей")
|
|
141
|
+
|
|
142
|
+
servers_raw = raw.get("servers", [])
|
|
143
|
+
if not isinstance(servers_raw, list):
|
|
144
|
+
raise ConfigError("Поле '[mcp].servers' должно быть списком таблиц ([[mcp.servers]])")
|
|
145
|
+
|
|
146
|
+
servers: list[MCPServerConfig] = []
|
|
147
|
+
for index, item in enumerate(servers_raw, start=1):
|
|
148
|
+
if not isinstance(item, dict):
|
|
149
|
+
raise ConfigError(f"MCP-сервер #{index} должен быть таблицей")
|
|
150
|
+
transport = _require_str(item, "transport", TRANSPORT_STDIO)
|
|
151
|
+
if transport not in VALID_TRANSPORTS:
|
|
152
|
+
valid = ", ".join(sorted(VALID_TRANSPORTS))
|
|
153
|
+
raise ConfigError(
|
|
154
|
+
f"MCP-сервер #{index}: неизвестный transport {transport!r}; ожидается: {valid}"
|
|
155
|
+
)
|
|
156
|
+
servers.append(
|
|
157
|
+
MCPServerConfig(
|
|
158
|
+
transport=transport,
|
|
159
|
+
command=_optional_str(item, "command"),
|
|
160
|
+
args=_str_list(item, "args"),
|
|
161
|
+
env=_str_dict(item, "env"),
|
|
162
|
+
url=_optional_str(item, "url"),
|
|
163
|
+
headers=_str_dict(item, "headers"),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
return servers
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def load_config(path: str | Path) -> AgentConfig:
|
|
170
|
+
"""Читает TOML-файл и возвращает валидированную конфигурацию агента.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
ConfigError: файл не найден, некорректный TOML или неверные значения.
|
|
174
|
+
"""
|
|
175
|
+
config_path = Path(path)
|
|
176
|
+
if not config_path.is_file():
|
|
177
|
+
raise ConfigError(f"Файл конфигурации не найден: {config_path}")
|
|
178
|
+
|
|
179
|
+
with config_path.open("rb") as handle:
|
|
180
|
+
try:
|
|
181
|
+
data = tomllib.load(handle)
|
|
182
|
+
except tomllib.TOMLDecodeError as exc:
|
|
183
|
+
raise ConfigError(f"Некорректный TOML в {config_path}: {exc}") from exc
|
|
184
|
+
|
|
185
|
+
return AgentConfig.from_dict(data)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Сборка агента ``ember`` из конфигурации ``ember_agent``.
|
|
2
|
+
|
|
3
|
+
Фабрика скрывает детали библиотеки ``ember``: провайдеров, MCP-клиенты
|
|
4
|
+
и передачу параметров в ``Agent``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from contextlib import ExitStack, contextmanager
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ember import Agent, MCPClient, MockProvider, OpenAIProvider
|
|
15
|
+
|
|
16
|
+
from ember_agent.config import (
|
|
17
|
+
PROVIDER_MOCK,
|
|
18
|
+
PROVIDER_OPENAI,
|
|
19
|
+
TRANSPORT_HTTP,
|
|
20
|
+
TRANSPORT_STDIO,
|
|
21
|
+
AgentConfig,
|
|
22
|
+
ConfigError,
|
|
23
|
+
MCPServerConfig,
|
|
24
|
+
ProviderConfig,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_provider(config: ProviderConfig) -> MockProvider | OpenAIProvider:
|
|
29
|
+
"""Создаёт провайдера ``ember`` по конфигурации.
|
|
30
|
+
|
|
31
|
+
Модель задаётся провайдеру: в ``ember`` именно провайдер подставляет
|
|
32
|
+
свою модель по умолчанию, когда агент её не переопределил.
|
|
33
|
+
"""
|
|
34
|
+
if config.type == PROVIDER_MOCK:
|
|
35
|
+
return MockProvider()
|
|
36
|
+
|
|
37
|
+
if config.type == PROVIDER_OPENAI:
|
|
38
|
+
api_key = os.environ.get(config.api_key_env)
|
|
39
|
+
if not api_key:
|
|
40
|
+
raise ConfigError(
|
|
41
|
+
f"Для провайдера 'openai' задайте переменную окружения "
|
|
42
|
+
f"{config.api_key_env!r} (или укажите другую в [provider].api_key_env)"
|
|
43
|
+
)
|
|
44
|
+
kwargs: dict[str, str] = {}
|
|
45
|
+
if config.model:
|
|
46
|
+
kwargs["model"] = config.model
|
|
47
|
+
if config.base_url:
|
|
48
|
+
kwargs["base_url"] = config.base_url
|
|
49
|
+
return OpenAIProvider(api_key=api_key, **kwargs)
|
|
50
|
+
|
|
51
|
+
raise ConfigError(f"Неизвестный тип провайдера: {config.type!r}") # не должно достигаться
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _open_mcp_client(config: MCPServerConfig) -> MCPClient:
|
|
55
|
+
"""Создаёт (ещё не открытый) MCP-клиент по конфигурации сервера."""
|
|
56
|
+
if config.transport == TRANSPORT_STDIO:
|
|
57
|
+
if not config.command:
|
|
58
|
+
raise ConfigError("MCP-сервер с transport='stdio': укажите 'command'")
|
|
59
|
+
kwargs: dict[str, Any] = {"command": config.command, "args": list(config.args)}
|
|
60
|
+
if config.env:
|
|
61
|
+
kwargs["env"] = dict(config.env)
|
|
62
|
+
return MCPClient.stdio(**kwargs)
|
|
63
|
+
|
|
64
|
+
if config.transport == TRANSPORT_HTTP:
|
|
65
|
+
if not config.url:
|
|
66
|
+
raise ConfigError("MCP-сервер с transport='http': укажите 'url'")
|
|
67
|
+
kwargs = {"url": config.url}
|
|
68
|
+
if config.headers:
|
|
69
|
+
kwargs["headers"] = dict(config.headers)
|
|
70
|
+
return MCPClient.http(**kwargs)
|
|
71
|
+
|
|
72
|
+
raise ConfigError(f"Неизвестный transport MCP: {config.transport!r}") # не должно достигаться
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@contextmanager
|
|
76
|
+
def build_agent(config: AgentConfig) -> Iterator[Agent]:
|
|
77
|
+
"""Создаёт агента ``ember`` из конфигурации.
|
|
78
|
+
|
|
79
|
+
Контекстный менеджер: пока контекст открыт, живут MCP-клиенты
|
|
80
|
+
(stdio-процессы и HTTP-соединения), после выхода — корректно закрываются.
|
|
81
|
+
|
|
82
|
+
Модель агенту не передаётся: она задана провайдеру (``[provider] model``),
|
|
83
|
+
а агент ``ember`` наследует модель провайдера по умолчанию.
|
|
84
|
+
"""
|
|
85
|
+
provider = build_provider(config.provider)
|
|
86
|
+
|
|
87
|
+
with ExitStack() as stack:
|
|
88
|
+
tools: list[Any] = []
|
|
89
|
+
for server in config.mcp_servers:
|
|
90
|
+
client = _open_mcp_client(server)
|
|
91
|
+
stack.enter_context(client)
|
|
92
|
+
tools.extend(client.list_tools())
|
|
93
|
+
|
|
94
|
+
kwargs: dict[str, Any] = {"provider": provider, "system_prompt": config.system_prompt}
|
|
95
|
+
if tools:
|
|
96
|
+
kwargs["tools"] = tools
|
|
97
|
+
|
|
98
|
+
yield Agent(**kwargs)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=2.0.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "emberio-labs-ember-agent"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Готовый к запуску агент на базе ember: настройка в TOML, запуск одной командой."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12,<4.0"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Emberio Labs", email = "dev@emberio.labs" }]
|
|
13
|
+
keywords = ["agent", "llm", "ai", "cli", "ember", "mcp"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"emberio-labs-ember[openai,mcp]>=0.3.0,<1.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
ember-agent = "ember_agent.cli:main"
|
|
30
|
+
|
|
31
|
+
[tool.poetry]
|
|
32
|
+
packages = [{ include = "ember_agent" }]
|
|
33
|
+
|
|
34
|
+
[tool.poetry.group.dev.dependencies]
|
|
35
|
+
pytest = "^8.3.0"
|
|
36
|
+
ruff = "^0.11.0"
|
|
37
|
+
mypy = "^1.15.0"
|
|
38
|
+
|
|
39
|
+
# --- Ruff: линтер и форматтер ---
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
target-version = "py312"
|
|
42
|
+
line-length = 100
|
|
43
|
+
src = ["ember_agent", "tests"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = [
|
|
47
|
+
"E", # pycodestyle: ошибки
|
|
48
|
+
"W", # pycodestyle: предупреждения
|
|
49
|
+
"F", # pyflakes
|
|
50
|
+
"I", # isort (порядок импортов)
|
|
51
|
+
"UP", # pyupgrade (современный синтаксис)
|
|
52
|
+
"B", # flake8-bugbear
|
|
53
|
+
"C4", # flake8-comprehensions
|
|
54
|
+
"PT", # flake8-pytest-style
|
|
55
|
+
"RUF", # специфичные для ruff
|
|
56
|
+
]
|
|
57
|
+
ignore = [
|
|
58
|
+
"RUF001", # проект пишется на русском: кириллица в строках — не ошибка
|
|
59
|
+
"RUF002", # то же для docstring
|
|
60
|
+
"RUF003", # то же для комментариев
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint.per-file-ignores]
|
|
64
|
+
"tests/**/*.py" = ["PT009"] # в тестах используем обычные assert, а не unittest-стиль
|
|
65
|
+
|
|
66
|
+
# --- mypy: статическая типизация ---
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
python_version = "3.12"
|
|
69
|
+
files = ["ember_agent", "tests"]
|
|
70
|
+
ignore_missing_imports = true # ember и dev-зависимости могут быть без стабов
|
|
71
|
+
disallow_untyped_defs = true
|
|
72
|
+
check_untyped_defs = true
|
|
73
|
+
no_implicit_optional = true
|
|
74
|
+
warn_unused_ignores = true
|
|
75
|
+
warn_redundant_casts = true
|
|
76
|
+
warn_return_any = true
|
|
77
|
+
|
|
78
|
+
# --- pytest ---
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
testpaths = ["tests"]
|
|
81
|
+
addopts = "-ra --strict-markers"
|