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.
@@ -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,8 @@
1
+ """Поддержка запуска как ``python -m ember_agent``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ember_agent.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
@@ -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"