abstract-toolserver 0.0.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.
- abstract_toolserver-0.0.1/PKG-INFO +120 -0
- abstract_toolserver-0.0.1/README.md +77 -0
- abstract_toolserver-0.0.1/pyproject.toml +3 -0
- abstract_toolserver-0.0.1/setup.cfg +4 -0
- abstract_toolserver-0.0.1/setup.py +44 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver/__init__.py +5 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver/__main__.py +5 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver/app.py +33 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver/gates.py +109 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver/routes/__init__.py +16 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver/tools.py +367 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver.egg-info/PKG-INFO +120 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver.egg-info/SOURCES.txt +15 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver.egg-info/dependency_links.txt +1 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver.egg-info/entry_points.txt +2 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver.egg-info/requires.txt +24 -0
- abstract_toolserver-0.0.1/src/abstract_toolserver.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abstract_toolserver
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The abstract_* ecosystem exposed as an API-callable AI toolset: functions become self-describing Flask endpoints with /endpoints discovery and ?help.
|
|
5
|
+
Home-page: https://github.com/AbstractEndeavors/abstract_toolserver
|
|
6
|
+
Author: putkoff
|
|
7
|
+
Author-email: partners@abstractendeavors.com
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: abstract_flask
|
|
16
|
+
Requires-Dist: abstract_utilities
|
|
17
|
+
Provides-Extra: files
|
|
18
|
+
Requires-Dist: abstract_search; extra == "files"
|
|
19
|
+
Requires-Dist: abstract_paths; extra == "files"
|
|
20
|
+
Provides-Extra: web
|
|
21
|
+
Requires-Dist: abstract_webtools; extra == "web"
|
|
22
|
+
Provides-Extra: media
|
|
23
|
+
Requires-Dist: abstract_ocr; extra == "media"
|
|
24
|
+
Requires-Dist: abstract_pandas; extra == "media"
|
|
25
|
+
Requires-Dist: media_intelligence; extra == "media"
|
|
26
|
+
Provides-Extra: ai
|
|
27
|
+
Requires-Dist: abstract_ai; extra == "ai"
|
|
28
|
+
Provides-Extra: db
|
|
29
|
+
Requires-Dist: abstract_database; extra == "db"
|
|
30
|
+
Provides-Extra: ui
|
|
31
|
+
Requires-Dist: abstract_clicks; extra == "ui"
|
|
32
|
+
Requires-Dist: abstract_windows; extra == "ui"
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: author-email
|
|
35
|
+
Dynamic: classifier
|
|
36
|
+
Dynamic: description
|
|
37
|
+
Dynamic: description-content-type
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: provides-extra
|
|
40
|
+
Dynamic: requires-dist
|
|
41
|
+
Dynamic: requires-python
|
|
42
|
+
Dynamic: summary
|
|
43
|
+
|
|
44
|
+
# abstract_toolserver
|
|
45
|
+
|
|
46
|
+
The `abstract_*` ecosystem exposed as an **API-callable AI toolset** — every tool
|
|
47
|
+
is a plain Python function turned into a self-describing HTTP endpoint by
|
|
48
|
+
[`abstract_flask`](https://github.com/AbstractEndeavors/abstract_flask). A portable
|
|
49
|
+
tool layer any model (Claude, hugpy, …) can drive over HTTP instead of being
|
|
50
|
+
bound to one runtime's tool harness.
|
|
51
|
+
|
|
52
|
+
## Run it
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install abstract_toolserver # + the extras you want to expose
|
|
56
|
+
python -m abstract_toolserver # HOST/PORT/DEBUG from TOOLSERVER_* env
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from abstract_toolserver import get_toolserver_app
|
|
61
|
+
app = get_toolserver_app() # a normal Flask/WSGI app
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Self-describing surface
|
|
65
|
+
|
|
66
|
+
The app auto-mounts introspection endpoints (from `abstract_flask`):
|
|
67
|
+
|
|
68
|
+
| Endpoint | What it gives an LLM |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `GET /prefixes` | the tool categories (`/fs`, `/db`, `/ui`, …) |
|
|
71
|
+
| `GET /endpoints` | every tool as `{endpoint, url, methods}` |
|
|
72
|
+
| `GET /<cat>/<tool>?help=true` | that tool's signature/help |
|
|
73
|
+
|
|
74
|
+
Call a tool with JSON; unknown keys are pruned to the function signature, and the
|
|
75
|
+
reply is `{"result": ...}` (or `{"error": ...}`). Discover-and-dispatch from a
|
|
76
|
+
client is already provided by `abstract_apis.make_endpoint_call`.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
curl -s localhost:5000/fs/count_tokens -d '{"text":"hello world"}'
|
|
80
|
+
# {"result": 2}
|
|
81
|
+
curl -s localhost:5000/db/schema # {"result": {table: [cols...]}}
|
|
82
|
+
curl -s 'localhost:5000/db/query?help=true'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Tool categories
|
|
86
|
+
|
|
87
|
+
| Prefix | Tools | Backend |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `/fs` | search, read_span, extract, read_file, write_file, read_json, find_keys, find_paths, glob, imports, find_content | abstract_search, abstract_utilities, abstract_paths |
|
|
90
|
+
| `/text` | count_tokens, chunk, detect_language | abstract_utilities |
|
|
91
|
+
| `/web` | text, links, attributes | abstract_webtools |
|
|
92
|
+
| `/media` | ocr_image, pdf_text, summarize, keywords, transcribe | abstract_ocr, abstract_pandas, media_intelligence |
|
|
93
|
+
| `/ai` | query | abstract_ai |
|
|
94
|
+
| `/db` | tables, schema, columns, fetch, **query** | abstract_database |
|
|
95
|
+
| `/sys` | run_cmd | stdlib (gated) |
|
|
96
|
+
| `/ui` | capture, monitors, ocr, windows, **click_verify** | abstract_clicks, abstract_windows |
|
|
97
|
+
|
|
98
|
+
## Safety gates
|
|
99
|
+
|
|
100
|
+
Backends load lazily, so the server boots on a headless box and a missing backend
|
|
101
|
+
errors only when its tool is called. Beyond that:
|
|
102
|
+
|
|
103
|
+
- **`/db/query`** — read-only gate: rejects anything that isn't a single
|
|
104
|
+
`SELECT`/`WITH`, blocks stacked statements and data-modifying keywords. Use
|
|
105
|
+
`/db/fetch` (identifier-composed, params-not-SQL) as the default read path.
|
|
106
|
+
- **`/sys/run_cmd`** — disabled unless `TOOLSERVER_CMD_ALLOWLIST=ls,grep,…` is set;
|
|
107
|
+
only allowlisted binaries run.
|
|
108
|
+
- **`/fs/read_file` · `/fs/write_file`** — local-only; the underlying SSH/remote
|
|
109
|
+
kwargs are never exposed at the boundary.
|
|
110
|
+
- **`/ui/click_verify`** — the click→observe→**verify** loop: locate (text or image
|
|
111
|
+
template) → click → re-capture → report whether the screen (or a `region`)
|
|
112
|
+
changed. The half most tool APIs lack.
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
| Env var | Purpose |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `TOOLSERVER_HOST` / `TOOLSERVER_PORT` / `TOOLSERVER_DEBUG` | bind + debug |
|
|
119
|
+
| `TOOLSERVER_CMD_ALLOWLIST` | comma-separated binaries `/sys/run_cmd` may run |
|
|
120
|
+
| `SOLCATCHER_POSTGRESQL_*` | DB connection (via abstract_database) |
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# abstract_toolserver
|
|
2
|
+
|
|
3
|
+
The `abstract_*` ecosystem exposed as an **API-callable AI toolset** — every tool
|
|
4
|
+
is a plain Python function turned into a self-describing HTTP endpoint by
|
|
5
|
+
[`abstract_flask`](https://github.com/AbstractEndeavors/abstract_flask). A portable
|
|
6
|
+
tool layer any model (Claude, hugpy, …) can drive over HTTP instead of being
|
|
7
|
+
bound to one runtime's tool harness.
|
|
8
|
+
|
|
9
|
+
## Run it
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install abstract_toolserver # + the extras you want to expose
|
|
13
|
+
python -m abstract_toolserver # HOST/PORT/DEBUG from TOOLSERVER_* env
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from abstract_toolserver import get_toolserver_app
|
|
18
|
+
app = get_toolserver_app() # a normal Flask/WSGI app
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Self-describing surface
|
|
22
|
+
|
|
23
|
+
The app auto-mounts introspection endpoints (from `abstract_flask`):
|
|
24
|
+
|
|
25
|
+
| Endpoint | What it gives an LLM |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `GET /prefixes` | the tool categories (`/fs`, `/db`, `/ui`, …) |
|
|
28
|
+
| `GET /endpoints` | every tool as `{endpoint, url, methods}` |
|
|
29
|
+
| `GET /<cat>/<tool>?help=true` | that tool's signature/help |
|
|
30
|
+
|
|
31
|
+
Call a tool with JSON; unknown keys are pruned to the function signature, and the
|
|
32
|
+
reply is `{"result": ...}` (or `{"error": ...}`). Discover-and-dispatch from a
|
|
33
|
+
client is already provided by `abstract_apis.make_endpoint_call`.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -s localhost:5000/fs/count_tokens -d '{"text":"hello world"}'
|
|
37
|
+
# {"result": 2}
|
|
38
|
+
curl -s localhost:5000/db/schema # {"result": {table: [cols...]}}
|
|
39
|
+
curl -s 'localhost:5000/db/query?help=true'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Tool categories
|
|
43
|
+
|
|
44
|
+
| Prefix | Tools | Backend |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `/fs` | search, read_span, extract, read_file, write_file, read_json, find_keys, find_paths, glob, imports, find_content | abstract_search, abstract_utilities, abstract_paths |
|
|
47
|
+
| `/text` | count_tokens, chunk, detect_language | abstract_utilities |
|
|
48
|
+
| `/web` | text, links, attributes | abstract_webtools |
|
|
49
|
+
| `/media` | ocr_image, pdf_text, summarize, keywords, transcribe | abstract_ocr, abstract_pandas, media_intelligence |
|
|
50
|
+
| `/ai` | query | abstract_ai |
|
|
51
|
+
| `/db` | tables, schema, columns, fetch, **query** | abstract_database |
|
|
52
|
+
| `/sys` | run_cmd | stdlib (gated) |
|
|
53
|
+
| `/ui` | capture, monitors, ocr, windows, **click_verify** | abstract_clicks, abstract_windows |
|
|
54
|
+
|
|
55
|
+
## Safety gates
|
|
56
|
+
|
|
57
|
+
Backends load lazily, so the server boots on a headless box and a missing backend
|
|
58
|
+
errors only when its tool is called. Beyond that:
|
|
59
|
+
|
|
60
|
+
- **`/db/query`** — read-only gate: rejects anything that isn't a single
|
|
61
|
+
`SELECT`/`WITH`, blocks stacked statements and data-modifying keywords. Use
|
|
62
|
+
`/db/fetch` (identifier-composed, params-not-SQL) as the default read path.
|
|
63
|
+
- **`/sys/run_cmd`** — disabled unless `TOOLSERVER_CMD_ALLOWLIST=ls,grep,…` is set;
|
|
64
|
+
only allowlisted binaries run.
|
|
65
|
+
- **`/fs/read_file` · `/fs/write_file`** — local-only; the underlying SSH/remote
|
|
66
|
+
kwargs are never exposed at the boundary.
|
|
67
|
+
- **`/ui/click_verify`** — the click→observe→**verify** loop: locate (text or image
|
|
68
|
+
template) → click → re-capture → report whether the screen (or a `region`)
|
|
69
|
+
changed. The half most tool APIs lack.
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
| Env var | Purpose |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `TOOLSERVER_HOST` / `TOOLSERVER_PORT` / `TOOLSERVER_DEBUG` | bind + debug |
|
|
76
|
+
| `TOOLSERVER_CMD_ALLOWLIST` | comma-separated binaries `/sys/run_cmd` may run |
|
|
77
|
+
| `SOLCATCHER_POSTGRESQL_*` | DB connection (via abstract_database) |
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import setuptools
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setuptools.setup(
|
|
7
|
+
name='abstract_toolserver',
|
|
8
|
+
version='0.0.1',
|
|
9
|
+
author='putkoff',
|
|
10
|
+
author_email='partners@abstractendeavors.com',
|
|
11
|
+
description="The abstract_* ecosystem exposed as an API-callable AI toolset: functions become self-describing Flask endpoints with /endpoints discovery and ?help.",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type='text/markdown',
|
|
14
|
+
url="https://github.com/AbstractEndeavors/abstract_toolserver",
|
|
15
|
+
classifiers=[
|
|
16
|
+
'Development Status :: 3 - Alpha',
|
|
17
|
+
'Intended Audience :: Developers',
|
|
18
|
+
'License :: OSI Approved :: MIT License',
|
|
19
|
+
'Programming Language :: Python :: 3',
|
|
20
|
+
'Programming Language :: Python :: 3.11',
|
|
21
|
+
],
|
|
22
|
+
install_requires=[
|
|
23
|
+
'abstract_flask',
|
|
24
|
+
'abstract_utilities',
|
|
25
|
+
],
|
|
26
|
+
extras_require={
|
|
27
|
+
# Tool backends are imported lazily; install only what you expose.
|
|
28
|
+
'files': ['abstract_search', 'abstract_paths'],
|
|
29
|
+
'web': ['abstract_webtools'],
|
|
30
|
+
'media': ['abstract_ocr', 'abstract_pandas', 'media_intelligence'],
|
|
31
|
+
'ai': ['abstract_ai'],
|
|
32
|
+
'db': ['abstract_database'],
|
|
33
|
+
'ui': ['abstract_clicks', 'abstract_windows'],
|
|
34
|
+
},
|
|
35
|
+
package_dir={"": "src"},
|
|
36
|
+
packages=setuptools.find_packages(where="src"),
|
|
37
|
+
python_requires=">=3.8",
|
|
38
|
+
entry_points={
|
|
39
|
+
"console_scripts": [
|
|
40
|
+
"abstract-toolserver = abstract_toolserver.app:main",
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
setup_requires=['wheel'],
|
|
44
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App factory + runner for the AI toolserver.
|
|
3
|
+
|
|
4
|
+
from abstract_toolserver.app import get_toolserver_app
|
|
5
|
+
app = get_toolserver_app() # WSGI app, /endpoints + /prefixes + ?help
|
|
6
|
+
app.run()
|
|
7
|
+
|
|
8
|
+
or just: python -m abstract_toolserver
|
|
9
|
+
"""
|
|
10
|
+
from abstract_flask import get_Flask_app, main_flask_start
|
|
11
|
+
|
|
12
|
+
from . import routes
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_toolserver_app(allowed_origins=None, debug=None, **flask_kwargs):
|
|
16
|
+
"""Return the fully-wired Flask app (all tool blueprints + introspection)."""
|
|
17
|
+
return get_Flask_app(
|
|
18
|
+
name="abstract_toolserver",
|
|
19
|
+
routes=routes,
|
|
20
|
+
allowed_origins=allowed_origins,
|
|
21
|
+
debug=debug,
|
|
22
|
+
**flask_kwargs,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
"""Run the server; HOST/PORT/DEBUG come from TOOLSERVER_* env vars."""
|
|
28
|
+
app = get_toolserver_app()
|
|
29
|
+
main_flask_start(app, key_head="TOOLSERVER")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
main()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Safety gates and lazy-import plumbing for the AI toolserver.
|
|
3
|
+
|
|
4
|
+
Everything a tool needs to be *safe to expose over HTTP* lives here:
|
|
5
|
+
* lazy import so the Flask app boots even when a heavy backend is absent,
|
|
6
|
+
* a read-only SQL guard for the raw-query tool,
|
|
7
|
+
* a command allowlist for shell exec (off by default),
|
|
8
|
+
* a scrubber that strips the SSH/remote-exec kwargs off the file tools.
|
|
9
|
+
"""
|
|
10
|
+
import importlib
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shlex
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolError(Exception):
|
|
17
|
+
"""Raised by a tool wrapper; the Flask layer turns it into {"error": ...}."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ──────────────────────────────────────────────────────────
|
|
21
|
+
# Lazy import ("pkg.module:attr" or "pkg.module")
|
|
22
|
+
# ──────────────────────────────────────────────────────────
|
|
23
|
+
def lazy(path):
|
|
24
|
+
"""Import on first call so a missing backend can't stop the server booting."""
|
|
25
|
+
module, _, attr = path.partition(":")
|
|
26
|
+
try:
|
|
27
|
+
mod = importlib.import_module(module)
|
|
28
|
+
except Exception as e: # ImportError, or a backend that explodes at import
|
|
29
|
+
raise ToolError(f"backend '{module}' unavailable: {e}")
|
|
30
|
+
if not attr:
|
|
31
|
+
return mod
|
|
32
|
+
try:
|
|
33
|
+
return getattr(mod, attr)
|
|
34
|
+
except AttributeError:
|
|
35
|
+
raise ToolError(f"'{module}' has no attribute '{attr}'")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def first_lazy(*paths):
|
|
39
|
+
"""Return the first import path that resolves; used when a symbol moved."""
|
|
40
|
+
last = None
|
|
41
|
+
for p in paths:
|
|
42
|
+
try:
|
|
43
|
+
return lazy(p)
|
|
44
|
+
except ToolError as e:
|
|
45
|
+
last = e
|
|
46
|
+
raise last or ToolError("no import path resolved")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ──────────────────────────────────────────────────────────
|
|
50
|
+
# SQL read-only guard
|
|
51
|
+
# ──────────────────────────────────────────────────────────
|
|
52
|
+
_WRITE_TOKENS = re.compile(
|
|
53
|
+
r"\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE|"
|
|
54
|
+
r"COPY|MERGE|CALL|DO|VACUUM|REINDEX|COMMENT|SET)\b",
|
|
55
|
+
re.IGNORECASE,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def assert_readonly_sql(query):
|
|
60
|
+
"""Reject anything that isn't a single SELECT/WITH read. Raises ToolError."""
|
|
61
|
+
if not isinstance(query, str) or not query.strip():
|
|
62
|
+
raise ToolError("query must be a non-empty string")
|
|
63
|
+
stripped = query.strip().rstrip(";").strip()
|
|
64
|
+
# block stacked statements (a trailing ';' was already removed above)
|
|
65
|
+
if ";" in stripped:
|
|
66
|
+
raise ToolError("multiple statements are not allowed")
|
|
67
|
+
if not re.match(r"^(SELECT|WITH)\b", stripped, re.IGNORECASE):
|
|
68
|
+
raise ToolError("only SELECT / WITH queries are allowed")
|
|
69
|
+
if _WRITE_TOKENS.search(stripped):
|
|
70
|
+
raise ToolError("query contains a data-modifying keyword")
|
|
71
|
+
return stripped
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ──────────────────────────────────────────────────────────
|
|
75
|
+
# Shell command allowlist (run_cmd is OFF unless explicitly enabled)
|
|
76
|
+
# ──────────────────────────────────────────────────────────
|
|
77
|
+
def _allowlist():
|
|
78
|
+
raw = os.environ.get("TOOLSERVER_CMD_ALLOWLIST", "")
|
|
79
|
+
return {c.strip() for c in raw.split(",") if c.strip()}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def assert_cmd_allowed(cmd):
|
|
83
|
+
"""Only permit a binary named in TOOLSERVER_CMD_ALLOWLIST. Raises ToolError."""
|
|
84
|
+
allow = _allowlist()
|
|
85
|
+
if not allow:
|
|
86
|
+
raise ToolError(
|
|
87
|
+
"run_cmd disabled — set TOOLSERVER_CMD_ALLOWLIST=ls,grep,... to enable"
|
|
88
|
+
)
|
|
89
|
+
try:
|
|
90
|
+
parts = shlex.split(cmd)
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
raise ToolError(f"unparseable command: {e}")
|
|
93
|
+
if not parts:
|
|
94
|
+
raise ToolError("empty command")
|
|
95
|
+
binary = os.path.basename(parts[0])
|
|
96
|
+
if binary not in allow:
|
|
97
|
+
raise ToolError(f"'{binary}' not in allowlist {sorted(allow)}")
|
|
98
|
+
return parts
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ──────────────────────────────────────────────────────────
|
|
102
|
+
# File-tool kwarg scrubber
|
|
103
|
+
# ──────────────────────────────────────────────────────────
|
|
104
|
+
_SSH_KEYS = {"user_at_host", "password", "key", "host", "ssh", "remote"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def strip_remote_kwargs(kwargs):
|
|
108
|
+
"""Drop the SSH/remote-exec kwargs so file tools stay local-only."""
|
|
109
|
+
return {k: v for k, v in kwargs.items() if k not in _SSH_KEYS}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Routes module consumed by abstract_flask.get_Flask_app(routes=...).
|
|
3
|
+
|
|
4
|
+
get_Flask_app scans this module for any top-level object named ``*_bp`` that is
|
|
5
|
+
a Blueprint and registers it, so all we do here is build one blueprint and hang
|
|
6
|
+
every tool category off it via register_categories.
|
|
7
|
+
"""
|
|
8
|
+
from abstract_flask import get_bp, register_categories
|
|
9
|
+
|
|
10
|
+
from ..tools import TOOLSETS
|
|
11
|
+
|
|
12
|
+
# name → "tools_bp" so the factory's *_bp auto-discovery finds it
|
|
13
|
+
tools_bp, logger = get_bp("tools", __name__)
|
|
14
|
+
|
|
15
|
+
# {category: {route: func}} → /category/route (+ /category/route/ , GET+POST)
|
|
16
|
+
register_categories(tools_bp, TOOLSETS)
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The compiled AI toolset.
|
|
3
|
+
|
|
4
|
+
Each function here is a thin, JSON-in / JSON-out wrapper over a symbol that
|
|
5
|
+
already exists in the abstract_* ecosystem. Backends are imported lazily (via
|
|
6
|
+
gates.lazy) so this module imports cleanly on a headless box; a tool whose
|
|
7
|
+
backend is missing raises ToolError only when actually called.
|
|
8
|
+
|
|
9
|
+
The TOOLSETS dict at the bottom is the whole API surface — it maps
|
|
10
|
+
{category: {route_name: callable}} and is fed straight to
|
|
11
|
+
abstract_flask.register_categories.
|
|
12
|
+
"""
|
|
13
|
+
import hashlib
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
from .gates import (
|
|
18
|
+
ToolError,
|
|
19
|
+
assert_cmd_allowed,
|
|
20
|
+
assert_readonly_sql,
|
|
21
|
+
first_lazy,
|
|
22
|
+
lazy,
|
|
23
|
+
strip_remote_kwargs,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ══════════════════════════════════════════════════════════
|
|
28
|
+
# FILES / CODE (abstract_search, abstract_utilities, abstract_paths)
|
|
29
|
+
# ══════════════════════════════════════════════════════════
|
|
30
|
+
def search_files(needle, paths, case_sensitive=False, regex=False):
|
|
31
|
+
"""Grep a string/regex across files or dirs (code, office, pdf)."""
|
|
32
|
+
fn = lazy("abstract_search:search_files")
|
|
33
|
+
return fn(needle, paths, case_sensitive=case_sensitive, regex=regex)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_span(path, ranges=None):
|
|
37
|
+
"""Read 1-based inclusive line ranges from one file (whole file if omitted)."""
|
|
38
|
+
return lazy("abstract_search:read_span")(path, ranges=ranges)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extract_text(path):
|
|
42
|
+
"""Any single file → plain text (pdf/docx/xlsx/pptx/code); errors-as-data."""
|
|
43
|
+
return lazy("abstract_search:extract_text")(path)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def read_file(file_path):
|
|
47
|
+
"""Read a local text file (SSH/remote kwargs are intentionally not exposed)."""
|
|
48
|
+
return lazy("abstract_utilities:read_from_file")(file_path=file_path)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def write_file(file_path, contents):
|
|
52
|
+
"""Write text to a local file (local-only; remote-exec kwargs stripped)."""
|
|
53
|
+
fn = lazy("abstract_utilities:write_to_file")
|
|
54
|
+
return fn(contents=contents, file_path=file_path, **strip_remote_kwargs({}))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def read_json(file_path):
|
|
58
|
+
"""Robustly load a JSON file."""
|
|
59
|
+
return lazy("abstract_utilities:safe_read_from_json")(file_path)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def find_keys(data, target_keys):
|
|
63
|
+
"""Recursively collect all values for the given keys in nested JSON."""
|
|
64
|
+
return lazy("abstract_utilities:find_keys")(data, target_keys)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_paths_to_key(data, key):
|
|
68
|
+
"""Return the paths where a key occurs in nested JSON."""
|
|
69
|
+
return lazy("abstract_utilities:find_paths_to_key")(data, key)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def count_tokens(text, model_name="gpt-4"):
|
|
73
|
+
"""Count LLM tokens in a string."""
|
|
74
|
+
return lazy("abstract_utilities:num_tokens_from_string")(text, model_name=model_name)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def chunk_text(data, max_tokens, model_name="gpt-4"):
|
|
78
|
+
"""Split text/code into token-bounded chunks."""
|
|
79
|
+
return lazy("abstract_utilities:chunk_any_to_tokens")(data, max_tokens, model_name=model_name)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def detect_language(text):
|
|
83
|
+
"""Heuristically identify the programming language of a snippet."""
|
|
84
|
+
return lazy("abstract_utilities:detect_language_from_text")(text)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def glob_files(path, pattern, ext=None):
|
|
88
|
+
"""Glob files under a path by pattern."""
|
|
89
|
+
return lazy("abstract_paths:glob_search")(path, pattern, ext=ext)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def python_imports(file_path):
|
|
93
|
+
"""List the imports of a Python file."""
|
|
94
|
+
return lazy("abstract_paths:extract_python_imports")(file_path)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def find_content(dirs, strings, get_lines=True):
|
|
98
|
+
"""grep-like recursive search: files containing the given strings (+line nums)."""
|
|
99
|
+
fn = first_lazy(
|
|
100
|
+
"abstract_paths.content_utils.backs:findContent",
|
|
101
|
+
"abstract_paths:findContent",
|
|
102
|
+
)
|
|
103
|
+
dirs = dirs if isinstance(dirs, (list, tuple)) else [dirs]
|
|
104
|
+
return fn(*dirs, strings=strings, get_lines=get_lines)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ══════════════════════════════════════════════════════════
|
|
108
|
+
# WEB (abstract_webtools)
|
|
109
|
+
# ══════════════════════════════════════════════════════════
|
|
110
|
+
def web_text(url):
|
|
111
|
+
"""Fetch a URL and return its visible page text."""
|
|
112
|
+
return lazy("abstract_webtools:get_soup_text")(url)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def web_links(url):
|
|
116
|
+
"""Crawl a page and return every link on it."""
|
|
117
|
+
return lazy("abstract_webtools:get_all_crawl_links")(url)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def web_attributes(url, tags_list=None):
|
|
121
|
+
"""Extract attribute values for given tags on a page."""
|
|
122
|
+
return lazy("abstract_webtools:get_all_attribute_values")(url=url, tags_list=tags_list)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ══════════════════════════════════════════════════════════
|
|
126
|
+
# MEDIA → TEXT (abstract_ocr, abstract_pandas, media_intelligence)
|
|
127
|
+
# ══════════════════════════════════════════════════════════
|
|
128
|
+
def ocr_image(image_path):
|
|
129
|
+
"""OCR a single image to text."""
|
|
130
|
+
return first_lazy(
|
|
131
|
+
"abstract_ocr:extract_text_from_image",
|
|
132
|
+
"media_intelligence.ocr:image_to_text",
|
|
133
|
+
)(image_path)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def pdf_text(path, ocr_if_empty=True):
|
|
137
|
+
"""Full text of a PDF (pdfplumber + OCR fallback)."""
|
|
138
|
+
return lazy("abstract_pandas:pdf_to_text")(path, ocr_if_empty=ocr_if_empty)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def summarize(text, max_output_words=None):
|
|
142
|
+
"""Abstractive summary of text (delegates to the media_intelligence facade)."""
|
|
143
|
+
fn = first_lazy("media_intelligence.enrich:summarize", "abstract_hugpy:summarize")
|
|
144
|
+
return fn(text=text, max_output_words=max_output_words) if max_output_words else fn(text=text)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def keywords(text, top_n=None):
|
|
148
|
+
"""Keyword/keyphrase extraction from text."""
|
|
149
|
+
fn = first_lazy("media_intelligence.enrich:keywords", "abstract_hugpy:extract_keywords")
|
|
150
|
+
return fn(text, top_n=top_n) if top_n else fn(text)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def transcribe(path):
|
|
154
|
+
"""Transcribe a local audio/video file to text."""
|
|
155
|
+
return first_lazy(
|
|
156
|
+
"media_intelligence.transcribe:transcribe",
|
|
157
|
+
"abstract_hugpy:whisper_transcribe",
|
|
158
|
+
)(path)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ══════════════════════════════════════════════════════════
|
|
162
|
+
# AI / LLM (abstract_ai)
|
|
163
|
+
# ══════════════════════════════════════════════════════════
|
|
164
|
+
def ai_query(prompt, model=None, completion_percentage=50):
|
|
165
|
+
"""Run one prompt through the abstract_ai model pipeline."""
|
|
166
|
+
fn = lazy("abstract_ai:make_general_query")
|
|
167
|
+
return fn(prompt=prompt, model=model, completion_percentage=completion_percentage)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ══════════════════════════════════════════════════════════
|
|
171
|
+
# DATABASE (abstract_database) — server-side, co-located
|
|
172
|
+
# ══════════════════════════════════════════════════════════
|
|
173
|
+
def db_tables(schema="public"):
|
|
174
|
+
"""List table names in a schema."""
|
|
175
|
+
return lazy("abstract_database:get_all_table_names")(schema=schema)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def db_schema(schema="public"):
|
|
179
|
+
"""Map every table to its columns — let the model discover the DB."""
|
|
180
|
+
return lazy("abstract_database:get_all_table_info")(schema=schema)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def db_columns(table_name, schema="public"):
|
|
184
|
+
"""List the columns of one table."""
|
|
185
|
+
return lazy("abstract_database:get_column_names")(table_name, schema=schema)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def db_fetch(table_name, column_names="*", search_map=None, any_value=False, schema="public"):
|
|
189
|
+
"""Safe, identifier-composed SELECT (the model supplies params, not SQL)."""
|
|
190
|
+
fn = lazy("abstract_database:fetch_any_combo_")
|
|
191
|
+
return fn(
|
|
192
|
+
table_name=table_name,
|
|
193
|
+
column_names=column_names,
|
|
194
|
+
search_map=search_map,
|
|
195
|
+
any_value=any_value,
|
|
196
|
+
schema=schema,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def db_query(query, values=None):
|
|
201
|
+
"""Run a raw SELECT/WITH query (read-only gate: writes are rejected)."""
|
|
202
|
+
safe = assert_readonly_sql(query)
|
|
203
|
+
return lazy("abstract_database:execute_query")(safe, values=values, fetch=True)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ══════════════════════════════════════════════════════════
|
|
207
|
+
# SYSTEM (gated)
|
|
208
|
+
# ══════════════════════════════════════════════════════════
|
|
209
|
+
def run_cmd(cmd):
|
|
210
|
+
"""Run a shell command — only binaries in TOOLSERVER_CMD_ALLOWLIST (off by default)."""
|
|
211
|
+
import subprocess
|
|
212
|
+
|
|
213
|
+
parts = assert_cmd_allowed(cmd)
|
|
214
|
+
proc = subprocess.run(parts, capture_output=True, text=True, timeout=60)
|
|
215
|
+
return {"returncode": proc.returncode, "stdout": proc.stdout, "stderr": proc.stderr}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ══════════════════════════════════════════════════════════
|
|
219
|
+
# COMPUTER-USE (abstract_clicks / abstract_windows) + the verify glue
|
|
220
|
+
# ══════════════════════════════════════════════════════════
|
|
221
|
+
def screen_capture(monitor_index=1, output_path=None):
|
|
222
|
+
"""Screenshot one monitor to a PNG; returns the file path."""
|
|
223
|
+
return lazy("abstract_clicks.utils.monitor_utils:capture_monitor")(monitor_index, output_path=output_path)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def screen_monitors():
|
|
227
|
+
"""Enumerate monitors with geometry."""
|
|
228
|
+
return lazy("abstract_clicks.utils.monitor_utils:get_monitors")()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def screen_ocr(screenshot_file="new_screen.png", confidence_threshold=60):
|
|
232
|
+
"""OCR the screen → list of {text,x,y,width,height,confidence}."""
|
|
233
|
+
return first_lazy(
|
|
234
|
+
"abstract_clicks:perform_ocr",
|
|
235
|
+
"abstract_clicks.managers.clipboardManager.utils:perform_ocr",
|
|
236
|
+
)(screenshot_file, confidence_threshold=confidence_threshold)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def list_windows(with_geometry=True):
|
|
240
|
+
"""Enumerate all open windows with geometry + process info (X11)."""
|
|
241
|
+
fn = lazy("abstract_windows.window_utils.window_utils:get_all_parsed_windows")
|
|
242
|
+
return fn(with_signature=True, with_geometry=with_geometry)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _locate(target=None, template_path=None, monitor_index=1):
|
|
246
|
+
"""Return (x, y) screen coords for a text target or an image template, or None."""
|
|
247
|
+
shot = screen_capture(monitor_index=monitor_index)
|
|
248
|
+
if template_path:
|
|
249
|
+
matches = first_lazy(
|
|
250
|
+
"abstract_clicks:get_ocr_locate_image",
|
|
251
|
+
"abstract_clicks.managers.clipboardManager.clipboardManager:ocr_locate_image",
|
|
252
|
+
)(shot, template_path)
|
|
253
|
+
if matches:
|
|
254
|
+
m = matches[0]
|
|
255
|
+
c = m.get("center") or (m.get("x"), m.get("y"))
|
|
256
|
+
return (int(c[0]), int(c[1]))
|
|
257
|
+
return None
|
|
258
|
+
# text target via OCR box centres
|
|
259
|
+
boxes = screen_ocr(shot)
|
|
260
|
+
tl = (target or "").strip().lower()
|
|
261
|
+
for b in boxes or []:
|
|
262
|
+
if tl and tl in str(b.get("text", "")).strip().lower():
|
|
263
|
+
return (int(b["x"] + b.get("width", 0) / 2), int(b["y"] + b.get("height", 0) / 2))
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _region_hash(png_path, region=None):
|
|
268
|
+
if region:
|
|
269
|
+
try:
|
|
270
|
+
Image = lazy("PIL.Image:") # module
|
|
271
|
+
img = Image.open(png_path)
|
|
272
|
+
x, y, w, h = region
|
|
273
|
+
img = img.crop((x, y, x + w, y + h))
|
|
274
|
+
return hashlib.sha256(img.tobytes()).hexdigest()
|
|
275
|
+
except ToolError:
|
|
276
|
+
pass # PIL missing → fall through to full-file hash
|
|
277
|
+
with open(png_path, "rb") as fh:
|
|
278
|
+
return hashlib.sha256(fh.read()).hexdigest()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def click_and_verify(target=None, template_path=None, x=None, y=None,
|
|
282
|
+
monitor_index=1, region=None, settle=0.6):
|
|
283
|
+
"""
|
|
284
|
+
The missing computer-use loop: locate → click → re-capture → report whether
|
|
285
|
+
the screen changed. Supply either explicit (x,y), a text `target`, or an
|
|
286
|
+
image `template_path`. `region`=[x,y,w,h] narrows the change check.
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
gui = lazy("abstract_clicks.managers.importClasses.importClasses:get_auto_gui")()
|
|
290
|
+
except ToolError:
|
|
291
|
+
gui = lazy("pyautogui:") # empty attr → the module itself
|
|
292
|
+
|
|
293
|
+
if x is None or y is None:
|
|
294
|
+
coords = _locate(target=target, template_path=template_path, monitor_index=monitor_index)
|
|
295
|
+
if not coords:
|
|
296
|
+
return {"clicked": None, "changed": False,
|
|
297
|
+
"error": "target not found on screen"}
|
|
298
|
+
x, y = coords
|
|
299
|
+
|
|
300
|
+
before = screen_capture(monitor_index=monitor_index)
|
|
301
|
+
b_hash = _region_hash(before, region)
|
|
302
|
+
|
|
303
|
+
gui.click(x, y)
|
|
304
|
+
time.sleep(settle)
|
|
305
|
+
|
|
306
|
+
after = screen_capture(monitor_index=monitor_index)
|
|
307
|
+
a_hash = _region_hash(after, region)
|
|
308
|
+
|
|
309
|
+
return {"clicked": [int(x), int(y)], "changed": b_hash != a_hash,
|
|
310
|
+
"before": before, "after": after}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ══════════════════════════════════════════════════════════
|
|
314
|
+
# THE API SURFACE → {category: {route: func}}
|
|
315
|
+
# ══════════════════════════════════════════════════════════
|
|
316
|
+
TOOLSETS = {
|
|
317
|
+
"fs": {
|
|
318
|
+
"search": search_files,
|
|
319
|
+
"read_span": read_span,
|
|
320
|
+
"extract": extract_text,
|
|
321
|
+
"read_file": read_file,
|
|
322
|
+
"write_file": write_file,
|
|
323
|
+
"read_json": read_json,
|
|
324
|
+
"find_keys": find_keys,
|
|
325
|
+
"find_paths": find_paths_to_key,
|
|
326
|
+
"glob": glob_files,
|
|
327
|
+
"imports": python_imports,
|
|
328
|
+
"find_content": find_content,
|
|
329
|
+
},
|
|
330
|
+
"text": {
|
|
331
|
+
"count_tokens": count_tokens,
|
|
332
|
+
"chunk": chunk_text,
|
|
333
|
+
"detect_language": detect_language,
|
|
334
|
+
},
|
|
335
|
+
"web": {
|
|
336
|
+
"text": web_text,
|
|
337
|
+
"links": web_links,
|
|
338
|
+
"attributes": web_attributes,
|
|
339
|
+
},
|
|
340
|
+
"media": {
|
|
341
|
+
"ocr_image": ocr_image,
|
|
342
|
+
"pdf_text": pdf_text,
|
|
343
|
+
"summarize": summarize,
|
|
344
|
+
"keywords": keywords,
|
|
345
|
+
"transcribe": transcribe,
|
|
346
|
+
},
|
|
347
|
+
"ai": {
|
|
348
|
+
"query": ai_query,
|
|
349
|
+
},
|
|
350
|
+
"db": {
|
|
351
|
+
"tables": db_tables,
|
|
352
|
+
"schema": db_schema,
|
|
353
|
+
"columns": db_columns,
|
|
354
|
+
"fetch": db_fetch,
|
|
355
|
+
"query": db_query, # SELECT-only gated
|
|
356
|
+
},
|
|
357
|
+
"sys": {
|
|
358
|
+
"run_cmd": run_cmd, # allowlist gated, off by default
|
|
359
|
+
},
|
|
360
|
+
"ui": {
|
|
361
|
+
"capture": screen_capture,
|
|
362
|
+
"monitors": screen_monitors,
|
|
363
|
+
"ocr": screen_ocr,
|
|
364
|
+
"windows": list_windows,
|
|
365
|
+
"click_verify": click_and_verify,
|
|
366
|
+
},
|
|
367
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abstract_toolserver
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The abstract_* ecosystem exposed as an API-callable AI toolset: functions become self-describing Flask endpoints with /endpoints discovery and ?help.
|
|
5
|
+
Home-page: https://github.com/AbstractEndeavors/abstract_toolserver
|
|
6
|
+
Author: putkoff
|
|
7
|
+
Author-email: partners@abstractendeavors.com
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: abstract_flask
|
|
16
|
+
Requires-Dist: abstract_utilities
|
|
17
|
+
Provides-Extra: files
|
|
18
|
+
Requires-Dist: abstract_search; extra == "files"
|
|
19
|
+
Requires-Dist: abstract_paths; extra == "files"
|
|
20
|
+
Provides-Extra: web
|
|
21
|
+
Requires-Dist: abstract_webtools; extra == "web"
|
|
22
|
+
Provides-Extra: media
|
|
23
|
+
Requires-Dist: abstract_ocr; extra == "media"
|
|
24
|
+
Requires-Dist: abstract_pandas; extra == "media"
|
|
25
|
+
Requires-Dist: media_intelligence; extra == "media"
|
|
26
|
+
Provides-Extra: ai
|
|
27
|
+
Requires-Dist: abstract_ai; extra == "ai"
|
|
28
|
+
Provides-Extra: db
|
|
29
|
+
Requires-Dist: abstract_database; extra == "db"
|
|
30
|
+
Provides-Extra: ui
|
|
31
|
+
Requires-Dist: abstract_clicks; extra == "ui"
|
|
32
|
+
Requires-Dist: abstract_windows; extra == "ui"
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: author-email
|
|
35
|
+
Dynamic: classifier
|
|
36
|
+
Dynamic: description
|
|
37
|
+
Dynamic: description-content-type
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: provides-extra
|
|
40
|
+
Dynamic: requires-dist
|
|
41
|
+
Dynamic: requires-python
|
|
42
|
+
Dynamic: summary
|
|
43
|
+
|
|
44
|
+
# abstract_toolserver
|
|
45
|
+
|
|
46
|
+
The `abstract_*` ecosystem exposed as an **API-callable AI toolset** — every tool
|
|
47
|
+
is a plain Python function turned into a self-describing HTTP endpoint by
|
|
48
|
+
[`abstract_flask`](https://github.com/AbstractEndeavors/abstract_flask). A portable
|
|
49
|
+
tool layer any model (Claude, hugpy, …) can drive over HTTP instead of being
|
|
50
|
+
bound to one runtime's tool harness.
|
|
51
|
+
|
|
52
|
+
## Run it
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install abstract_toolserver # + the extras you want to expose
|
|
56
|
+
python -m abstract_toolserver # HOST/PORT/DEBUG from TOOLSERVER_* env
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from abstract_toolserver import get_toolserver_app
|
|
61
|
+
app = get_toolserver_app() # a normal Flask/WSGI app
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Self-describing surface
|
|
65
|
+
|
|
66
|
+
The app auto-mounts introspection endpoints (from `abstract_flask`):
|
|
67
|
+
|
|
68
|
+
| Endpoint | What it gives an LLM |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `GET /prefixes` | the tool categories (`/fs`, `/db`, `/ui`, …) |
|
|
71
|
+
| `GET /endpoints` | every tool as `{endpoint, url, methods}` |
|
|
72
|
+
| `GET /<cat>/<tool>?help=true` | that tool's signature/help |
|
|
73
|
+
|
|
74
|
+
Call a tool with JSON; unknown keys are pruned to the function signature, and the
|
|
75
|
+
reply is `{"result": ...}` (or `{"error": ...}`). Discover-and-dispatch from a
|
|
76
|
+
client is already provided by `abstract_apis.make_endpoint_call`.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
curl -s localhost:5000/fs/count_tokens -d '{"text":"hello world"}'
|
|
80
|
+
# {"result": 2}
|
|
81
|
+
curl -s localhost:5000/db/schema # {"result": {table: [cols...]}}
|
|
82
|
+
curl -s 'localhost:5000/db/query?help=true'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Tool categories
|
|
86
|
+
|
|
87
|
+
| Prefix | Tools | Backend |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `/fs` | search, read_span, extract, read_file, write_file, read_json, find_keys, find_paths, glob, imports, find_content | abstract_search, abstract_utilities, abstract_paths |
|
|
90
|
+
| `/text` | count_tokens, chunk, detect_language | abstract_utilities |
|
|
91
|
+
| `/web` | text, links, attributes | abstract_webtools |
|
|
92
|
+
| `/media` | ocr_image, pdf_text, summarize, keywords, transcribe | abstract_ocr, abstract_pandas, media_intelligence |
|
|
93
|
+
| `/ai` | query | abstract_ai |
|
|
94
|
+
| `/db` | tables, schema, columns, fetch, **query** | abstract_database |
|
|
95
|
+
| `/sys` | run_cmd | stdlib (gated) |
|
|
96
|
+
| `/ui` | capture, monitors, ocr, windows, **click_verify** | abstract_clicks, abstract_windows |
|
|
97
|
+
|
|
98
|
+
## Safety gates
|
|
99
|
+
|
|
100
|
+
Backends load lazily, so the server boots on a headless box and a missing backend
|
|
101
|
+
errors only when its tool is called. Beyond that:
|
|
102
|
+
|
|
103
|
+
- **`/db/query`** — read-only gate: rejects anything that isn't a single
|
|
104
|
+
`SELECT`/`WITH`, blocks stacked statements and data-modifying keywords. Use
|
|
105
|
+
`/db/fetch` (identifier-composed, params-not-SQL) as the default read path.
|
|
106
|
+
- **`/sys/run_cmd`** — disabled unless `TOOLSERVER_CMD_ALLOWLIST=ls,grep,…` is set;
|
|
107
|
+
only allowlisted binaries run.
|
|
108
|
+
- **`/fs/read_file` · `/fs/write_file`** — local-only; the underlying SSH/remote
|
|
109
|
+
kwargs are never exposed at the boundary.
|
|
110
|
+
- **`/ui/click_verify`** — the click→observe→**verify** loop: locate (text or image
|
|
111
|
+
template) → click → re-capture → report whether the screen (or a `region`)
|
|
112
|
+
changed. The half most tool APIs lack.
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
| Env var | Purpose |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `TOOLSERVER_HOST` / `TOOLSERVER_PORT` / `TOOLSERVER_DEBUG` | bind + debug |
|
|
119
|
+
| `TOOLSERVER_CMD_ALLOWLIST` | comma-separated binaries `/sys/run_cmd` may run |
|
|
120
|
+
| `SOLCATCHER_POSTGRESQL_*` | DB connection (via abstract_database) |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/abstract_toolserver/__init__.py
|
|
5
|
+
src/abstract_toolserver/__main__.py
|
|
6
|
+
src/abstract_toolserver/app.py
|
|
7
|
+
src/abstract_toolserver/gates.py
|
|
8
|
+
src/abstract_toolserver/tools.py
|
|
9
|
+
src/abstract_toolserver.egg-info/PKG-INFO
|
|
10
|
+
src/abstract_toolserver.egg-info/SOURCES.txt
|
|
11
|
+
src/abstract_toolserver.egg-info/dependency_links.txt
|
|
12
|
+
src/abstract_toolserver.egg-info/entry_points.txt
|
|
13
|
+
src/abstract_toolserver.egg-info/requires.txt
|
|
14
|
+
src/abstract_toolserver.egg-info/top_level.txt
|
|
15
|
+
src/abstract_toolserver/routes/__init__.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
abstract_flask
|
|
2
|
+
abstract_utilities
|
|
3
|
+
|
|
4
|
+
[ai]
|
|
5
|
+
abstract_ai
|
|
6
|
+
|
|
7
|
+
[db]
|
|
8
|
+
abstract_database
|
|
9
|
+
|
|
10
|
+
[files]
|
|
11
|
+
abstract_search
|
|
12
|
+
abstract_paths
|
|
13
|
+
|
|
14
|
+
[media]
|
|
15
|
+
abstract_ocr
|
|
16
|
+
abstract_pandas
|
|
17
|
+
media_intelligence
|
|
18
|
+
|
|
19
|
+
[ui]
|
|
20
|
+
abstract_clicks
|
|
21
|
+
abstract_windows
|
|
22
|
+
|
|
23
|
+
[web]
|
|
24
|
+
abstract_webtools
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
abstract_toolserver
|