todomat 0.2.1__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.
- todomat-0.2.1/PKG-INFO +115 -0
- todomat-0.2.1/README.md +94 -0
- todomat-0.2.1/pyproject.toml +77 -0
- todomat-0.2.1/setup.cfg +4 -0
- todomat-0.2.1/tests/test_contracts.py +47 -0
- todomat-0.2.1/tests/test_format_incomplete.py +15 -0
- todomat-0.2.1/tests/test_nlp2dsl_errors.py +32 -0
- todomat-0.2.1/tests/test_nlp2uri_engine.py +21 -0
- todomat-0.2.1/tests/test_routing.py +63 -0
- todomat-0.2.1/todomat/__init__.py +15 -0
- todomat-0.2.1/todomat/domains/__init__.py +1 -0
- todomat-0.2.1/todomat/domains/engines/__init__.py +6 -0
- todomat-0.2.1/todomat/domains/engines/curllm.py +131 -0
- todomat-0.2.1/todomat/domains/engines/errors.py +44 -0
- todomat-0.2.1/todomat/domains/engines/iterun.py +149 -0
- todomat-0.2.1/todomat/domains/engines/nlp2dsl.py +116 -0
- todomat-0.2.1/todomat/domains/engines/nlp2uri.py +69 -0
- todomat-0.2.1/todomat/domains/openai/__init__.py +4 -0
- todomat-0.2.1/todomat/domains/openai/format.py +73 -0
- todomat-0.2.1/todomat/domains/openai/models.py +40 -0
- todomat-0.2.1/todomat/domains/orchestrator/__init__.py +3 -0
- todomat-0.2.1/todomat/domains/orchestrator/trigger.py +144 -0
- todomat-0.2.1/todomat/domains/registry/__init__.py +3 -0
- todomat-0.2.1/todomat/domains/registry/client.py +18 -0
- todomat-0.2.1/todomat/domains/routing/__init__.py +23 -0
- todomat-0.2.1/todomat/domains/routing/intent.py +198 -0
- todomat-0.2.1/todomat/domains/shared/__init__.py +16 -0
- todomat-0.2.1/todomat/domains/shared/config.py +69 -0
- todomat-0.2.1/todomat/domains/shared/contracts.py +87 -0
- todomat-0.2.1/todomat.egg-info/PKG-INFO +115 -0
- todomat-0.2.1/todomat.egg-info/SOURCES.txt +39 -0
- todomat-0.2.1/todomat.egg-info/dependency_links.txt +1 -0
- todomat-0.2.1/todomat.egg-info/entry_points.txt +2 -0
- todomat-0.2.1/todomat.egg-info/requires.txt +16 -0
- todomat-0.2.1/todomat.egg-info/top_level.txt +2 -0
- todomat-0.2.1/todomat_mcp/__init__.py +1 -0
- todomat-0.2.1/todomat_mcp/backends.py +214 -0
- todomat-0.2.1/todomat_mcp/gateway.py +34 -0
- todomat-0.2.1/todomat_mcp/routing.py +33 -0
- todomat-0.2.1/todomat_mcp/runner.py +122 -0
- todomat-0.2.1/todomat_mcp/server.py +94 -0
todomat-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: todomat
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Todomat — TODO orchestration with NLP2DSL, ITERUN, and curllm pipelines
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: httpx>=0.26.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
11
|
+
Provides-Extra: mcp
|
|
12
|
+
Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
13
|
+
Provides-Extra: services
|
|
14
|
+
Requires-Dist: fastapi>=0.115; extra == "services"
|
|
15
|
+
Requires-Dist: uvicorn[standard]>=0.32; extra == "services"
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
18
|
+
Requires-Dist: goal>=2.1.0; extra == "dev"
|
|
19
|
+
Requires-Dist: costs>=0.1.20; extra == "dev"
|
|
20
|
+
Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
21
|
+
|
|
22
|
+
# Todomat
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## AI Cost Tracking
|
|
26
|
+
|
|
27
|
+
   
|
|
28
|
+
  
|
|
29
|
+
|
|
30
|
+
- 🤖 **LLM usage:** $0.1500 (1 commits)
|
|
31
|
+
- 👤 **Human dev:** ~$100 (1.0h @ $100/h, 30min dedup)
|
|
32
|
+
|
|
33
|
+
Generated on 2026-06-07 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
Stack testowy strony www dla klientów wdrażających automatyzację przez prompty.
|
|
38
|
+
|
|
39
|
+
Łączy **Open WebUI** z **curllm** (dane online), **NLP2DSL** (procesy biznesowe) i **ITERUN** (usługi/API/stacki) przez OpenAI-compatible router.
|
|
40
|
+
|
|
41
|
+
## Szybki start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cp .env.example .env
|
|
45
|
+
./scripts/bootstrap.sh
|
|
46
|
+
./scripts/test-local.sh
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Endpointy
|
|
50
|
+
|
|
51
|
+
| Usługa | URL |
|
|
52
|
+
|--------|-----|
|
|
53
|
+
| Open WebUI | http://localhost:3000 |
|
|
54
|
+
| Pipeline Router | http://localhost:9099/v1 |
|
|
55
|
+
| Trigger Gateway | http://localhost:8084/trigger |
|
|
56
|
+
| curllm API (opcjonalnie) | http://localhost:8810 |
|
|
57
|
+
|
|
58
|
+
## curllm — dane z internetu
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
./scripts/start-curllm.sh # opcjonalnie, pełny browser+LLM
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Domyślnie adapter używa lekkiego HTTP fetch (`CURLLM_LIGHT_FETCH=1`). Pełny curllm: `CURLLM_LIGHT_FETCH=0` + uruchomiony API na :8810.
|
|
65
|
+
|
|
66
|
+
## Open WebUI — połączenie
|
|
67
|
+
|
|
68
|
+
Admin → Settings → Connections:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
Base URL: http://pipeline-router:9099/v1 # gdy Open WebUI w sieci todomat
|
|
72
|
+
API Key: dev-key
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Jeśli Open WebUI działa poza compose: `http://host.docker.internal:9099/v1`.
|
|
76
|
+
Po `./scripts/bootstrap.sh` kontener `openwebui` jest automatycznie podłączany do sieci `todomat_todomat-net`.
|
|
77
|
+
|
|
78
|
+
## Przykładowe prompty
|
|
79
|
+
|
|
80
|
+
Zob. `examples/prompts/` — w tym [orchestration.md](examples/prompts/orchestration.md) (usługi, getv, desktop, workflow).
|
|
81
|
+
|
|
82
|
+
## MCP (Cursor / agenty)
|
|
83
|
+
|
|
84
|
+
Jeden router MCP routuje do curllm, nlp2dsl i iterun:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install -e .
|
|
88
|
+
./scripts/start-mcp.sh
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Dokumentacja: [docs/MCP.md](docs/MCP.md) · konfiguracja Cursor: `examples/mcp-cursor.json`
|
|
92
|
+
|
|
93
|
+
## Orchestracja (URI, MCP, wiele usług)
|
|
94
|
+
|
|
95
|
+
Przewodnik uruchomienia i kontroli usług, artefaktów, zmiennych getv i zadań:
|
|
96
|
+
|
|
97
|
+
- **[docs/ORCHESTRATION.md](docs/ORCHESTRATION.md)** — trigger-gateway, MCP, scenariusze
|
|
98
|
+
- [docs/MCP.md](docs/MCP.md) — router `todomat-mcp`
|
|
99
|
+
- [docs/INTEGRATION.md](docs/INTEGRATION.md) — Open WebUI
|
|
100
|
+
- [nlp2uri orchestration](https://github.com/semcod/nlp2uri/blob/main/docs/orchestration.md) — schematy URI
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
./scripts/sync-env-from-getv.sh llm/groq # zmienne → .env
|
|
104
|
+
./scripts/install-mcp-stack.sh # MCP + nlp2uri + getv
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Dokumentacja
|
|
108
|
+
|
|
109
|
+
- [docs/ORCHESTRATION.md](docs/ORCHESTRATION.md)
|
|
110
|
+
- [docs/INTEGRATION.md](docs/INTEGRATION.md)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
Licensed under Apache-2.0.
|
todomat-0.2.1/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Todomat
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## AI Cost Tracking
|
|
5
|
+
|
|
6
|
+
   
|
|
7
|
+
  
|
|
8
|
+
|
|
9
|
+
- 🤖 **LLM usage:** $0.1500 (1 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$100 (1.0h @ $100/h, 30min dedup)
|
|
11
|
+
|
|
12
|
+
Generated on 2026-06-07 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
Stack testowy strony www dla klientów wdrażających automatyzację przez prompty.
|
|
17
|
+
|
|
18
|
+
Łączy **Open WebUI** z **curllm** (dane online), **NLP2DSL** (procesy biznesowe) i **ITERUN** (usługi/API/stacki) przez OpenAI-compatible router.
|
|
19
|
+
|
|
20
|
+
## Szybki start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cp .env.example .env
|
|
24
|
+
./scripts/bootstrap.sh
|
|
25
|
+
./scripts/test-local.sh
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Endpointy
|
|
29
|
+
|
|
30
|
+
| Usługa | URL |
|
|
31
|
+
|--------|-----|
|
|
32
|
+
| Open WebUI | http://localhost:3000 |
|
|
33
|
+
| Pipeline Router | http://localhost:9099/v1 |
|
|
34
|
+
| Trigger Gateway | http://localhost:8084/trigger |
|
|
35
|
+
| curllm API (opcjonalnie) | http://localhost:8810 |
|
|
36
|
+
|
|
37
|
+
## curllm — dane z internetu
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
./scripts/start-curllm.sh # opcjonalnie, pełny browser+LLM
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Domyślnie adapter używa lekkiego HTTP fetch (`CURLLM_LIGHT_FETCH=1`). Pełny curllm: `CURLLM_LIGHT_FETCH=0` + uruchomiony API na :8810.
|
|
44
|
+
|
|
45
|
+
## Open WebUI — połączenie
|
|
46
|
+
|
|
47
|
+
Admin → Settings → Connections:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Base URL: http://pipeline-router:9099/v1 # gdy Open WebUI w sieci todomat
|
|
51
|
+
API Key: dev-key
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Jeśli Open WebUI działa poza compose: `http://host.docker.internal:9099/v1`.
|
|
55
|
+
Po `./scripts/bootstrap.sh` kontener `openwebui` jest automatycznie podłączany do sieci `todomat_todomat-net`.
|
|
56
|
+
|
|
57
|
+
## Przykładowe prompty
|
|
58
|
+
|
|
59
|
+
Zob. `examples/prompts/` — w tym [orchestration.md](examples/prompts/orchestration.md) (usługi, getv, desktop, workflow).
|
|
60
|
+
|
|
61
|
+
## MCP (Cursor / agenty)
|
|
62
|
+
|
|
63
|
+
Jeden router MCP routuje do curllm, nlp2dsl i iterun:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install -e .
|
|
67
|
+
./scripts/start-mcp.sh
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Dokumentacja: [docs/MCP.md](docs/MCP.md) · konfiguracja Cursor: `examples/mcp-cursor.json`
|
|
71
|
+
|
|
72
|
+
## Orchestracja (URI, MCP, wiele usług)
|
|
73
|
+
|
|
74
|
+
Przewodnik uruchomienia i kontroli usług, artefaktów, zmiennych getv i zadań:
|
|
75
|
+
|
|
76
|
+
- **[docs/ORCHESTRATION.md](docs/ORCHESTRATION.md)** — trigger-gateway, MCP, scenariusze
|
|
77
|
+
- [docs/MCP.md](docs/MCP.md) — router `todomat-mcp`
|
|
78
|
+
- [docs/INTEGRATION.md](docs/INTEGRATION.md) — Open WebUI
|
|
79
|
+
- [nlp2uri orchestration](https://github.com/semcod/nlp2uri/blob/main/docs/orchestration.md) — schematy URI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
./scripts/sync-env-from-getv.sh llm/groq # zmienne → .env
|
|
83
|
+
./scripts/install-mcp-stack.sh # MCP + nlp2uri + getv
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Dokumentacja
|
|
87
|
+
|
|
88
|
+
- [docs/ORCHESTRATION.md](docs/ORCHESTRATION.md)
|
|
89
|
+
- [docs/INTEGRATION.md](docs/INTEGRATION.md)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
Licensed under Apache-2.0.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "todomat"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "Todomat — TODO orchestration with NLP2DSL, ITERUN, and curllm pipelines"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.26.0",
|
|
14
|
+
"pydantic>=2.0",
|
|
15
|
+
"beautifulsoup4>=4.12",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
mcp = [
|
|
20
|
+
"mcp>=1.0.0",
|
|
21
|
+
]
|
|
22
|
+
services = [
|
|
23
|
+
"fastapi>=0.115",
|
|
24
|
+
"uvicorn[standard]>=0.32",
|
|
25
|
+
]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=8.0",
|
|
28
|
+
"goal>=2.1.0",
|
|
29
|
+
"costs>=0.1.20",
|
|
30
|
+
"pfix>=0.1.60",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=8.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
todomat-mcp = "todomat_mcp.server:main"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
include = ["todomat*", "todomat_mcp*"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
python_files = ["test_*.py"]
|
|
47
|
+
pythonpath = ["."]
|
|
48
|
+
addopts = "-v --tb=short"
|
|
49
|
+
|
|
50
|
+
[tool.pfix]
|
|
51
|
+
# Self-healing Python configuration
|
|
52
|
+
model = "openrouter/qwen/qwen3-coder-next"
|
|
53
|
+
auto_apply = true
|
|
54
|
+
auto_install_deps = true
|
|
55
|
+
auto_restart = false
|
|
56
|
+
max_retries = 3
|
|
57
|
+
create_backups = false
|
|
58
|
+
git_auto_commit = false
|
|
59
|
+
|
|
60
|
+
[tool.pfix.runtime_todo]
|
|
61
|
+
enabled = true
|
|
62
|
+
todo_file = "TODO.md"
|
|
63
|
+
min_severity = "low"
|
|
64
|
+
deduplicate = true
|
|
65
|
+
|
|
66
|
+
[tool.costs]
|
|
67
|
+
# AI Cost tracking configuration
|
|
68
|
+
badge = true
|
|
69
|
+
update_readme = true
|
|
70
|
+
readme_path = "README.md"
|
|
71
|
+
default_model = "openrouter/qwen/qwen3-coder-next"
|
|
72
|
+
analysis_mode = "byok"
|
|
73
|
+
full_history = true
|
|
74
|
+
max_commits = 500
|
|
75
|
+
|
|
76
|
+
# Cost thresholds for badge colors (USD)
|
|
77
|
+
badge_color_thresholds = { low = 1.0, medium = 5.0, high = 10.0, critical = 50.0 }
|
todomat-0.2.1/setup.cfg
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Domain contract tests."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from todomat.domains.engines.nlp2dsl import Nlp2dslEngine, mock_workflow
|
|
6
|
+
from todomat.domains.routing.intent import resolve_plan
|
|
7
|
+
from todomat.domains.shared.config import _mock_for_engine
|
|
8
|
+
from todomat.domains.shared.contracts import TodoIntentRequest, TodoResult
|
|
9
|
+
from todomat.domains.shared.config import EngineConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_resolve_plan_with_model():
|
|
13
|
+
plan = resolve_plan(TodoIntentRequest(text="pobierz https://x.com", model="curllm-web-research"))
|
|
14
|
+
assert plan.pipeline == "curllm"
|
|
15
|
+
assert plan.execute is True
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_todo_result_roundtrip():
|
|
19
|
+
payload = {"status": "complete", "summary": "ok", "dsl": {"steps": []}, "mock": True}
|
|
20
|
+
result = TodoResult.from_engine_payload(payload, pipeline="nlp2dsl")
|
|
21
|
+
data = result.to_dict()
|
|
22
|
+
assert data["pipeline"] == "nlp2dsl"
|
|
23
|
+
assert data["dsl"] == {"steps": []}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_nlp2dsl_mock_engine():
|
|
27
|
+
import asyncio
|
|
28
|
+
|
|
29
|
+
engine = Nlp2dslEngine(EngineConfig(mock=True, upstream_url="http://localhost:8010"))
|
|
30
|
+
out = asyncio.run(engine.process("wyślij fakturę 200 PLN", execute=False))
|
|
31
|
+
assert out["mock"] is True
|
|
32
|
+
assert "send_invoice" in str(out["dsl"])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_mock_workflow_invoice_amount():
|
|
36
|
+
out = mock_workflow("faktura 999 PLN", execute=True)
|
|
37
|
+
step = out["dsl"]["steps"][0]
|
|
38
|
+
assert step["config"]["amount"] == 999.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_per_engine_mock_env(monkeypatch):
|
|
42
|
+
monkeypatch.delenv("MOCK_MODE", raising=False)
|
|
43
|
+
monkeypatch.setenv("NLP2DSL_MOCK_MODE", "0")
|
|
44
|
+
assert _mock_for_engine("nlp2dsl") is False
|
|
45
|
+
monkeypatch.delenv("NLP2DSL_MOCK_MODE", raising=False)
|
|
46
|
+
monkeypatch.setenv("MOCK_MODE", "1")
|
|
47
|
+
assert _mock_for_engine("nlp2dsl") is True
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from todomat.domains.openai.format import format_chat_response
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_incomplete_shows_prompt_user():
|
|
5
|
+
result = {
|
|
6
|
+
"status": "incomplete",
|
|
7
|
+
"pipeline": "nlp2dsl",
|
|
8
|
+
"summary": "Status: incomplete",
|
|
9
|
+
"prompt_user": "Podaj: adres e-mail odbiorcy, treść wiadomości (body)",
|
|
10
|
+
"missing_fields": ["send_email.to", "send_email.body"],
|
|
11
|
+
}
|
|
12
|
+
text = format_chat_response("orchestrator-auto", result)
|
|
13
|
+
assert "Podaj:" in text
|
|
14
|
+
assert "send_email.to" in text
|
|
15
|
+
assert "incomplete" in text.lower() or "uzupełnienia" in text.lower()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from todomat.domains.engines.errors import normalize_nlp2dsl_prompt, parse_upstream_error
|
|
2
|
+
from todomat.domains.openai.format import format_chat_response
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class _FakeResponse:
|
|
6
|
+
def __init__(self, payload: dict, status_code: int = 422) -> None:
|
|
7
|
+
self._payload = payload
|
|
8
|
+
self.status_code = status_code
|
|
9
|
+
|
|
10
|
+
def json(self) -> dict:
|
|
11
|
+
return self._payload
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_normalize_z_trescja():
|
|
15
|
+
text = "Wyślij email do jan@firma.pl z treścią: Dzień dobry"
|
|
16
|
+
assert "treść:" in normalize_nlp2dsl_prompt(text).lower()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parse_nested_upstream_error():
|
|
20
|
+
resp = _FakeResponse({"detail": {"detail": {"error": "Nie rozpoznano intencji", "hint": "Spróbuj fakturę"}}})
|
|
21
|
+
out = parse_upstream_error(resp)
|
|
22
|
+
assert out["status"] == "error"
|
|
23
|
+
assert out["hint"] == "Spróbuj fakturę"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_format_error_response():
|
|
27
|
+
text = format_chat_response(
|
|
28
|
+
"orchestrator-auto",
|
|
29
|
+
{"status": "error", "pipeline": "nlp2dsl", "error": "Nie rozpoznano intencji", "hint": "Spróbuj fakturę"},
|
|
30
|
+
)
|
|
31
|
+
assert "Wskazówka" in text
|
|
32
|
+
assert "Spróbuj fakturę" in text
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""nlp2uri engine mock tests."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from todomat.domains.engines.nlp2uri import Nlp2uriEngine
|
|
6
|
+
from todomat.domains.shared.config import EngineConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_nlp2uri_engine_mock_desktop():
|
|
10
|
+
engine = Nlp2uriEngine(EngineConfig(mock=True, upstream_url=""))
|
|
11
|
+
result = asyncio.run(engine.process("otwórz firefox", execute=False))
|
|
12
|
+
assert result["status"] == "ok"
|
|
13
|
+
assert result["mock"] is True
|
|
14
|
+
assert "firefox" in result["plan"]["uri"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_nlp2uri_engine_mock_getv():
|
|
18
|
+
engine = Nlp2uriEngine(EngineConfig(mock=True, upstream_url=""))
|
|
19
|
+
result = asyncio.run(engine.process("pokaż GROQ_API_KEY z getv", execute=False))
|
|
20
|
+
assert result["source"] == "getv"
|
|
21
|
+
assert result["uri"].startswith("getv://")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Routing logic tests — no MCP subprocess."""
|
|
2
|
+
|
|
3
|
+
from todomat_mcp.routing import (
|
|
4
|
+
detect_pipeline,
|
|
5
|
+
downstream_target,
|
|
6
|
+
parse_execute_flag,
|
|
7
|
+
route_decision,
|
|
8
|
+
route_model,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_detect_curllm_online():
|
|
13
|
+
assert detect_pipeline("pobierz dane z https://example.com") == "online-auto"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_detect_iterun_service():
|
|
17
|
+
assert detect_pipeline("utwórz mikroserwis fastapi z redis") == "iterun"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_detect_iterun_web_contact_app():
|
|
21
|
+
assert detect_pipeline("stworz aplikacje web kontaktu dla firmy") == "iterun"
|
|
22
|
+
assert route_model("orchestrator-auto", "stworz aplikacje web kontaktu dla firmy") == "iterun"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_detect_nlp2dsl_default():
|
|
26
|
+
assert detect_pipeline("wyślij fakturę do klienta po opłaceniu zamówienia") == "nlp2dsl"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_downstream_after_web():
|
|
30
|
+
assert downstream_target("pobierz dane i wygeneruj stack docker") == "iterun"
|
|
31
|
+
assert downstream_target("pobierz dane i zautomatyzuj proces faktury") == "nlp2dsl"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_route_model_mapping():
|
|
35
|
+
assert route_model("curllm-web-research", "x") == "curllm"
|
|
36
|
+
assert route_model("orchestrator-online", "x") == "online-auto"
|
|
37
|
+
assert route_model("iterun-service-builder", "x") == "iterun"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_parse_execute_flag_default_execute():
|
|
41
|
+
text, execute = parse_execute_flag("wyślij fakturę do klienta")
|
|
42
|
+
assert execute is True
|
|
43
|
+
assert text == "wyślij fakturę do klienta"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_parse_execute_flag_dry_run():
|
|
47
|
+
text, execute = parse_execute_flag("wyślij fakturę --dry-run")
|
|
48
|
+
assert execute is False
|
|
49
|
+
assert "--dry-run" not in text
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_detect_nlp2uri_desktop():
|
|
53
|
+
assert detect_pipeline("otwórz firefox") == "nlp2uri"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_detect_nlp2uri_getv():
|
|
57
|
+
assert detect_pipeline("pokaż GROQ_API_KEY z getv") == "nlp2uri"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_route_decision_auto():
|
|
61
|
+
d = route_decision("zbuduj api gateway", pipeline="auto")
|
|
62
|
+
assert d["pipeline"] == "iterun"
|
|
63
|
+
assert d["service_hints"] is True
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Todomat core — shared contracts, routing, engines, and orchestration."""
|
|
2
|
+
|
|
3
|
+
from todomat.domains.shared.contracts import (
|
|
4
|
+
ProcessRequest,
|
|
5
|
+
TodoIntentRequest,
|
|
6
|
+
TodoPlan,
|
|
7
|
+
TodoResult,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ProcessRequest",
|
|
12
|
+
"TodoIntentRequest",
|
|
13
|
+
"TodoPlan",
|
|
14
|
+
"TodoResult",
|
|
15
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Domain layer — contracts, routing, engines, orchestration."""
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from todomat.domains.engines.curllm import CurllmEngine
|
|
2
|
+
from todomat.domains.engines.iterun import IterunEngine
|
|
3
|
+
from todomat.domains.engines.nlp2dsl import Nlp2dslEngine
|
|
4
|
+
from todomat.domains.engines.nlp2uri import Nlp2uriEngine
|
|
5
|
+
|
|
6
|
+
__all__ = ["CurllmEngine", "IterunEngine", "Nlp2dslEngine", "Nlp2uriEngine"]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""curllm web-research engine — light HTTP, upstream API, or mock."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from bs4 import BeautifulSoup
|
|
11
|
+
|
|
12
|
+
from todomat.domains.shared.config import CurllmEngineConfig
|
|
13
|
+
|
|
14
|
+
URL_RE = re.compile(r"https?://[^\s<>\"']+", re.I)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_url(text: str, explicit: str | None = None) -> str | None:
|
|
18
|
+
if explicit:
|
|
19
|
+
return explicit.strip()
|
|
20
|
+
match = URL_RE.search(text)
|
|
21
|
+
return match.group(0).rstrip(".,);]") if match else None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def instruction_from_text(text: str, url: str) -> str:
|
|
25
|
+
cleaned = text.replace(url, "").strip()
|
|
26
|
+
if cleaned:
|
|
27
|
+
return cleaned
|
|
28
|
+
return "extract page title, main text summary and all links"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def light_fetch(url: str, instruction: str) -> dict[str, Any]:
|
|
32
|
+
with httpx.Client(timeout=30.0, follow_redirects=True) as client:
|
|
33
|
+
resp = client.get(url, headers={"User-Agent": "todomat-curllm-adapter/0.1"})
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
html = resp.text
|
|
36
|
+
|
|
37
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
38
|
+
title = (soup.title.string or "").strip() if soup.title else ""
|
|
39
|
+
paragraphs = [p.get_text(" ", strip=True) for p in soup.find_all("p")[:8]]
|
|
40
|
+
text_summary = " ".join(p for p in paragraphs if p)[:2000]
|
|
41
|
+
links = []
|
|
42
|
+
for a in soup.find_all("a", href=True)[:30]:
|
|
43
|
+
href = a["href"]
|
|
44
|
+
if href.startswith("http"):
|
|
45
|
+
links.append({"text": a.get_text(strip=True)[:120], "href": href})
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"mode": "light_http",
|
|
49
|
+
"url": url,
|
|
50
|
+
"instruction": instruction,
|
|
51
|
+
"title": title,
|
|
52
|
+
"summary": text_summary or title,
|
|
53
|
+
"links": links,
|
|
54
|
+
"status_code": resp.status_code,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def curllm_fetch(url: str, instruction: str, upstream: str) -> dict[str, Any]:
|
|
59
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
60
|
+
resp = await client.post(
|
|
61
|
+
f"{upstream.rstrip('/')}/api/execute",
|
|
62
|
+
json={
|
|
63
|
+
"url": url,
|
|
64
|
+
"data": instruction,
|
|
65
|
+
"visual_mode": False,
|
|
66
|
+
"stealth_mode": False,
|
|
67
|
+
"headers": {"Accept-Language": "pl-PL,pl;q=0.9"},
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
resp.raise_for_status()
|
|
71
|
+
data = resp.json()
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"mode": "curllm",
|
|
75
|
+
"url": url,
|
|
76
|
+
"instruction": instruction,
|
|
77
|
+
"success": data.get("success", True),
|
|
78
|
+
"result": data.get("result") or data,
|
|
79
|
+
"steps_taken": data.get("steps_taken"),
|
|
80
|
+
"run_log": data.get("run_log"),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def context_block(web: dict[str, Any]) -> str:
|
|
85
|
+
payload = {k: v for k, v in web.items() if k not in {"raw"}}
|
|
86
|
+
return json.dumps(payload, ensure_ascii=False, indent=2)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CurllmEngine:
|
|
90
|
+
def __init__(self, config: CurllmEngineConfig) -> None:
|
|
91
|
+
self.config = config
|
|
92
|
+
|
|
93
|
+
async def fetch_web(
|
|
94
|
+
self,
|
|
95
|
+
text: str,
|
|
96
|
+
*,
|
|
97
|
+
url: str | None = None,
|
|
98
|
+
instruction: str | None = None,
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
target_url = extract_url(text, url)
|
|
101
|
+
if not target_url:
|
|
102
|
+
raise ValueError("Brak URL w prompcie — podaj https://...")
|
|
103
|
+
|
|
104
|
+
instr = instruction or instruction_from_text(text, target_url)
|
|
105
|
+
|
|
106
|
+
if self.config.mock or self.config.light_fetch:
|
|
107
|
+
try:
|
|
108
|
+
return light_fetch(target_url, instr)
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
if self.config.mock:
|
|
111
|
+
return {
|
|
112
|
+
"mode": "mock",
|
|
113
|
+
"url": target_url,
|
|
114
|
+
"instruction": instr,
|
|
115
|
+
"summary": f"Mock fetch dla {target_url}",
|
|
116
|
+
"links": [],
|
|
117
|
+
"error": str(exc),
|
|
118
|
+
}
|
|
119
|
+
raise
|
|
120
|
+
|
|
121
|
+
return await curllm_fetch(target_url, instr, self.config.upstream_url)
|
|
122
|
+
|
|
123
|
+
async def process(self, text: str, *, execute: bool = False) -> dict[str, Any]:
|
|
124
|
+
web = await self.fetch_web(text)
|
|
125
|
+
return {
|
|
126
|
+
"status": "complete",
|
|
127
|
+
"summary": f"Pobrano dane online z {web.get('url')} ({web.get('mode')})",
|
|
128
|
+
"web_data": web,
|
|
129
|
+
"context": context_block(web),
|
|
130
|
+
"mock": web.get("mode") in {"mock", "light_http"},
|
|
131
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Parse upstream HTTP errors into user-facing payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize_nlp2dsl_prompt(text: str) -> str:
|
|
11
|
+
"""Rewrite common Polish phrasing so NLP2DSL rules parser recognizes fields."""
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
text = re.sub(r"\bz\s+treścią\s*:", "treść:", text, flags=re.IGNORECASE)
|
|
15
|
+
text = re.sub(r"\bz\s+trescia\s*:", "treść:", text, flags=re.IGNORECASE)
|
|
16
|
+
return text
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_upstream_error(resp: httpx.Response) -> dict[str, Any]:
|
|
20
|
+
try:
|
|
21
|
+
data = resp.json()
|
|
22
|
+
except Exception:
|
|
23
|
+
return {"status": "error", "summary": (resp.text or "Błąd upstream")[:500]}
|
|
24
|
+
|
|
25
|
+
detail: Any = data
|
|
26
|
+
for _ in range(5):
|
|
27
|
+
if isinstance(detail, dict) and set(detail.keys()) == {"detail"}:
|
|
28
|
+
detail = detail["detail"]
|
|
29
|
+
else:
|
|
30
|
+
break
|
|
31
|
+
|
|
32
|
+
if isinstance(detail, dict):
|
|
33
|
+
error = detail.get("error")
|
|
34
|
+
hint = detail.get("hint")
|
|
35
|
+
summary = hint or error or detail.get("message") or str(detail)
|
|
36
|
+
return {
|
|
37
|
+
"status": "error",
|
|
38
|
+
"summary": summary,
|
|
39
|
+
"error": error,
|
|
40
|
+
"hint": hint,
|
|
41
|
+
"text": detail.get("text"),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {"status": "error", "summary": str(detail)[:500]}
|