ivoryos-edge 0.1.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.
- ivoryos_edge-0.1.1/MANIFEST.in +2 -0
- ivoryos_edge-0.1.1/PKG-INFO +70 -0
- ivoryos_edge-0.1.1/README.md +40 -0
- ivoryos_edge-0.1.1/build_backend.py +66 -0
- ivoryos_edge-0.1.1/ivoryos_edge/__init__.py +22 -0
- ivoryos_edge-0.1.1/ivoryos_edge/__main__.py +4 -0
- ivoryos_edge-0.1.1/ivoryos_edge/agent/__init__.py +13 -0
- ivoryos_edge-0.1.1/ivoryos_edge/agent/ask.py +130 -0
- ivoryos_edge-0.1.1/ivoryos_edge/agent/chat.py +200 -0
- ivoryos_edge-0.1.1/ivoryos_edge/agent/deck.py +207 -0
- ivoryos_edge-0.1.1/ivoryos_edge/agent/history.py +160 -0
- ivoryos_edge-0.1.1/ivoryos_edge/agent/mcp_server.py +315 -0
- ivoryos_edge-0.1.1/ivoryos_edge/agent/providers.py +309 -0
- ivoryos_edge-0.1.1/ivoryos_edge/agent/routes.py +1129 -0
- ivoryos_edge-0.1.1/ivoryos_edge/agent/safety_draft.py +184 -0
- ivoryos_edge-0.1.1/ivoryos_edge/agent/validate.py +539 -0
- ivoryos_edge-0.1.1/ivoryos_edge/broker.py +268 -0
- ivoryos_edge-0.1.1/ivoryos_edge/campaigns.py +247 -0
- ivoryos_edge-0.1.1/ivoryos_edge/classic.py +220 -0
- ivoryos_edge-0.1.1/ivoryos_edge/cli.py +142 -0
- ivoryos_edge-0.1.1/ivoryos_edge/cloud_pairing.py +281 -0
- ivoryos_edge-0.1.1/ivoryos_edge/compatibility.py +133 -0
- ivoryos_edge-0.1.1/ivoryos_edge/datasheet.py +368 -0
- ivoryos_edge-0.1.1/ivoryos_edge/deck.py +154 -0
- ivoryos_edge-0.1.1/ivoryos_edge/deck_config.py +224 -0
- ivoryos_edge-0.1.1/ivoryos_edge/expressions.py +259 -0
- ivoryos_edge-0.1.1/ivoryos_edge/instance_lock.py +103 -0
- ivoryos_edge-0.1.1/ivoryos_edge/introspection.py +1250 -0
- ivoryos_edge-0.1.1/ivoryos_edge/labware.py +313 -0
- ivoryos_edge-0.1.1/ivoryos_edge/live.py +130 -0
- ivoryos_edge-0.1.1/ivoryos_edge/models.py +234 -0
- ivoryos_edge-0.1.1/ivoryos_edge/optimizer/ax_optimizer.py +439 -0
- ivoryos_edge-0.1.1/ivoryos_edge/optimizer/base_optimizer.py +112 -0
- ivoryos_edge-0.1.1/ivoryos_edge/optimizer/baybe_optimizer.py +400 -0
- ivoryos_edge-0.1.1/ivoryos_edge/optimizer/constraints.py +212 -0
- ivoryos_edge-0.1.1/ivoryos_edge/optimizer/nimo_optimizer.py +225 -0
- ivoryos_edge-0.1.1/ivoryos_edge/optimizer/registry.py +63 -0
- ivoryos_edge-0.1.1/ivoryos_edge/origin_guard.py +129 -0
- ivoryos_edge-0.1.1/ivoryos_edge/parts.py +65 -0
- ivoryos_edge-0.1.1/ivoryos_edge/paths.py +59 -0
- ivoryos_edge-0.1.1/ivoryos_edge/plugins.py +295 -0
- ivoryos_edge-0.1.1/ivoryos_edge/queue.py +2918 -0
- ivoryos_edge-0.1.1/ivoryos_edge/restart.py +127 -0
- ivoryos_edge-0.1.1/ivoryos_edge/run_edits.py +339 -0
- ivoryos_edge-0.1.1/ivoryos_edge/runtime.py +170 -0
- ivoryos_edge-0.1.1/ivoryos_edge/safety.py +1355 -0
- ivoryos_edge-0.1.1/ivoryos_edge/server.py +2788 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/404/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/404.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/__next.__PAGE__.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/__next._full.txt +27 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/082t0g3a2sfim.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/09dhvumbzpqy-.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/0cwww4o6rzoro.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/0epiczc8lfnmb.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/0f7c99nchwkwr.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/0j1jjl1we-k53.js +68 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/0n79bqobc7usb.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/10hewadzaxemp.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/13bcz82-w1sy0.js +31 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/14p4wg13h2qxq.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/16b3hgm00dstg.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1805184i-q2p9.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1cq1h_-0ihhcs.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1g8e26l1vwumd.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1gjk-lnpmx4xb.js +22 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1pfflhpwef9mw.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1pmdgl6xlkz8v.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1rl_o025_4rbw.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1s73ivatypy7z.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1t5_-leq_0v4f.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1tgvbmmpponjq.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/1wlr5ikxkapwl.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/25tzfvp5q3oz0.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/2alwbtjc3tdp-.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/2ay1ktguq3mou.css +3 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/2ji0fkvck-xt_.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/2m8d2zm0tizrs.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/2trsnqcxaidz_.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/2ye5q0u-v564a.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/31ui9n6optrl0.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/3ddg3ngla-895.js +18 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/3hxkm6np690__.css +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/3x8m_ka65dm-k.js +39 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/448o__y5i7w-9.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/chunks/turbopack-1ar5u93f7rv0m.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/1bffadaabf893a1e-s.3-6t-g6q0vh0a.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/2bbe8d2671613f1f-s.0k62hbripvv8p.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/2c55a0e60120577a-s.0-dom-5bn10r2.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/5476f68d60460930-s.2uwcyprjm3xu3.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/5ce348bf30bf5439-s.31988l_ccedte.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/797e433ab948586e-s.p.0r6juujl39pe6.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/9c72aa0f40e4eef8-s.1y4-pdgsjb-pw.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/ad66f9afd8947f86-s.3lvt2whj97whp.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/favicon.2pac4ynac9mfn.ico +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/media/icon.2lb5-s1yde0r6.png +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/r5nNvy6Xg_RdE1H9usGp6/_buildManifest.js +11 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/r5nNvy6Xg_RdE1H9usGp6/_clientMiddlewareManifest.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_next/static/r5nNvy6Xg_RdE1H9usGp6/_ssgManifest.js +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_not-found/__next._full.txt +22 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_not-found/__next._not-found.__PAGE__.txt +28 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_not-found/__next._tree.txt +3 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_not-found/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/_not-found/index.txt +22 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/campaign/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/campaign/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/campaign/__next.campaign.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/campaign/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/campaign/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/cloud/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/cloud/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/cloud/__next.cloud.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/cloud/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/cloud/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/data/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/data/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/data/__next.data.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/data/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/data/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/designer/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/designer/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/designer/__next.designer.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/designer/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/designer/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/execution/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/execution/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/execution/__next.execution.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/execution/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/execution/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/favicon.ico +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/icon.png +0 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/index.txt +27 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/instruments/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/instruments/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/instruments/__next.instruments.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/instruments/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/instruments/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/ivoryos-mark.svg +86 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/launcher/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/launcher/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/launcher/__next.launcher.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/launcher/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/launcher/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/library/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/library/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/library/__next.library.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/library/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/library/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/once/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/once/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/once/__next.once.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/once/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/once/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/optimize/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/optimize/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/optimize/__next.optimize.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/optimize/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/optimize/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/plugin/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/plugin/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/plugin/__next.plugin.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/plugin/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/plugin/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/queue/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/queue/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/queue/__next.queue.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/queue/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/queue/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/safety/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/safety/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/safety/__next.safety.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/safety/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/safety/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/settings/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/settings/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/settings/__next.settings.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/settings/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/settings/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/stages/__next._full.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/stages/__next._tree.txt +6 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/stages/__next.stages.__PAGE__.txt +32 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/stages/index.html +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge/ui/stages/index.txt +29 -0
- ivoryos_edge-0.1.1/ivoryos_edge/units.py +37 -0
- ivoryos_edge-0.1.1/ivoryos_edge/workflows.py +882 -0
- ivoryos_edge-0.1.1/ivoryos_edge.egg-info/PKG-INFO +70 -0
- ivoryos_edge-0.1.1/ivoryos_edge.egg-info/SOURCES.txt +204 -0
- ivoryos_edge-0.1.1/ivoryos_edge.egg-info/dependency_links.txt +1 -0
- ivoryos_edge-0.1.1/ivoryos_edge.egg-info/entry_points.txt +2 -0
- ivoryos_edge-0.1.1/ivoryos_edge.egg-info/requires.txt +22 -0
- ivoryos_edge-0.1.1/ivoryos_edge.egg-info/top_level.txt +1 -0
- ivoryos_edge-0.1.1/pyproject.toml +62 -0
- ivoryos_edge-0.1.1/setup.cfg +4 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ivoryos-edge
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: IvoryOS Edge Server
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/ivoryos-ai/ivoryos-next
|
|
7
|
+
Project-URL: Documentation, https://ivoryos-ai.github.io/ivoryos-next/
|
|
8
|
+
Project-URL: Issues, https://github.com/ivoryos-ai/ivoryos-next/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: fastapi
|
|
12
|
+
Requires-Dist: uvicorn
|
|
13
|
+
Requires-Dist: python-socketio[client]
|
|
14
|
+
Requires-Dist: pydantic
|
|
15
|
+
Requires-Dist: python-dotenv
|
|
16
|
+
Requires-Dist: aiohttp
|
|
17
|
+
Requires-Dist: sqlalchemy[asyncio]
|
|
18
|
+
Requires-Dist: aiosqlite
|
|
19
|
+
Requires-Dist: paho-mqtt
|
|
20
|
+
Requires-Dist: httpx>=0.28.1
|
|
21
|
+
Requires-Dist: pandas
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
25
|
+
Requires-Dist: httpx; extra == "test"
|
|
26
|
+
Provides-Extra: mcp
|
|
27
|
+
Requires-Dist: mcp>=1.10; extra == "mcp"
|
|
28
|
+
Provides-Extra: claude
|
|
29
|
+
Requires-Dist: anthropic>=1.0; extra == "claude"
|
|
30
|
+
|
|
31
|
+
# ivoryos-edge
|
|
32
|
+
|
|
33
|
+
The IvoryOS edge: it runs next to your instruments, reads the Python classes that drive them, and
|
|
34
|
+
serves a web page to design, run and optimize experiments with them. Every method becomes a step
|
|
35
|
+
you can drag into a workflow, and every argument a field of the right type. Runs, results and
|
|
36
|
+
workflows are kept on that computer.
|
|
37
|
+
|
|
38
|
+
Most people use it through the [IvoryOS desktop app](https://github.com/ivoryos-ai/ivoryos-next/releases),
|
|
39
|
+
which installs this package itself. Install it on its own to run a deck from your own Python
|
|
40
|
+
environment, such as one your editor uses.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install ivoryos-edge
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Python 3.10 or newer. The web interface is inside the package; nothing else is needed.
|
|
49
|
+
|
|
50
|
+
## Run your instruments
|
|
51
|
+
|
|
52
|
+
End a script that creates your instrument objects with `ivoryos_edge.run(__name__)`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import ivoryos_edge
|
|
56
|
+
from my_lab import SyringePump
|
|
57
|
+
|
|
58
|
+
pump = SyringePump(port="COM3") # each object becomes an instrument
|
|
59
|
+
|
|
60
|
+
ivoryos_edge.run(__name__) # http://localhost:8080
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then open `http://localhost:8080` in a browser on that computer. A deck can also be a JSON file,
|
|
64
|
+
run with `ivoryos-edge --deck deck.json`.
|
|
65
|
+
|
|
66
|
+
## Links
|
|
67
|
+
|
|
68
|
+
- [Documentation](https://ivoryos-ai.github.io/ivoryos-next/)
|
|
69
|
+
- [Automation Hub](https://ivoryos.ai): drivers, platforms and workflow templates
|
|
70
|
+
- [Source and issues](https://github.com/ivoryos-ai/ivoryos-next)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ivoryos-edge
|
|
2
|
+
|
|
3
|
+
The IvoryOS edge: it runs next to your instruments, reads the Python classes that drive them, and
|
|
4
|
+
serves a web page to design, run and optimize experiments with them. Every method becomes a step
|
|
5
|
+
you can drag into a workflow, and every argument a field of the right type. Runs, results and
|
|
6
|
+
workflows are kept on that computer.
|
|
7
|
+
|
|
8
|
+
Most people use it through the [IvoryOS desktop app](https://github.com/ivoryos-ai/ivoryos-next/releases),
|
|
9
|
+
which installs this package itself. Install it on its own to run a deck from your own Python
|
|
10
|
+
environment, such as one your editor uses.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install ivoryos-edge
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Python 3.10 or newer. The web interface is inside the package; nothing else is needed.
|
|
19
|
+
|
|
20
|
+
## Run your instruments
|
|
21
|
+
|
|
22
|
+
End a script that creates your instrument objects with `ivoryos_edge.run(__name__)`:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import ivoryos_edge
|
|
26
|
+
from my_lab import SyringePump
|
|
27
|
+
|
|
28
|
+
pump = SyringePump(port="COM3") # each object becomes an instrument
|
|
29
|
+
|
|
30
|
+
ivoryos_edge.run(__name__) # http://localhost:8080
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then open `http://localhost:8080` in a browser on that computer. A deck can also be a JSON file,
|
|
34
|
+
run with `ivoryos-edge --deck deck.json`.
|
|
35
|
+
|
|
36
|
+
## Links
|
|
37
|
+
|
|
38
|
+
- [Documentation](https://ivoryos-ai.github.io/ivoryos-next/)
|
|
39
|
+
- [Automation Hub](https://ivoryos.ai): drivers, platforms and workflow templates
|
|
40
|
+
- [Source and issues](https://github.com/ivoryos-ai/ivoryos-next)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The build backend of ivoryos-edge: setuptools, refusing a wheel or sdist with no web UI in it.
|
|
2
|
+
|
|
3
|
+
A wheel or sdist is the edge as people install it (`pip install ivoryos-edge`, the desktop app's
|
|
4
|
+
bundled wheel), and it serves the pages it carries in `ivoryos_edge/ui/`, which are the same
|
|
5
|
+
version as its API. That folder is a build output, not source: scripts/build-edge.js at the
|
|
6
|
+
repository root builds the frontend, copies frontend/out into it and builds. Building without it
|
|
7
|
+
used to succeed and gave an edge that starts with an API and no pages, so it is refused here, and
|
|
8
|
+
the built file is checked to hold the pages too (a package-data rule that stops matching would
|
|
9
|
+
otherwise drop them silently).
|
|
10
|
+
|
|
11
|
+
An editable install (`pip install -e`, a source checkout in development) needs no copy: the edge
|
|
12
|
+
serves frontend/out from the checkout (`_frontend_dirs` in ivoryos_edge/server.py).
|
|
13
|
+
"""
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import tarfile
|
|
17
|
+
import zipfile
|
|
18
|
+
|
|
19
|
+
from setuptools import build_meta as _setuptools
|
|
20
|
+
from setuptools.build_meta import * # noqa: F401,F403 every other hook, editable installs included
|
|
21
|
+
|
|
22
|
+
_UI_INDEX = "ivoryos_edge/ui/index.html"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _refuse(message):
|
|
26
|
+
raise RuntimeError(
|
|
27
|
+
f"{message}\nA wheel or sdist of ivoryos-edge must carry the web UI, or the edge it installs "
|
|
28
|
+
"starts with no pages. Build it with `npm run build:edge` from the repository root, which "
|
|
29
|
+
"builds frontend/ and copies frontend/out into ivoryos_edge/ui/ first. (In development, "
|
|
30
|
+
"`pip install -e ./edge_server` needs no copy: it serves frontend/out from the checkout.)"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _require_ui():
|
|
35
|
+
if not os.path.isfile(_UI_INDEX):
|
|
36
|
+
_refuse(f"No web UI to package: {os.path.abspath(_UI_INDEX)} does not exist.")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _check_built(path, names):
|
|
40
|
+
if not any(name == _UI_INDEX or name.endswith("/" + _UI_INDEX) for name in names):
|
|
41
|
+
os.remove(path)
|
|
42
|
+
_refuse(f"{os.path.basename(path)} came out without {_UI_INDEX}, so it was removed. "
|
|
43
|
+
"Check [tool.setuptools.package-data] in edge_server/pyproject.toml.")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
|
47
|
+
_require_ui()
|
|
48
|
+
# setuptools copies the package into build/lib and packs everything there, earlier builds'
|
|
49
|
+
# files too: the UI's script names change with every frontend build, so old ones would pile up.
|
|
50
|
+
shutil.rmtree(os.path.join("build", "lib", "ivoryos_edge", "ui"), ignore_errors=True)
|
|
51
|
+
name = _setuptools.build_wheel(wheel_directory, config_settings, metadata_directory)
|
|
52
|
+
path = os.path.join(wheel_directory, name)
|
|
53
|
+
with zipfile.ZipFile(path) as wheel:
|
|
54
|
+
names = wheel.namelist()
|
|
55
|
+
_check_built(path, names)
|
|
56
|
+
return name
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_sdist(sdist_directory, config_settings=None):
|
|
60
|
+
_require_ui()
|
|
61
|
+
name = _setuptools.build_sdist(sdist_directory, config_settings)
|
|
62
|
+
path = os.path.join(sdist_directory, name)
|
|
63
|
+
with tarfile.open(path) as sdist:
|
|
64
|
+
names = sdist.getnames()
|
|
65
|
+
_check_built(path, names)
|
|
66
|
+
return name
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""IvoryOS edge server.
|
|
2
|
+
|
|
3
|
+
`ivoryos_edge.run(__name__)` is resolved lazily (PEP 562) rather than imported here. Importing the
|
|
4
|
+
server fixes every data path (see paths.py) and creates the database engine, and the command-line
|
|
5
|
+
entry point has to set `IVORYOS_DATA_DIR` before that happens -- which it cannot do if merely
|
|
6
|
+
importing the package has already imported the server.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__all__ = ["run", "Unit"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
if name == "run":
|
|
14
|
+
from .server import run
|
|
15
|
+
return run
|
|
16
|
+
if name == "Unit":
|
|
17
|
+
# `from ivoryos_edge import Unit`, for a driver's annotations (units.py). Lazy like `run`,
|
|
18
|
+
# for the opposite reason: units.py imports nothing, and a driver module importing it
|
|
19
|
+
# must not drag the server in.
|
|
20
|
+
from .units import Unit
|
|
21
|
+
return Unit
|
|
22
|
+
raise AttributeError(f"module 'ivoryos_edge' has no attribute {name!r}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""The agent tool layer: what an assistant is allowed to know and to ask for.
|
|
2
|
+
|
|
3
|
+
One layer, two surfaces. `routes.py` publishes it over HTTP under /api/agent; the MCP server
|
|
4
|
+
(`ivoryos_edge.agent.mcp_server`) is a thin stdio process that calls those same endpoints, and
|
|
5
|
+
the Designer's chat panel calls them from the browser. Adding a third surface, or swapping which
|
|
6
|
+
model is driving, should never mean reimplementing what a tool *does* — which is the whole
|
|
7
|
+
reason this is a module and not a pile of route handlers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ivoryos_edge.agent.deck import describe_deck, describe_method
|
|
11
|
+
from ivoryos_edge.agent.validate import validate_body, summarise
|
|
12
|
+
|
|
13
|
+
__all__ = ["describe_deck", "describe_method", "validate_body", "summarise"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Answering questions about this lab's runs and deck, from its own records.
|
|
2
|
+
|
|
3
|
+
Small local models are unreliable at a tool-calling protocol and fine at emitting one JSON object
|
|
4
|
+
(see providers.py), so tools are offered the same way workflows are written: each reply is one
|
|
5
|
+
JSON object, either a tool to run or the answer. The tool runs here, its result goes back as the
|
|
6
|
+
next message, and after a few rounds the model must answer with what it has.
|
|
7
|
+
|
|
8
|
+
Read-only by construction: the tools are history.search / read / compare and nothing else. An
|
|
9
|
+
answer that should change something (a workflow, a run, a safety rule) is the other modes' job,
|
|
10
|
+
which file proposals for a person to accept.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from ivoryos_edge.agent import history
|
|
17
|
+
from ivoryos_edge.agent.providers import extract_json_object
|
|
18
|
+
|
|
19
|
+
MAX_TOOL_CALLS = 5
|
|
20
|
+
MAX_RESULT_CHARS = 12000
|
|
21
|
+
|
|
22
|
+
SYSTEM_PROMPT = """You answer questions about this lab's experiments, using its own records.
|
|
23
|
+
|
|
24
|
+
You can look things up. Reply with exactly one JSON object and nothing else, either a lookup:
|
|
25
|
+
|
|
26
|
+
{"tool": "search_runs", "args": {"q": "<words in the run's name, columns or instruments>", "status": "<completed, error, cancelled or all>", "limit": 20}}
|
|
27
|
+
{"tool": "run_table", "args": {"run_id": <id>}}
|
|
28
|
+
{"tool": "compare_runs", "args": {"run_ids": [<id>, <id>], "columns": ["<column>"]}}
|
|
29
|
+
|
|
30
|
+
or, when you can answer:
|
|
31
|
+
|
|
32
|
+
{"answer": "<your answer, in plain language; short markdown lists and tables are fine>", "runs": [<ids of the runs your answer is about>]}
|
|
33
|
+
|
|
34
|
+
How to work:
|
|
35
|
+
- run_table gives one run's columns, rows and per-column numbers (n, min, max, mean, and the best
|
|
36
|
+
row for an optimization objective). compare_runs computes the same across runs. Use those
|
|
37
|
+
numbers; do not compute statistics yourself from rows.
|
|
38
|
+
- Every number in your answer must come from a lookup result in this conversation. If the records
|
|
39
|
+
do not hold what was asked, say so plainly and say what they do hold.
|
|
40
|
+
- Name runs as "<name> (#<id>)" so the person can open them.
|
|
41
|
+
- A column ending in "(objective)" is what an optimization was trying to improve.
|
|
42
|
+
- Prefer one or two lookups. You may make at most %d.""" % MAX_TOOL_CALLS
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _clip_json(value: Any) -> str:
|
|
46
|
+
text = json.dumps(value, default=str)
|
|
47
|
+
if len(text) > MAX_RESULT_CHARS:
|
|
48
|
+
text = text[:MAX_RESULT_CHARS] + ' ... (cut: ask for fewer rows or one run at a time)'
|
|
49
|
+
return text
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _run_tool(queue_manager, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
53
|
+
if name == "search_runs":
|
|
54
|
+
return await history.search(queue_manager, q=str(args.get("q") or ""), status=str(args.get("status") or "all"),
|
|
55
|
+
limit=args.get("limit") or 20, offset=args.get("offset") or 0,
|
|
56
|
+
sort=str(args.get("sort") or "newest"))
|
|
57
|
+
if name == "run_table":
|
|
58
|
+
table = await history.read(queue_manager, args.get("run_id"))
|
|
59
|
+
return table or {"error": f"No run #{args.get('run_id')}."}
|
|
60
|
+
if name == "compare_runs":
|
|
61
|
+
columns = args.get("columns")
|
|
62
|
+
return await history.compare(queue_manager, args.get("run_ids") or [],
|
|
63
|
+
[str(c) for c in columns] if isinstance(columns, list) and columns else None)
|
|
64
|
+
return {"error": f"There is no tool called '{name}'. Use search_runs, run_table or compare_runs."}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def answer(provider, queue_manager, question: str, history_turns: Optional[List[dict]] = None,
|
|
68
|
+
page_context: str = "", deck_names: Optional[List[str]] = None,
|
|
69
|
+
on_progress: Optional[Callable[[dict], Awaitable[None]]] = None):
|
|
70
|
+
"""Run the look-up-then-answer loop. Returns (result, transcript); result is
|
|
71
|
+
{answer, runs, lookups: [{tool, args}], ok}."""
|
|
72
|
+
async def report(**event):
|
|
73
|
+
if on_progress is not None:
|
|
74
|
+
await on_progress(event)
|
|
75
|
+
|
|
76
|
+
recent = await history.search(queue_manager, limit=15)
|
|
77
|
+
context = ["The 15 most recent runs on this deck (newest first):\n" + _clip_json(recent)]
|
|
78
|
+
if deck_names:
|
|
79
|
+
context.append("Instruments on this deck: " + ", ".join(deck_names))
|
|
80
|
+
if page_context:
|
|
81
|
+
context.append("Where the person is in the app right now: " + page_context)
|
|
82
|
+
messages: List[dict] = [
|
|
83
|
+
{"role": "user", "content": "\n\n".join(context)},
|
|
84
|
+
{"role": "assistant", "content": "Understood. I will reply with one JSON object: a lookup or the answer."},
|
|
85
|
+
]
|
|
86
|
+
for turn in history_turns or []:
|
|
87
|
+
if turn.get("role") in ("user", "assistant") and turn.get("content"):
|
|
88
|
+
messages.append({"role": turn["role"], "content": str(turn["content"])[:8000]})
|
|
89
|
+
messages.append({"role": "user", "content": question})
|
|
90
|
+
|
|
91
|
+
lookups: List[dict] = []
|
|
92
|
+
transcript: List[dict] = []
|
|
93
|
+
await report(phase="thinking")
|
|
94
|
+
for turn in range(MAX_TOOL_CALLS + 2):
|
|
95
|
+
raw = await provider.complete(SYSTEM_PROMPT, messages, json_mode=True)
|
|
96
|
+
transcript.append({"turn": turn, "raw": raw})
|
|
97
|
+
try:
|
|
98
|
+
reply = extract_json_object(raw)
|
|
99
|
+
except ValueError:
|
|
100
|
+
messages += [{"role": "assistant", "content": raw[:4000]},
|
|
101
|
+
{"role": "user", "content": "That was not one JSON object. Reply with a lookup or an answer, as JSON only."}]
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
if reply.get("answer") is not None:
|
|
105
|
+
runs = [r for r in (reply.get("runs") or []) if isinstance(r, int)]
|
|
106
|
+
await report(phase="answered")
|
|
107
|
+
return {"answer": str(reply["answer"]).strip(), "runs": runs, "lookups": lookups, "ok": True}, transcript
|
|
108
|
+
|
|
109
|
+
tool = str(reply.get("tool") or "")
|
|
110
|
+
if not tool:
|
|
111
|
+
messages += [{"role": "assistant", "content": raw[:4000]},
|
|
112
|
+
{"role": "user", "content": 'Reply with {"tool": ..., "args": {...}} or {"answer": ..., "runs": [...]}.'}]
|
|
113
|
+
continue
|
|
114
|
+
if len(lookups) >= MAX_TOOL_CALLS:
|
|
115
|
+
messages += [{"role": "assistant", "content": raw[:4000]},
|
|
116
|
+
{"role": "user", "content": "No more lookups. Answer now with what you have, and say what is missing."}]
|
|
117
|
+
continue
|
|
118
|
+
args = reply.get("args") if isinstance(reply.get("args"), dict) else {}
|
|
119
|
+
lookups.append({"tool": tool, "args": args})
|
|
120
|
+
await report(phase="looking_up", tool=tool, args=args)
|
|
121
|
+
try:
|
|
122
|
+
result = await _run_tool(queue_manager, tool, args)
|
|
123
|
+
except Exception as e: # a bad argument from the model is its to fix, not a failure here
|
|
124
|
+
result = {"error": str(e)}
|
|
125
|
+
messages += [{"role": "assistant", "content": json.dumps(reply)[:4000]},
|
|
126
|
+
{"role": "user", "content": f"Result of {tool}:\n{_clip_json(result)}"}]
|
|
127
|
+
|
|
128
|
+
await report(phase="gave_up")
|
|
129
|
+
return {"answer": "I could not get to an answer from the records. Try asking about one run, or name the runs to compare.",
|
|
130
|
+
"runs": [], "lookups": lookups, "ok": False}, transcript
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Turning a paragraph of protocol into a reviewable workflow.
|
|
2
|
+
|
|
3
|
+
The loop is: describe the deck, ask for one JSON workflow body, validate it against that same
|
|
4
|
+
deck, and hand the errors back to the model to fix — up to a small number of attempts. The
|
|
5
|
+
retry is what makes this usable with a local model: a 8B model asked cold gets an argument name
|
|
6
|
+
or a unit wrong perhaps half the time, and gets it right when told precisely what was wrong,
|
|
7
|
+
because `validate_body`'s messages name the offending field and list the legal alternatives.
|
|
8
|
+
|
|
9
|
+
What comes out is never applied. It is filed as a proposal for the scientist to accept, so the
|
|
10
|
+
worst case of a bad translation is a rejected diff rather than a wrong reaction.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
from ivoryos_edge.agent.deck import describe_deck
|
|
16
|
+
from ivoryos_edge.agent.providers import ProviderError, extract_json_object
|
|
17
|
+
from ivoryos_edge.agent.validate import validate_body, summarise
|
|
18
|
+
|
|
19
|
+
MAX_ATTEMPTS = 3
|
|
20
|
+
|
|
21
|
+
SYSTEM_PROMPT = """You translate laboratory protocols into IvoryOS workflows.
|
|
22
|
+
|
|
23
|
+
You are given the exact set of instruments and methods available on this lab's deck. You must
|
|
24
|
+
use only those. Never invent an instrument, a method or an argument name: if the protocol asks
|
|
25
|
+
for something the deck cannot do, say so in your summary and leave that step out rather than
|
|
26
|
+
approximating it with a method that does something else.
|
|
27
|
+
|
|
28
|
+
Reply with a single JSON object and nothing else:
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
"summary": "<plain language: what this does, and for an edit, what you changed and why>",
|
|
32
|
+
"body": {
|
|
33
|
+
"name": "<short name>",
|
|
34
|
+
"description": "<one line>",
|
|
35
|
+
"prep": [ ...steps... ],
|
|
36
|
+
"script": [ ...steps... ],
|
|
37
|
+
"cleanup": [ ...steps... ]
|
|
38
|
+
},
|
|
39
|
+
"questions": ["<anything the protocol left ambiguous that a person must decide>"]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
A step is {"instrument": "<name>", "action": "<method>", "args": {"<arg>": <value>}}.
|
|
43
|
+
|
|
44
|
+
Rules that matter:
|
|
45
|
+
- Numbers are bare numbers. Write 65, never "65 C" or "65 degrees". A parameter's `unit` says
|
|
46
|
+
what the number is in: a volume whose unit is mL takes 2.5 for 2.5 mL, never 2500.
|
|
47
|
+
- `prep` runs once at the start, `script` is the part repeated per sample or per optimization
|
|
48
|
+
trial, and `cleanup` runs once at the end. If the protocol does not distinguish them, put
|
|
49
|
+
everything in `script`.
|
|
50
|
+
- To save a result for a later step or as an optimization objective, add to that step:
|
|
51
|
+
"return": "name", "return_bindings": [{"path": "<a save_from value from the deck>", "var": "name"}].
|
|
52
|
+
Use the exact `save_from` strings the deck lists. Only numeric results can be optimized.
|
|
53
|
+
- To use a saved value later, write it as a string "#name" in an argument.
|
|
54
|
+
- A value the protocol leaves to the operator should be "#name" and listed in `questions`,
|
|
55
|
+
or read with a Flow_Control User_Input step.
|
|
56
|
+
- To compute a value from earlier ones (a counter, a dilution), use a Flow_Control Set_Variable
|
|
57
|
+
step: "args": {"variable_name": "dilution", "value": "volume / 2"}, earlier variables by bare
|
|
58
|
+
name in "value", then "#dilution" in later arguments.
|
|
59
|
+
- Every Flow_Control "If" needs a matching "End_If", and every "While" a matching "End_While".
|
|
60
|
+
- Prefer fewer, correct steps. Do not invent safety, washing or calibration steps the protocol
|
|
61
|
+
does not mention.
|
|
62
|
+
|
|
63
|
+
If you are editing an existing workflow, keep every step the scientist did not ask you to
|
|
64
|
+
change, exactly as it was."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _deck_prompt(schema, workflows):
|
|
68
|
+
return (
|
|
69
|
+
"This lab's deck, as JSON. These are the only instruments and methods that exist:\n\n"
|
|
70
|
+
+ json.dumps(describe_deck(schema, workflows=workflows), indent=1)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _existing_prompt(name, body):
|
|
75
|
+
return (
|
|
76
|
+
f"You are editing the saved workflow '{name}'. Its current body is:\n\n"
|
|
77
|
+
+ json.dumps(body, indent=1)
|
|
78
|
+
+ "\n\nReturn the complete new body, not a patch."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _retry_prompt(issues):
|
|
83
|
+
errors = [i for i in issues if i["severity"] == "error"]
|
|
84
|
+
lines = []
|
|
85
|
+
for issue in errors:
|
|
86
|
+
line = f"- {issue['where']}: {issue['message']}"
|
|
87
|
+
if issue.get("hint"):
|
|
88
|
+
line += f" ({issue['hint']})"
|
|
89
|
+
lines.append(line)
|
|
90
|
+
return (
|
|
91
|
+
"That workflow does not validate against the deck. Fix exactly these problems and "
|
|
92
|
+
"return the corrected JSON object:\n\n" + "\n".join(lines)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def translate(provider, schema, user_message, history=None, workflows=(),
|
|
97
|
+
existing_name=None, existing_body=None, max_attempts=MAX_ATTEMPTS,
|
|
98
|
+
on_progress=None, resolve_workflow=None, check_fields=None):
|
|
99
|
+
"""Run the translate-validate-correct loop.
|
|
100
|
+
|
|
101
|
+
Returns (result, transcript) where result is {summary, body, questions, issues, ok,
|
|
102
|
+
attempts} and transcript is the raw exchange, kept so a failure can be shown to the
|
|
103
|
+
scientist rather than swallowed.
|
|
104
|
+
|
|
105
|
+
`on_progress` is awaited with a dict at each phase. The interesting part of this loop is not
|
|
106
|
+
the tokens, it is the check-and-correct cycle — "wrote 9 steps, two were wrong, fixing" is
|
|
107
|
+
what tells a waiting scientist the thing is working and roughly how well. A local model
|
|
108
|
+
takes tens of seconds per attempt, which is far too long to show nothing.
|
|
109
|
+
"""
|
|
110
|
+
async def report(**event):
|
|
111
|
+
if on_progress is not None:
|
|
112
|
+
await on_progress(event)
|
|
113
|
+
messages = []
|
|
114
|
+
context = [_deck_prompt(schema, workflows)]
|
|
115
|
+
if existing_body:
|
|
116
|
+
context.append(_existing_prompt(existing_name, existing_body))
|
|
117
|
+
messages.append({"role": "user", "content": "\n\n".join(context)})
|
|
118
|
+
messages.append({
|
|
119
|
+
"role": "assistant",
|
|
120
|
+
"content": "Understood. I will use only those instruments and methods, and reply with a single JSON object.",
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
for turn in (history or []):
|
|
124
|
+
role = turn.get("role")
|
|
125
|
+
content = turn.get("content")
|
|
126
|
+
if role in ("user", "assistant") and content:
|
|
127
|
+
messages.append({"role": role, "content": str(content)[:20000]})
|
|
128
|
+
|
|
129
|
+
messages.append({"role": "user", "content": user_message})
|
|
130
|
+
|
|
131
|
+
await report(phase="reading_deck", instruments=len(schema),
|
|
132
|
+
editing=existing_name or None)
|
|
133
|
+
|
|
134
|
+
transcript = []
|
|
135
|
+
last_error = None
|
|
136
|
+
for attempt in range(1, max_attempts + 1):
|
|
137
|
+
await report(phase="drafting", attempt=attempt, max_attempts=max_attempts)
|
|
138
|
+
raw = await provider.complete(SYSTEM_PROMPT, messages, json_mode=True)
|
|
139
|
+
transcript.append({"attempt": attempt, "raw": raw})
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
parsed = extract_json_object(raw)
|
|
143
|
+
except ValueError as e:
|
|
144
|
+
last_error = str(e)
|
|
145
|
+
await report(phase="unreadable", attempt=attempt, detail=str(e))
|
|
146
|
+
messages.append({"role": "assistant", "content": raw[:4000]})
|
|
147
|
+
messages.append({
|
|
148
|
+
"role": "user",
|
|
149
|
+
"content": "That was not valid JSON. Reply with a single JSON object and nothing else.",
|
|
150
|
+
})
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
body = parsed.get("body")
|
|
154
|
+
if not isinstance(body, dict):
|
|
155
|
+
last_error = "The reply had no 'body' object."
|
|
156
|
+
await report(phase="unreadable", attempt=attempt, detail=last_error)
|
|
157
|
+
messages.append({"role": "assistant", "content": raw[:4000]})
|
|
158
|
+
messages.append({
|
|
159
|
+
"role": "user",
|
|
160
|
+
"content": "The JSON object must contain a 'body' with prep/script/cleanup arrays.",
|
|
161
|
+
})
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
body.setdefault("name", existing_name or parsed.get("name") or "Untitled protocol")
|
|
165
|
+
for phase in ("prep", "script", "cleanup"):
|
|
166
|
+
body.setdefault(phase, [])
|
|
167
|
+
|
|
168
|
+
step_count = sum(len(body.get(phase) or []) for phase in ("prep", "script", "cleanup"))
|
|
169
|
+
await report(phase="validating", attempt=attempt, steps=step_count,
|
|
170
|
+
name=body.get("name"))
|
|
171
|
+
|
|
172
|
+
issues = validate_body(body, schema, workflows, resolve_workflow, check_fields)
|
|
173
|
+
errors = [i for i in issues if i["severity"] == "error"]
|
|
174
|
+
result = {
|
|
175
|
+
"summary": str(parsed.get("summary") or "").strip(),
|
|
176
|
+
"body": body,
|
|
177
|
+
"questions": [str(q) for q in (parsed.get("questions") or [])][:10],
|
|
178
|
+
"issues": issues,
|
|
179
|
+
"validation_summary": summarise(issues),
|
|
180
|
+
"ok": not errors,
|
|
181
|
+
"attempts": attempt,
|
|
182
|
+
}
|
|
183
|
+
if not errors:
|
|
184
|
+
await report(phase="valid", attempt=attempt, steps=step_count)
|
|
185
|
+
return result, transcript
|
|
186
|
+
|
|
187
|
+
await report(phase="found_problems", attempt=attempt,
|
|
188
|
+
errors=[f"{i['where']}: {i['message']}" for i in errors])
|
|
189
|
+
|
|
190
|
+
if attempt == max_attempts:
|
|
191
|
+
await report(phase="gave_up", attempt=attempt, remaining=len(errors))
|
|
192
|
+
# Out of attempts, but the draft still goes back: a workflow with two bad arguments
|
|
193
|
+
# and the errors named against it is a far better starting point for the scientist
|
|
194
|
+
# than an apology, and the Designer shows the issues inline.
|
|
195
|
+
return result, transcript
|
|
196
|
+
|
|
197
|
+
messages.append({"role": "assistant", "content": json.dumps(parsed)[:8000]})
|
|
198
|
+
messages.append({"role": "user", "content": _retry_prompt(issues)})
|
|
199
|
+
|
|
200
|
+
raise ProviderError(last_error or "The model did not return a usable workflow.")
|