smithy-engine 0.6.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.
Files changed (94) hide show
  1. smithy_engine-0.6.0/.github/workflows/ci.yml +75 -0
  2. smithy_engine-0.6.0/.github/workflows/release.yml +54 -0
  3. smithy_engine-0.6.0/.gitignore +25 -0
  4. smithy_engine-0.6.0/CHANGELOG.md +144 -0
  5. smithy_engine-0.6.0/PKG-INFO +372 -0
  6. smithy_engine-0.6.0/README.md +344 -0
  7. smithy_engine-0.6.0/bot.py +31 -0
  8. smithy_engine-0.6.0/examples/basic_bot.py +30 -0
  9. smithy_engine-0.6.0/examples/config_demo.py +49 -0
  10. smithy_engine-0.6.0/examples/config_demo.toml +18 -0
  11. smithy_engine-0.6.0/examples/config_demo_broken.toml +3 -0
  12. smithy_engine-0.6.0/examples/custom_tool.py +36 -0
  13. smithy_engine-0.6.0/examples/notepad_click.json +30 -0
  14. smithy_engine-0.6.0/examples/reframework_bot.py +126 -0
  15. smithy_engine-0.6.0/examples/reframework_bot.toml +14 -0
  16. smithy_engine-0.6.0/examples/reframework_invoices.csv +4 -0
  17. smithy_engine-0.6.0/flow.json +154 -0
  18. smithy_engine-0.6.0/my.flow.json +15 -0
  19. smithy_engine-0.6.0/pyproject.toml +59 -0
  20. smithy_engine-0.6.0/recording.json +1430 -0
  21. smithy_engine-0.6.0/requirements.lock +89 -0
  22. smithy_engine-0.6.0/schemas/flow-v2.schema.json +116 -0
  23. smithy_engine-0.6.0/src/smithy/__init__.py +75 -0
  24. smithy_engine-0.6.0/src/smithy/core/__init__.py +28 -0
  25. smithy_engine-0.6.0/src/smithy/core/blocking.py +53 -0
  26. smithy_engine-0.6.0/src/smithy/core/config.py +171 -0
  27. smithy_engine-0.6.0/src/smithy/core/errors.py +105 -0
  28. smithy_engine-0.6.0/src/smithy/core/events.py +67 -0
  29. smithy_engine-0.6.0/src/smithy/core/http_queue.py +298 -0
  30. smithy_engine-0.6.0/src/smithy/core/logging.py +73 -0
  31. smithy_engine-0.6.0/src/smithy/core/queue.py +503 -0
  32. smithy_engine-0.6.0/src/smithy/core/registry.py +66 -0
  33. smithy_engine-0.6.0/src/smithy/core/retry.py +79 -0
  34. smithy_engine-0.6.0/src/smithy/core/schema.py +92 -0
  35. smithy_engine-0.6.0/src/smithy/core/tool.py +114 -0
  36. smithy_engine-0.6.0/src/smithy/core/transactions.py +523 -0
  37. smithy_engine-0.6.0/src/smithy/facade.py +652 -0
  38. smithy_engine-0.6.0/src/smithy/py.typed +0 -0
  39. smithy_engine-0.6.0/src/smithy/windows/__init__.py +0 -0
  40. smithy_engine-0.6.0/src/smithy/windows/element.py +84 -0
  41. smithy_engine-0.6.0/src/smithy/windows/selector.py +320 -0
  42. smithy_engine-0.6.0/src/smithy/windows/selector_rank.py +280 -0
  43. smithy_engine-0.6.0/src/smithy/windows/tools/__init__.py +72 -0
  44. smithy_engine-0.6.0/src/smithy/windows/tools/_resolve.py +139 -0
  45. smithy_engine-0.6.0/src/smithy/windows/tools/click.py +147 -0
  46. smithy_engine-0.6.0/src/smithy/windows/tools/clipboard.py +82 -0
  47. smithy_engine-0.6.0/src/smithy/windows/tools/delay.py +55 -0
  48. smithy_engine-0.6.0/src/smithy/windows/tools/drag.py +82 -0
  49. smithy_engine-0.6.0/src/smithy/windows/tools/exists.py +59 -0
  50. smithy_engine-0.6.0/src/smithy/windows/tools/get_element.py +59 -0
  51. smithy_engine-0.6.0/src/smithy/windows/tools/get_text.py +76 -0
  52. smithy_engine-0.6.0/src/smithy/windows/tools/highlight.py +122 -0
  53. smithy_engine-0.6.0/src/smithy/windows/tools/hover.py +62 -0
  54. smithy_engine-0.6.0/src/smithy/windows/tools/input_text.py +77 -0
  55. smithy_engine-0.6.0/src/smithy/windows/tools/keyboard.py +270 -0
  56. smithy_engine-0.6.0/src/smithy/windows/tools/list_elements.py +92 -0
  57. smithy_engine-0.6.0/src/smithy/windows/tools/process.py +239 -0
  58. smithy_engine-0.6.0/src/smithy/windows/tools/screenshot.py +201 -0
  59. smithy_engine-0.6.0/src/smithy/windows/tools/scroll.py +100 -0
  60. smithy_engine-0.6.0/src/smithy/windows/tools/select.py +67 -0
  61. smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/__init__.py +28 -0
  62. smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/__main__.py +5 -0
  63. smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/capture.py +387 -0
  64. smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/cli.py +149 -0
  65. smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/emit.py +151 -0
  66. smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/generate.py +274 -0
  67. smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/recorder.py +739 -0
  68. smithy_engine-0.6.0/src/smithy/windows/tools/set_text.py +110 -0
  69. smithy_engine-0.6.0/src/smithy/windows/tools/wait.py +150 -0
  70. smithy_engine-0.6.0/src/smithy/windows/tools/window.py +147 -0
  71. smithy_engine-0.6.0/test.txt +1 -0
  72. smithy_engine-0.6.0/tests/conftest.py +11 -0
  73. smithy_engine-0.6.0/tests/core/__init__.py +0 -0
  74. smithy_engine-0.6.0/tests/core/test_config.py +129 -0
  75. smithy_engine-0.6.0/tests/core/test_errors.py +95 -0
  76. smithy_engine-0.6.0/tests/core/test_events.py +217 -0
  77. smithy_engine-0.6.0/tests/core/test_facade.py +340 -0
  78. smithy_engine-0.6.0/tests/core/test_http_queue.py +355 -0
  79. smithy_engine-0.6.0/tests/core/test_jsonl_logging.py +99 -0
  80. smithy_engine-0.6.0/tests/core/test_queue.py +202 -0
  81. smithy_engine-0.6.0/tests/core/test_retry.py +106 -0
  82. smithy_engine-0.6.0/tests/core/test_schema.py +169 -0
  83. smithy_engine-0.6.0/tests/core/test_tool_registry.py +150 -0
  84. smithy_engine-0.6.0/tests/core/test_transactions.py +363 -0
  85. smithy_engine-0.6.0/tests/test_audit_regressions.py +201 -0
  86. smithy_engine-0.6.0/tests/windows/__init__.py +0 -0
  87. smithy_engine-0.6.0/tests/windows/test_emit.py +141 -0
  88. smithy_engine-0.6.0/tests/windows/test_gui_tools.py +712 -0
  89. smithy_engine-0.6.0/tests/windows/test_input_text.py +17 -0
  90. smithy_engine-0.6.0/tests/windows/test_keyboard.py +52 -0
  91. smithy_engine-0.6.0/tests/windows/test_process_allowlist.py +60 -0
  92. smithy_engine-0.6.0/tests/windows/test_selector.py +157 -0
  93. smithy_engine-0.6.0/tests/windows/test_selector_capture.py +772 -0
  94. smithy_engine-0.6.0/tests/windows/test_selector_rank.py +397 -0
@@ -0,0 +1,75 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ pull_request:
7
+ branches: [master, main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v5
15
+ with:
16
+ version: "0.11"
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.14"
20
+ - run: uv pip install --system ruff
21
+ - name: Ruff check
22
+ run: ruff check src/ tests/ examples/
23
+ - name: Ruff format check
24
+ run: ruff format --check src/ tests/ examples/
25
+
26
+ typecheck:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: astral-sh/setup-uv@v5
31
+ with:
32
+ version: "0.11"
33
+ - uses: actions/setup-python@v5
34
+ with:
35
+ python-version: "3.14"
36
+ - run: uv pip install --system -e ".[dev]"
37
+ - name: Mypy strict
38
+ run: mypy src/smithy --strict
39
+
40
+ test:
41
+ strategy:
42
+ fail-fast: false
43
+ matrix:
44
+ os: [ubuntu-latest, windows-latest]
45
+ python-version: ["3.11", "3.14"]
46
+ runs-on: ${{ matrix.os }}
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+ - uses: astral-sh/setup-uv@v5
50
+ with:
51
+ version: "0.11"
52
+ - uses: actions/setup-python@v5
53
+ with:
54
+ python-version: ${{ matrix.python-version }}
55
+ - name: Install (with windows extras on windows runners)
56
+ run: uv pip install --system -e ".[dev,windows]"
57
+ - name: Pytest
58
+ run: pytest tests/ -q
59
+
60
+ build:
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ - uses: actions/checkout@v4
64
+ - uses: astral-sh/setup-uv@v5
65
+ with:
66
+ version: "0.11"
67
+ - uses: actions/setup-python@v5
68
+ with:
69
+ python-version: "3.14"
70
+ - name: Build sdist and wheel
71
+ run: uv build
72
+ - uses: actions/upload-artifact@v4
73
+ with:
74
+ name: dist
75
+ path: dist/
@@ -0,0 +1,54 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ id-token: write # PyPI trusted publishing
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ with:
17
+ version: "0.11"
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.14"
21
+ - name: Build sdist and wheel
22
+ run: uv build
23
+ - uses: actions/upload-artifact@v4
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ publish:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment: pypi
32
+ steps:
33
+ - uses: actions/download-artifact@v4
34
+ with:
35
+ name: dist
36
+ path: dist/
37
+ - name: Publish to PyPI
38
+ uses: pypa/gh-action-pypi-publish@release/v1
39
+
40
+ github-release:
41
+ needs: build
42
+ runs-on: ubuntu-latest
43
+ permissions:
44
+ contents: write
45
+ steps:
46
+ - uses: actions/download-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+ - name: Create GitHub release
51
+ uses: softprops/action-gh-release@v2
52
+ with:
53
+ files: dist/*
54
+ generate_release_notes: true
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+ .venv/
10
+ venv/
11
+ *.egg
12
+ .env
13
+
14
+ # Test artifacts
15
+ selectors.json
16
+ selectors1.json
17
+ selectors2.json
18
+ selectors3.json
19
+ test_output.txt
20
+
21
+ # Lockfile (library, not app)
22
+ uv.lock
23
+
24
+ # Smith Studio (private, paid product)
25
+ apps/smith-studio/
@@ -0,0 +1,144 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0
4
+
5
+ Playwright-style codegen: recorded flows render as runnable bot scripts.
6
+
7
+ ### Added
8
+
9
+ - Code generation (`windows/tools/selector_capture/emit.py`): any
10
+ capture file (`single`/`series`/`record` — same `nodes` shape)
11
+ renders as a `Smithy(tools=windows_tools())` script with one
12
+ `await bot.*` call per node. New `emit` CLI subcommand
13
+ (`emit -i flow.json -o bot.py [--clip]`) plus `--emit BOT.py` on
14
+ every record mode for one-pass record-to-code.
15
+ - Honest placeholders instead of silent gaps: a `TODO` header for the
16
+ unseen app launch (`process_run` + PID scoping), `text="TODO: fill
17
+ in"` for series-mode `input_text` (keys are never captured), and
18
+ `WARNING` comments from static selector-fragility scoring.
19
+
20
+ ## 0.4.1
21
+
22
+ Unified capture output: every recorder mode writes the same flow shape.
23
+
24
+ ### Changed
25
+
26
+ - `single` mode now writes `{"tool": "selector-capture", "nodes":
27
+ [...]}` with one ranked node (same shape as `series`/`record`)
28
+ instead of the divergent `captures`/`best_selector` format. The node
29
+ `args` is the ranked minimal selector, `full_path` is attached, and
30
+ numeric control types are translated — previously `best_selector`
31
+ carried the raw `"50011"` the runtime rejects. The `-d`/
32
+ `--description` flag is still accepted but only logged, not persisted.
33
+
34
+ ### Fixed
35
+
36
+ - Series-mode `windows.input_text` nodes were missing `full_path`
37
+ (keyboard flushes built nodes without the last clicked element's
38
+ path) — now every node carries it. Known limitation, now documented:
39
+ series mode captures the input *target*, not the typed text itself.
40
+
41
+ ## 0.4.0
42
+
43
+ Playwright-style selector engine for the desktop: ranked selectors with
44
+ uniqueness checks and honest confidence instead of all-fields dumps.
45
+
46
+ ### Added
47
+
48
+ - Selector ranking (`windows/selector_rank.py`): candidate generation in
49
+ priority order (automation ID → name + type → class + type, minimal
50
+ first), static stability scoring (dynamic digits/dates, wildcards, hex
51
+ runs, long names), live-desktop uniqueness check, `high`/`medium`/`low`
52
+ confidence with warnings. Low confidence means "add an anchor", never
53
+ a made-up stable selector.
54
+ - `ElementSelector.count_from_desktop(limit)` — bounded tree walk for
55
+ counting matches (strict-mode primitive; dev-time helper, not a runtime
56
+ search path).
57
+ - `resolve_element(..., strict=True)` — fail on ambiguous selectors
58
+ (2+ matches → `InvalidInput`) instead of silently taking the first.
59
+ - `generate_nodes_from_config()` — flow nodes from an already-ranked
60
+ minimal config.
61
+ - Record mode now ranks every capture: logs the winning selector,
62
+ confidence, and warnings, and emits nodes from the ranked config
63
+ (falls back to the unranked dump if the UIA walk fails).
64
+
65
+ ### Fixed
66
+
67
+ - Real captures carry numeric control types (`"50000"`) which the
68
+ runtime rejects — `build_inline_selector` now translates them to names
69
+ (`"button"`) and drops untranslatable ones.
70
+
71
+ ## 0.3.0
72
+
73
+ GUI batch: comfortable desktop automation on top of the 0.2.0 core.
74
+
75
+ ### Added
76
+
77
+ - Ten new Windows tools: `windows.scroll` (wheel over element/point),
78
+ `windows.hover` (menus, tooltips), `windows.exists` (single-lookup
79
+ boolean), `windows.get_text` (ValuePattern → Name fallback),
80
+ `windows.window` (activate/minimize/maximize/restore/move/close by PID
81
+ via Win32), `windows.select` (dropdown/combobox/list via
82
+ SelectionItemPattern), `windows.drag` (two endpoints, coordinates or
83
+ `from_*`/`to_*` selectors), `windows.clipboard` (get/set via
84
+ `pyperclip`), `windows.list_elements` (direct-children dump for
85
+ discovering automation IDs), `windows.highlight` (colored rectangle
86
+ flash for debugging selectors).
87
+ - `Smithy` facade methods for every new tool (`scroll`, `hover`,
88
+ `exists`, `get_text`, `window`, `select`, `drag`, `clipboard`,
89
+ `list_elements`, `highlight`), all accepting an optional `handle` for
90
+ PID scoping.
91
+ - Shared `_resolve.py` helpers: `resolve_point` (coordinates win over
92
+ selectors) and `resolve_element`.
93
+
94
+ ### Changed
95
+
96
+ - `windows.click` now takes `button` (left/right), `clicks` (1/2), and
97
+ `x`/`y` coordinate clicks (double right-click = two `RightClick`
98
+ calls; no module-level `DoubleClick` exists in `uiautomation`).
99
+ - `windows.wait` now takes `wait_for` (`appear`/`disappear`) with a
100
+ symmetric poll loop; `PlatformError` mid-poll counts as still present.
101
+ - `smithy[windows]` extra now includes `pyperclip` (clipboard support).
102
+
103
+ ## 0.2.0
104
+
105
+ First minor release: transactions, config, and hardening on top of the
106
+ 0.1.x tool core.
107
+
108
+ ### Added
109
+
110
+ - Transactional queue model (`core/queue.py`): `Queue` protocol,
111
+ `InMemoryQueue` / `SqliteQueue` with atomic FIFO claim, lease expiry,
112
+ `max_attempts` requeue, idempotent add, `run_id` ownership; `HttpQueue`
113
+ client for the orchestrator (stdlib only, retries on 502–504).
114
+ - REFramework-style runner (`core/transactions.py`): `run_transactions` /
115
+ `run_transactions_async`, `BusinessError` vs `InfrastructureError`
116
+ contract, `Cancelled` cooperative stop, background lease heartbeat
117
+ (capped at 30 min), `on_progress` hook, `TransactionReport`.
118
+ - TOML robot config (`core/config.py`): `load_config` with fail-fast
119
+ validation (`required` / `must_exist`), frozen attribute-style `Config`,
120
+ `SMITHY_*` env overlay (`__` nests, TOML-typed values).
121
+ - Schema validation (`core/schema.py`): `ToolRegistry.execute` validates
122
+ configs against `schema()` (hand-rolled subset, no new deps).
123
+ - Tool-level retries (`core/retry.py`): `RetryTool` wrapper
124
+ (`attempts` / `delay_ms` / `retry_on`, defaults to `ElementNotFound`).
125
+ - JSONL audit log (`core/logging.py`): `JsonlEventLogger` middleware with
126
+ `transaction_id`, duration, and error stamped per event.
127
+ - `windows_tools()` factory (`windows/tools/__init__.py`): default tool
128
+ set in one call, UIA imports stay lazy.
129
+ - `ProcessTool` allowlist is now configurable: constructor param,
130
+ `SMITHY_ALLOWED_COMMANDS` env override, `allowed_commands` introspection.
131
+ - `parse_control_type()` is public (`windows/selector.py`).
132
+ - Examples: `reframework_bot.py` (dispatcher + performer skeleton),
133
+ `config_demo.py` with good/broken TOMLs.
134
+
135
+ ### Changed
136
+
137
+ - Error model consolidated to the single `ToolError` family; the unused
138
+ legacy `SmithError` / `InvalidParams` / `ContextError` were removed.
139
+
140
+ ## 0.1.1
141
+
142
+ - Windows UI tools (process, click, wait, delay, screenshot, input_text,
143
+ keyboard, set_text, get_element), selector capture CLI, middleware
144
+ event bus, `@tool` decorator, `Smithy` facade.
@@ -0,0 +1,372 @@
1
+ Metadata-Version: 2.5
2
+ Name: smithy-engine
3
+ Version: 0.6.0
4
+ Summary: Free Python RPA engine — create automation bots with simple API
5
+ Author: as-kurosss
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Provides-Extra: all
9
+ Requires-Dist: mss>=9; extra == 'all'
10
+ Requires-Dist: pillow>=10; extra == 'all'
11
+ Requires-Dist: pynput>=1.7; extra == 'all'
12
+ Requires-Dist: pyperclip>=1.8; extra == 'all'
13
+ Requires-Dist: uiautomation>=2.0.29; extra == 'all'
14
+ Provides-Extra: capture
15
+ Requires-Dist: pynput>=1.7; extra == 'capture'
16
+ Requires-Dist: pyperclip>=1.8; extra == 'capture'
17
+ Provides-Extra: dev
18
+ Requires-Dist: mypy>=1.18; extra == 'dev'
19
+ Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
20
+ Requires-Dist: pytest>=8; extra == 'dev'
21
+ Requires-Dist: ruff>=0.12; extra == 'dev'
22
+ Provides-Extra: windows
23
+ Requires-Dist: mss>=9; extra == 'windows'
24
+ Requires-Dist: pillow>=10; extra == 'windows'
25
+ Requires-Dist: pyperclip>=1.8; extra == 'windows'
26
+ Requires-Dist: uiautomation>=2.0.29; extra == 'windows'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Smithy
30
+
31
+ Free Python RPA engine — create automation bots with simple async API.
32
+
33
+ ## Quick Start
34
+
35
+ ```python
36
+ import asyncio
37
+ from smithy import Smithy
38
+ from smithy.windows.tools import windows_tools
39
+
40
+ bot = Smithy(tools=windows_tools())
41
+
42
+
43
+ async def main() -> None:
44
+ app = await bot.process_run("notepad.exe")
45
+ await bot.wait(app, class_name="Notepad", name="*Notepad")
46
+ await bot.click(app, name="File")
47
+ await bot.delay(duration_ms=300)
48
+ await bot.click(app, name="Save As...")
49
+ await bot.input_text(app, text="hello world")
50
+ await bot.keyboard(keys="[CTRL]S")
51
+ await bot.screenshot("notepad.png")
52
+ await bot.process_stop(app)
53
+
54
+
55
+ asyncio.run(main())
56
+ ```
57
+
58
+ ## Built-in Tools
59
+
60
+ - **ProcessTool** (`windows.process`) — launch and stop Windows processes by name
61
+ - **ClickTool** (`windows.click`) — click a UI element or coordinates; `button` (left/right), `clicks` (1/2)
62
+ - **WaitTool** (`windows.wait`) — poll until a UI element appears or disappears (`wait_for`, with timeout)
63
+ - **DelayTool** (`windows.delay`) — pause execution for a fixed duration
64
+ - **ScreenshotTool** (`windows.screenshot`) — capture the screen or a window to a file
65
+ - **InputTextTool** (`windows.input_text`) — type plain text into a UI element
66
+ - **KeyboardTool** (`windows.keyboard`) — send key combos and presses (e.g. `"[CTRL]S"`, `"[CTRL!]"`, `"[ENTER]"`)
67
+ - **SetTextTool** (`windows.set_text`) — replace a UI element's text programmatically (ValuePattern / WM_SETTEXT)
68
+ - **GetElementTool** (`windows.get_element`) — read a UI element's attributes as a dict
69
+ - **ScrollTool** (`windows.scroll`) — scroll the wheel over an element or point (`direction`, `wheel_clicks`)
70
+ - **HoverTool** (`windows.hover`) — move the mouse over an element (menus, tooltips)
71
+ - **ExistsTool** (`windows.exists`) — single-lookup boolean check (no waiting, no raising)
72
+ - **GetTextTool** (`windows.get_text`) — read an element's visible text (ValuePattern → Name)
73
+ - **WindowTool** (`windows.window`) — activate/minimize/maximize/restore/move/close a window by PID
74
+ - **SelectTool** (`windows.select`) — select an item in a dropdown, combobox, or list
75
+ - **DragTool** (`windows.drag`) — drag between two endpoints (coordinates or `from_*`/`to_*` selectors)
76
+ - **ClipboardTool** (`windows.clipboard`) — read/write clipboard text (needs `pyperclip`)
77
+ - **ListElementsTool** (`windows.list_elements`) — list direct children to discover automation IDs
78
+ - **HighlightTool** (`windows.highlight`) — flash a colored rectangle for debugging selectors
79
+
80
+ All UI tools accept optional `pid` (or a `ProcessHandle`) to scope element search to a specific window.
81
+
82
+ `ProcessTool` only starts executables from its allowlist — pass
83
+ `windows_tools(allowed_commands=["myapp.exe"])` or set
84
+ `SMITHY_ALLOWED_COMMANDS="myapp.exe,other.exe"` to override the demo list.
85
+
86
+ ## Custom Tools
87
+
88
+ Create tools from simple async functions:
89
+
90
+ ```python
91
+ from smithy import Smithy, tool
92
+
93
+
94
+ @tool("greet", description="Greet a person")
95
+ async def greet(config: dict) -> dict:
96
+ name = config.get("name", "World")
97
+ return {"message": f"Hello, {name}!"}
98
+
99
+
100
+ bot = Smithy(tools=[greet])
101
+
102
+
103
+ async def main() -> None:
104
+ result = await bot.call("greet", name="Alice")
105
+ print(result["message"]) # Hello, Alice!
106
+
107
+
108
+ asyncio.run(main())
109
+ ```
110
+
111
+ ## Transactions (REFramework-style)
112
+
113
+ The framework owns the Init → Get → Process → SetStatus → End loop over a
114
+ queue (local SQLite file or orchestrator via `HttpQueue`):
115
+
116
+ ```python
117
+ import asyncio
118
+ from smithy import InMemoryQueue, run_transactions_async
119
+ from smithy.core.errors import BusinessError
120
+
121
+ queue = InMemoryQueue()
122
+ queue.get_or_create_queue("invoices", max_attempts=3)
123
+
124
+
125
+ async def process(item) -> dict:
126
+ if not item.payload.get("number"):
127
+ raise BusinessError("invoice has no number") # terminal, no retry
128
+ return {"posted": True}
129
+
130
+
131
+ async def main() -> None:
132
+ report = await run_transactions_async(queue, "invoices", process)
133
+ print(report.processed, report.succeeded, report.business_failed)
134
+
135
+
136
+ asyncio.run(main())
137
+ ```
138
+
139
+ `BusinessError` marks an item terminally failed; `InfrastructureError` (or
140
+ any unexpected exception) requeues it within the `max_attempts` budget;
141
+ `Cancelled` stops the loop cooperatively. Long items get a background
142
+ lease heartbeat (capped at 30 minutes). See
143
+ [`examples/reframework_bot.py`](examples/reframework_bot.py) for a full
144
+ dispatcher + performer skeleton.
145
+
146
+ ## Robot Config (TOML)
147
+
148
+ One TOML per robot (replaces the two-column Excel sheet), validated up
149
+ front — the bot fails in Init, never mid-run:
150
+
151
+ ```python
152
+ from smithy import load_config
153
+
154
+ CONFIG = load_config(
155
+ "reframework_bot.toml",
156
+ required=["robot.queue", "paths.workdir"],
157
+ must_exist=["paths.workdir"],
158
+ )
159
+ print(CONFIG.robot.queue) # attribute access, frozen after load
160
+ ```
161
+
162
+ Per-environment tweaks without editing TOML via `SMITHY_*` env vars:
163
+ `SMITHY_ROBOT__QUEUE=invoices-prod` overrides `robot.queue` (`__` nests,
164
+ values are TOML-typed). Secrets never live here — only references to
165
+ orchestrator assets. See [`examples/config_demo.py`](examples/config_demo.py).
166
+
167
+ ## Error Handling
168
+
169
+ ```python
170
+ from smithy.core.errors import InvalidInput, ElementNotFound, PlatformError
171
+
172
+ try:
173
+ await bot.click(app, name="Nonexistent")
174
+ except ElementNotFound:
175
+ print("Element not found")
176
+ except PlatformError as e:
177
+ print(f"Platform error: {e}")
178
+ ```
179
+
180
+ ## Selector Ranking (Playwright-style)
181
+
182
+ Record mode (`record` below) ranks every captured element like
183
+ Playwright's codegen: candidates in priority order (automation ID →
184
+ name + type → class + type), stability scoring, and a live uniqueness
185
+ check. The winning selector ships with `high`/`medium`/`low` confidence
186
+ plus warnings — `low` means the element needs an anchor, not blind trust:
187
+
188
+ ```python
189
+ from smithy.windows.selector_rank import rank_best_selector
190
+ from smithy.windows.tools.selector_capture.capture import capture_at_point
191
+
192
+ _, sel = capture_at_point(400, 300)
193
+ ranked = rank_best_selector(sel)
194
+ print(ranked.config) # e.g. {"automation_id": "btnOk"}
195
+ print(ranked.confidence, ranked.warnings)
196
+ ```
197
+
198
+ `resolve_element(..., strict=True)` fails on ambiguous selectors (2+
199
+ matches) instead of taking the first — the desktop equivalent of strict
200
+ mode. Numeric control types from real captures (`"50000"`) are
201
+ translated to names automatically.
202
+
203
+ ## Selector Capture
204
+
205
+ A dev utility for inspecting UI elements at screen coordinates and generating tool configs:
206
+
207
+ ```bash
208
+ pip install smithy-engine[capture]
209
+
210
+ # Single capture mode — one flow node
211
+ python -m smithy.windows.tools.selector_capture single -o selectors.json
212
+
213
+ # Series mode — auto-record clicks and typing
214
+ python -m smithy.windows.tools.selector_capture series -o recording.json
215
+
216
+ # Interactive record mode
217
+ python -m smithy.windows.tools.selector_capture record -o flow.json
218
+ ```
219
+
220
+ All three modes write the same shape — `{"tool": "selector-capture",
221
+ "nodes": [{"tool", "args", "full_path"}]}` (`single` is just a
222
+ one-node flow). `args` holds the ranked minimal selector (the
223
+ `best_selector` equivalent), `full_path` the full UIA path for debugging
224
+ and anchors. Note: series mode records click targets with full paths,
225
+ but keyboard input captures only the target element, not the typed text
226
+ itself — fill in `text` afterwards or use record mode.
227
+
228
+ ## Codegen (Playwright-style code recording)
229
+
230
+ Any capture file renders as a replayable bot script — record once, get
231
+ runnable code:
232
+
233
+ ```bash
234
+ python -m smithy.windows.tools.selector_capture emit -i flow.json -o bot.py
235
+
236
+ # ...or in one pass, straight from recording:
237
+ python -m smithy.windows.tools.selector_capture record -o flow.json --emit bot.py
238
+ ```
239
+
240
+ The script uses `Smithy(tools=windows_tools())` with one `await bot.*`
241
+ call per node. No magic: the recorder never sees the launched process
242
+ (so there's a `TODO` showing `process_run` + PID scoping), uncaptured
243
+ `input_text` gets an explicit `text="TODO: fill in"` placeholder, and
244
+ fragile selectors ship with `WARNING` comments. Open `bot.py` in your
245
+ editor, fill in the TODOs, run.
246
+
247
+ ## Visual Editor
248
+
249
+ The flow is built in [smithy-designer](https://github.com/as-kurosss/smithy-engine-designer) —
250
+ a separate visual editor (MIT): drag-and-drop canvas, step debugger with
251
+ breakpoints, XML-like selectors, typed variables.
252
+
253
+ ```bash
254
+ pip install smithy-designer
255
+ smithy-designer flow.json
256
+ ```
257
+
258
+ ## Flow format (v2)
259
+
260
+ The flow file is a versioned JSON document — the contract between the
261
+ designer, the file on disk, and the execution engine. The schema lives in
262
+ [`schemas/flow-v2.schema.json`](schemas/flow-v2.schema.json).
263
+
264
+ Compatibility rules:
265
+
266
+ - **adding optional fields does not bump the version** — unknown keys are
267
+ ignored by older readers (`label`, `breakpoints` were added this way);
268
+ - **removing/renaming fields or changing semantics requires v3** and a
269
+ migration path; readers must reject unknown versions with an explicit error;
270
+ - the engine and the designer both validate `version` on load and never
271
+ silently overwrite a file of a different version.
272
+
273
+ Example:
274
+
275
+ ```json
276
+ {
277
+ "version": 2,
278
+ "nodes": [
279
+ { "id": "start", "kind": "start", "config": {}, "position": [120, 160] },
280
+ { "id": "a1", "kind": "tool", "tool": "windows.click",
281
+ "config": { "name": "OK", "control_type": "Button" },
282
+ "save_as": "result", "position": [340, 160] }
283
+ ],
284
+ "edges": [
285
+ { "id": "e1", "source": "start", "source_handle": "out", "target": "a1" }
286
+ ]
287
+ }
288
+ ```
289
+
290
+ ## Install
291
+
292
+ ```bash
293
+ pip install smithy-engine # core (no deps)
294
+ pip install smithy-engine[windows] # Windows UIA tools
295
+ pip install smithy-engine[capture] # selector capture (pynput + pyperclip)
296
+ pip install smithy-engine[all] # everything
297
+ pip install -e ".[dev]" # development
298
+ ```
299
+
300
+ ## Development
301
+
302
+ ```bash
303
+ # Using uv (recommended)
304
+ uv venv .venv
305
+ .venv\Scripts\activate
306
+ uv pip install -e ".[dev,windows,capture]"
307
+
308
+ # Or with pip
309
+ python -m venv .venv
310
+ .venv\Scripts\activate
311
+ pip install -e ".[dev,windows,capture]"
312
+
313
+ pytest # run tests
314
+ ruff check src/ tests/ # linter
315
+ mypy src/smithy --strict # type check
316
+ ```
317
+
318
+ ## Project Structure
319
+
320
+ ```
321
+ src/smithy/
322
+ ├── __init__.py — Public API: Smithy, ProcessHandle, Tool, errors
323
+ ├── facade.py — Smithy facade (async tool dispatch)
324
+ ├── core/
325
+ │ ├── tool.py — Tool protocol, AbstractTool, @tool decorator
326
+ │ ├── registry.py — ToolRegistry (name → tool dispatch, schema validation)
327
+ │ ├── schema.py — Hand-rolled JSON Schema subset validator
328
+ │ ├── retry.py — RetryTool (attempts / delay / retry_on)
329
+ │ ├── logging.py — JsonlEventLogger (JSONL audit log middleware)
330
+ │ ├── config.py — TOML robot config + SMITHY_* env overlay
331
+ │ ├── queue.py — Queue protocol, InMemoryQueue, SqliteQueue
332
+ │ ├── http_queue.py — HttpQueue client for the orchestrator
333
+ │ ├── transactions.py — REFramework-style runner + heartbeat
334
+ │ ├── events.py — EventBus, ToolEvent, Middleware
335
+ │ └── errors.py — Error hierarchy (ToolError, ElementNotFound, etc.)└── windows/
336
+ ├── element.py — SafeUIElement (thread-safe COM wrapper)
337
+ ├── selector.py — ElementSelector (UIA tree search + match counting)
338
+ ├── selector_rank.py — Selector ranking (candidates, scoring, confidence)
339
+ └── tools/
340
+ ├── process.py — ProcessTool
341
+ ├── click.py — ClickTool (button/clicks/coordinates)
342
+ ├── wait.py — WaitTool (appear/disappear)
343
+ ├── delay.py — DelayTool
344
+ ├── screenshot.py — ScreenshotTool
345
+ ├── input_text.py — InputTextTool
346
+ ├── keyboard.py — KeyboardTool
347
+ ├── set_text.py — SetTextTool
348
+ ├── get_element.py — GetElementTool
349
+ ├── scroll.py — ScrollTool
350
+ ├── hover.py — HoverTool
351
+ ├── exists.py — ExistsTool
352
+ ├── get_text.py — GetTextTool
353
+ ├── window.py — WindowTool
354
+ ├── select.py — SelectTool
355
+ ├── drag.py — DragTool
356
+ ├── clipboard.py — ClipboardTool
357
+ ├── list_elements.py — ListElementsTool
358
+ ├── highlight.py — HighlightTool
359
+ ├── _resolve.py — Shared element/point resolution helpers
360
+ └── selector_capture/ — Dev tool for UI inspection + codegen
361
+ ```
362
+
363
+ ## Examples
364
+
365
+ - [`examples/basic_bot.py`](examples/basic_bot.py) — Launch Notepad and interact with its UI
366
+ - [`examples/custom_tool.py`](examples/custom_tool.py) — Create and use custom tools
367
+ - [`examples/reframework_bot.py`](examples/reframework_bot.py) — REFramework skeleton: dispatcher + performer over a queue
368
+ - [`examples/config_demo.py`](examples/config_demo.py) — Load and validate a TOML robot config
369
+
370
+ ## License
371
+
372
+ MIT