biocognia-intelligence 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ /build/
5
+ /dist/
6
+ /wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.5
2
+ Name: biocognia-intelligence
3
+ Version: 0.1.0
4
+ Summary: Biocognia Intelligence: one AI workspace over the BioCognia products installed, opened with `biocognia`
5
+ Project-URL: Homepage, https://biocognia.com
6
+ Author-email: Ajit Johnson Nirmal <ajitjohnson.n@gmail.com>
7
+ License-Expression: LicenseRef-Proprietary
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
11
+ Requires-Python: >=3.12
12
+ Requires-Dist: biocognia<0.2,>=0.1.2
13
+ Requires-Dist: spatialagents<0.3,>=0.2.2
14
+ Provides-Extra: scimappro
15
+ Requires-Dist: scimappro[ai,bridge,chat]; extra == 'scimappro'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Biocognia Intelligence
19
+
20
+ One AI workspace over the BioCognia products you have installed, opened with one command:
21
+
22
+ ```bash
23
+ pip install "biocognia-intelligence[scimappro]"
24
+ biocognia
25
+ ```
26
+
27
+ `biocognia` opens the Workspace in your browser. Start with your data: a file, a
28
+ folder, or a data location. The product that fits it opens it (or the one you
29
+ choose), and the assistant helps you explore and interpret it. Your workspaces
30
+ from each product are listed together. The products you have installed, such
31
+ as Plexora, can be opened from the home screen in their own windows.
32
+
33
+ | Command | What it does |
34
+ |---|---|
35
+ | `biocognia` | Opens the app. `--no-browser` prints its address instead, `--port` serves elsewhere (default 8480), and `--home ID` sets the product a new workspace belongs to. |
36
+ | `biocognia list` | The products installed. `--licence` adds whether each one's AI licence lets a conversation run, and `--json` prints it all as JSON. |
37
+ | `biocognia open plexora` | Opens a product's own application. |
38
+ | `biocognia license status` | This device's licences, for every product. `activate`, `refresh`, `install` and the other verbs work as in each product's own command line. |
39
+ | `biocognia serve --desktop` | A desktop shell's sidecar: one ready line, then the app until stdin closes. |
40
+
41
+ ## How it is put together
42
+
43
+ - **A workspace belongs to one product**, its home. It is stored where that
44
+ product keeps its sessions, so the product's own app (`scimappro`) lists and
45
+ opens the same workspaces.
46
+ - **The home product pays.** Its AI licence pays for the conversation, through
47
+ that product's own gateway. There is no separate Intelligence licence: AI
48
+ comes with each product's licence.
49
+ - **The runtime is shared.** The conversation, plans, approvals and the page are
50
+ [spatialagents](https://pypi.org/project/spatialagents/). Licences and the
51
+ AI token come from [biocognia](https://pypi.org/project/biocognia/).
52
+ - **Products register themselves.** A product joins by registering one object
53
+ under the `biocognia.products` entry point, from a package small enough to
54
+ import in a moment. The contract is in `src/biocognia_intelligence/registry.py`.
55
+
56
+ ```toml
57
+ [project.entry-points."biocognia.products"]
58
+ myproduct = "myproduct_intelligence:PRODUCT"
59
+ ```
@@ -0,0 +1,42 @@
1
+ # Biocognia Intelligence
2
+
3
+ One AI workspace over the BioCognia products you have installed, opened with one command:
4
+
5
+ ```bash
6
+ pip install "biocognia-intelligence[scimappro]"
7
+ biocognia
8
+ ```
9
+
10
+ `biocognia` opens the Workspace in your browser. Start with your data: a file, a
11
+ folder, or a data location. The product that fits it opens it (or the one you
12
+ choose), and the assistant helps you explore and interpret it. Your workspaces
13
+ from each product are listed together. The products you have installed, such
14
+ as Plexora, can be opened from the home screen in their own windows.
15
+
16
+ | Command | What it does |
17
+ |---|---|
18
+ | `biocognia` | Opens the app. `--no-browser` prints its address instead, `--port` serves elsewhere (default 8480), and `--home ID` sets the product a new workspace belongs to. |
19
+ | `biocognia list` | The products installed. `--licence` adds whether each one's AI licence lets a conversation run, and `--json` prints it all as JSON. |
20
+ | `biocognia open plexora` | Opens a product's own application. |
21
+ | `biocognia license status` | This device's licences, for every product. `activate`, `refresh`, `install` and the other verbs work as in each product's own command line. |
22
+ | `biocognia serve --desktop` | A desktop shell's sidecar: one ready line, then the app until stdin closes. |
23
+
24
+ ## How it is put together
25
+
26
+ - **A workspace belongs to one product**, its home. It is stored where that
27
+ product keeps its sessions, so the product's own app (`scimappro`) lists and
28
+ opens the same workspaces.
29
+ - **The home product pays.** Its AI licence pays for the conversation, through
30
+ that product's own gateway. There is no separate Intelligence licence: AI
31
+ comes with each product's licence.
32
+ - **The runtime is shared.** The conversation, plans, approvals and the page are
33
+ [spatialagents](https://pypi.org/project/spatialagents/). Licences and the
34
+ AI token come from [biocognia](https://pypi.org/project/biocognia/).
35
+ - **Products register themselves.** A product joins by registering one object
36
+ under the `biocognia.products` entry point, from a package small enough to
37
+ import in a moment. The contract is in `src/biocognia_intelligence/registry.py`.
38
+
39
+ ```toml
40
+ [project.entry-points."biocognia.products"]
41
+ myproduct = "myproduct_intelligence:PRODUCT"
42
+ ```
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "biocognia-intelligence"
3
+ version = "0.1.0"
4
+ description = "Biocognia Intelligence: one AI workspace over the BioCognia products installed, opened with `biocognia`"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Ajit Johnson Nirmal", email = "ajitjohnson.n@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ license = "LicenseRef-Proprietary"
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Intended Audience :: Science/Research",
14
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
15
+ ]
16
+ # The application, not a library: the licensing client and the runtime are the
17
+ # shared layer, and each product joins by registering under the
18
+ # `biocognia.products` entry point (registry.py). `biocognia --help` and
19
+ # `biocognia list` load neither: every subcommand imports what it needs.
20
+ dependencies = [
21
+ "biocognia>=0.1.2,<0.2",
22
+ "spatialagents>=0.2.2,<0.3",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ scimappro = ["scimappro[ai,chat,bridge]"]
27
+
28
+ [project.scripts]
29
+ biocognia = "biocognia_intelligence.cli:main"
30
+
31
+ [project.urls]
32
+ Homepage = "https://biocognia.com"
33
+
34
+ [build-system]
35
+ requires = ["hatchling"]
36
+ build-backend = "hatchling.build"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/biocognia_intelligence"]
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "pytest>=9.1.1",
44
+ "pytest-timeout>=2.3",
45
+ ]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+ timeout = 120
@@ -0,0 +1,10 @@
1
+ """Biocognia Intelligence: one AI workspace over the BioCognia products installed.
2
+
3
+ `biocognia` opens it (cli.py): spatialagents' Workspace page, where a
4
+ workspace belongs to one product -- its home, whose AI licence pays for the
5
+ conversation -- and the home screen lists the person's applications.
6
+ Products join by registering under the `biocognia.products` entry point
7
+ (registry.py). Importing this package loads nothing.
8
+ """
9
+
10
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ """`python -m biocognia_intelligence`: what a desktop shell spawns."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,98 @@
1
+ """The app's HTTP contract: spatialagents' Workspace (`WorkspaceApi`), and the
2
+ person's applications beside it.
3
+
4
+ GET /apps each installed product: {id, name, brand, version, ai, can_open, home, note?}
5
+ POST /apps/:id/open start its own application, in its own window
6
+
7
+ Everything else is the Workspace's.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ import subprocess
14
+ import sys
15
+
16
+ _APPS = re.compile(r"^/apps(?:/([^/]+)/(open))?$")
17
+
18
+
19
+ class IntelligenceApi:
20
+ """`inner` is the `WorkspaceApi`; `products` the registered products and
21
+ `stale` the family's installed too old to register (registry.py);
22
+ `start(argv, product_id) -> pid` starts an application (tests pass their own)."""
23
+
24
+ def __init__(self, inner, products, *, stale=(), start=None):
25
+ self.inner = inner
26
+ self.products = {product.id: product for product in products}
27
+ self.stale = {row["id"]: row for row in stale}
28
+ self.start = start or _start
29
+
30
+ def __getattr__(self, name):
31
+ # What the server asks of the Workspace's API (whether it runs plans) is the Workspace's.
32
+ return getattr(self.inner, name)
33
+
34
+ def handle(self, method: str, path: str, *, query=None, body=None, headers=None):
35
+ found = _APPS.match("/" + str(path).strip("/"))
36
+ if not found:
37
+ return self.inner.handle(method, path, query=query, body=body, headers=headers)
38
+ from spatialagents.chat.http import missing, ok, refused
39
+
40
+ method = method.upper()
41
+ if found.group(1) is None and method == "GET":
42
+ return ok({"apps": self.apps()})
43
+ if found.group(1) is not None and method == "POST":
44
+ product_id = found.group(1)
45
+ argv = self._argv(product_id)
46
+ if argv is None:
47
+ return refused({"code": "not_found" if product_id not in self.products else "capability_unavailable",
48
+ "message": f"{product_id!r} has no application of its own here",
49
+ "detail": None, "retryable": False})
50
+ name = self._name(product_id)
51
+ try:
52
+ pid = self.start(argv, product_id)
53
+ except OSError as problem:
54
+ return refused({"code": "resource_unavailable", "message": f"{name} would not start: {problem}",
55
+ "detail": None, "retryable": True})
56
+ return ok({"started": True, "pid": pid, "message": f"{name} is opening in its own window"})
57
+ return missing(f"no route {method} {path}")
58
+
59
+ def apps(self) -> list:
60
+ from .registry import version_of
61
+ from .routing import _ai
62
+
63
+ rows = []
64
+ for product in self.products.values():
65
+ rows.append({"id": product.id, "name": product.name, "brand": product.brand,
66
+ "version": version_of(product.distribution), "ai": _ai(product),
67
+ "can_open": self._argv(product.id) is not None,
68
+ "home": callable(getattr(product, "home", None))})
69
+ for row in self.stale.values():
70
+ rows.append({"id": row["id"], "name": row["name"], "brand": row["brand"], "version": row["version"],
71
+ "ai": None, "can_open": True, "home": False,
72
+ "note": f"opens in its own window; from {row['registers_from']} its workspaces open here too"})
73
+ return rows
74
+
75
+ def _name(self, product_id: str) -> str:
76
+ product = self.products.get(product_id)
77
+ return product.name if product is not None else self.stale[product_id]["name"]
78
+
79
+ def _argv(self, product_id: str):
80
+ product = self.products.get(product_id)
81
+ if product is not None:
82
+ argv = product.app()
83
+ return list(argv) if argv else None
84
+ if product_id in self.stale:
85
+ return [sys.executable, *self.stale[product_id]["app"]]
86
+ return None
87
+
88
+
89
+ def _start(argv, product_id) -> int:
90
+ """Start an application on its own, its output in the app's logs."""
91
+ from .paths import app_dir
92
+
93
+ logs = app_dir() / "logs"
94
+ logs.mkdir(parents=True, exist_ok=True)
95
+ with open(logs / f"{product_id}.log", "ab") as log:
96
+ started = subprocess.Popen(list(argv), stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT,
97
+ start_new_session=True)
98
+ return started.pid
@@ -0,0 +1,242 @@
1
+ """The app: spatialagents' Workspace page over every product that can hold a
2
+ workspace, and the person's applications on its home screen.
3
+
4
+ `server()` builds it (not started); `launch` is a bare `biocognia`, which
5
+ opens it in a browser where this looks like a desktop; `serve` is the same
6
+ without the browser, or with `--desktop` the desktop shell's sidecar (one
7
+ ready line, then until stdin closes). Like each product's own app it opens
8
+ without an AI licence: the page says so, and model calls in a workspace whose
9
+ product has none are refused by that product's gateway.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+
16
+ from . import __version__, registry
17
+ from .paths import DESKTOP_PORT, LAUNCH_PORT, app_dir
18
+
19
+ log = logging.getLogger("biocognia_intelligence.app")
20
+
21
+ TITLE = "Biocognia Intelligence"
22
+ TAGLINE = ("Choose a file, folder, or data location. The product that fits it opens it, "
23
+ "and AI helps you explore and interpret your data.")
24
+
25
+ #: Set where nobody is sitting at this machine's screen: a cluster job, CI, an
26
+ #: SSH login (the products' list).
27
+ HEADLESS = ("CI", "SLURM_JOB_ID", "PBS_JOBID", "LSB_JOBID", "SSH_CONNECTION", "SSH_CLIENT")
28
+
29
+
30
+ class NoHome(RuntimeError):
31
+ """No installed product can hold a workspace."""
32
+
33
+
34
+ def homes_of(products) -> tuple[list, list]:
35
+ """`([(product, home, opener)], [problem sentences])`: each product that
36
+ can hold a workspace, its Home and the opener it built."""
37
+ homes, problems = [], []
38
+ for product in products:
39
+ try:
40
+ home = product.home()
41
+ if home is None:
42
+ continue
43
+ homes.append((product, home, home.opener()))
44
+ except Exception as problem: # noqa: BLE001 -- the others still open
45
+ log.warning("%s cannot hold workspaces here", product.name, exc_info=True)
46
+ problems.append(f"{product.name} cannot hold workspaces here: {problem}")
47
+ return homes, problems
48
+
49
+
50
+ def server(*, port=0, host="127.0.0.1", token=None, default=None, require_token=False, frame_ancestors=None,
51
+ products=None, stale=None, start=None, on_open=None):
52
+ """A `ChatServer` for the app (not started). `products` and `stale` are
53
+ the registry's unless given; its `workspaces` attribute is the
54
+ `WorkspaceService` (close it on the way out), `problems` what could not
55
+ be opened."""
56
+ from spatialagents.chat.server import ChatServer
57
+ from spatialagents.workspace import static_dir
58
+ from spatialagents.workspace.http import WorkspaceApi
59
+ from spatialagents.workspace.service import WorkspaceService
60
+
61
+ from .api import IntelligenceApi
62
+ from .routing import RoutingOpener
63
+
64
+ if products is None:
65
+ products = registry.products()
66
+ stale = registry.too_old() if stale is None else stale
67
+ homes, problems = homes_of(products)
68
+ if not homes:
69
+ raise NoHome("no BioCognia product that holds workspaces is installed" +
70
+ (f" ({'; '.join(problems)})" if problems else ""))
71
+ routing = RoutingOpener(homes, default=default, on_open=on_open)
72
+ service = WorkspaceService(routing)
73
+ api = IntelligenceApi(WorkspaceApi(service), products, stale=stale or (), start=start)
74
+ page = {"assistant": TITLE, "product": TITLE, "brand": "biocognia", "version": __version__,
75
+ "tagline": TAGLINE, "products": routing.homes(), "apps": True}
76
+ found = ChatServer(api, host=host, port=int(port or 0), token=token, title=TITLE, page=page,
77
+ page_dir=static_dir(), frame_ancestors=tuple(frame_ancestors or ("'self'",)),
78
+ require_token=require_token)
79
+ found.workspaces = service
80
+ found.problems = problems
81
+ found.homes = [product.id for product, _, _ in homes]
82
+ return found
83
+
84
+
85
+ def wants_browser(preference="auto", env=None, system=None) -> bool:
86
+ """Whether a bare `biocognia` opens a browser: always for `yes`, never for
87
+ `no`, and for `auto` on a desktop -- macOS, Windows, or Linux with a
88
+ display -- but not on a cluster node, in CI or over SSH."""
89
+ import os
90
+ import platform
91
+
92
+ if preference in ("yes", "no"):
93
+ return preference == "yes"
94
+ env = os.environ if env is None else env
95
+ system = platform.system() if system is None else system
96
+ if any(env.get(name) for name in HEADLESS):
97
+ return False
98
+ if system in ("Darwin", "Windows"):
99
+ return True
100
+ return bool(env.get("DISPLAY") or env.get("WAYLAND_DISPLAY"))
101
+
102
+
103
+ def _bound(build, asked, fallback_port, error, **options):
104
+ """`build(port=...)` on the port asked for, else the usual one, else any."""
105
+ try:
106
+ return build(port=asked or fallback_port, **options)
107
+ except OSError:
108
+ if asked:
109
+ error(f"biocognia: port {asked} is in use; choose another with --port, or leave it out")
110
+ return None
111
+ return build(port=0, **options)
112
+
113
+
114
+ def _say_licences(found, error) -> None:
115
+ for problem in found.problems:
116
+ error(f"biocognia: {problem}")
117
+ for home in found.workspaces.opener.homes():
118
+ if not home["ai"]["ok"]:
119
+ error(f"biocognia: {home['name']}: {home['ai']['reason']}")
120
+ error(f" {home['name']} workspaces open; the assistant stays off in them until this device has "
121
+ f"its licence.")
122
+
123
+
124
+ def launch(args, *, write, error, open_url=None) -> int:
125
+ """`biocognia` alone: the app on its home screen, in a browser where this
126
+ looks like a desktop, until Ctrl-C."""
127
+ try:
128
+ found = _bound(server, getattr(args, "port", 0) or 0, LAUNCH_PORT, error,
129
+ default=getattr(args, "home", None))
130
+ except (NoHome, ValueError) as problem:
131
+ return _no_home(problem, error)
132
+ if found is None:
133
+ return 1
134
+ _say_licences(found, error)
135
+ write(TITLE)
136
+ write(f"Open {found.launch_url}")
137
+ write("The link works once; press Ctrl-C to stop.")
138
+ if wants_browser(getattr(args, "browser", "auto")):
139
+ if open_url is None:
140
+ import webbrowser
141
+
142
+ open_url = webbrowser.open
143
+ open_url(found.launch_url)
144
+ return _serve_until_stopped(found)
145
+
146
+
147
+ def serve(args, *, write, error, read=None) -> int:
148
+ """`biocognia serve`: the app without a browser; with `--desktop`, the
149
+ desktop shell's sidecar."""
150
+ if getattr(args, "desktop", False):
151
+ return desktop(args, write=write, error=error, read=read)
152
+ try:
153
+ found = _bound(server, getattr(args, "port", 0) or 0, LAUNCH_PORT, error,
154
+ host=getattr(args, "host", None) or "127.0.0.1", default=getattr(args, "home", None))
155
+ except (NoHome, ValueError) as problem:
156
+ return _no_home(problem, error)
157
+ if found is None:
158
+ return 1
159
+ _say_licences(found, error)
160
+ write(f"{TITLE} at {found.launch_url}")
161
+ return _serve_until_stopped(found)
162
+
163
+
164
+ def ready_line(found, *, token: str) -> str:
165
+ """The one line a desktop shell reads (spatialagents' `ready_line`)."""
166
+ from spatialagents.desktop import ready_line as line
167
+
168
+ host, port = found.address
169
+ return line(host=host, port=port, token=token, version=__version__, origin=found.url,
170
+ data_root=str(app_dir()), settings_path=str(app_dir() / "desktop.json"),
171
+ app="biocognia-intelligence", homes=list(found.homes))
172
+
173
+
174
+ def desktop(args, *, write, error, read=None) -> int:
175
+ """The desktop shell's sidecar: one ready line, then serve until stdin closes."""
176
+ import os
177
+ import secrets
178
+ import signal
179
+ import sys
180
+ import threading
181
+
182
+ token = os.environ.get("SPATIALAGENTS_CHAT_TOKEN") or secrets.token_urlsafe(16)
183
+ try:
184
+ found = _bound(server, getattr(args, "port", 0) or 0, DESKTOP_PORT, error, token=token,
185
+ default=getattr(args, "home", None))
186
+ except (NoHome, ValueError) as problem:
187
+ return _no_home(problem, error)
188
+ if found is None:
189
+ return 1
190
+ write(ready_line(found, token=token))
191
+ stopped = threading.Event()
192
+
193
+ def stop(*_):
194
+ if not stopped.is_set():
195
+ stopped.set()
196
+ threading.Thread(target=found.stop, daemon=True).start()
197
+
198
+ def watch():
199
+ # The shell keeps our stdin open; when it goes away, so do we.
200
+ reader = read or (lambda: sys.stdin.read(1))
201
+ try:
202
+ while reader():
203
+ pass
204
+ except Exception: # noqa: BLE001 -- a closed stdin is a goodbye
205
+ pass
206
+ stop()
207
+
208
+ threading.Thread(target=watch, name="biocognia-desktop-stdin", daemon=True).start()
209
+ try:
210
+ signal.signal(signal.SIGTERM, stop)
211
+ except ValueError:
212
+ pass # not the main thread (a test)
213
+ try:
214
+ found.serve_forever()
215
+ except KeyboardInterrupt:
216
+ pass
217
+ finally:
218
+ stop()
219
+ found.workspaces.close()
220
+ return 0
221
+
222
+
223
+ def _serve_until_stopped(found) -> int:
224
+ try:
225
+ found.serve_forever()
226
+ except KeyboardInterrupt:
227
+ pass
228
+ finally:
229
+ found.stop()
230
+ found.workspaces.close()
231
+ return 0
232
+
233
+
234
+ def _no_home(problem, error) -> int:
235
+ error(f"biocognia: {problem}")
236
+ stale = registry.too_old()
237
+ for row in stale:
238
+ error(f" {row['name']} {row['version']} is installed but too old to open here; it registers from "
239
+ f"{row['registers_from']}.")
240
+ if not stale:
241
+ error(' Install one: pip install "biocognia-intelligence[scimappro]"')
242
+ return 2
@@ -0,0 +1,140 @@
1
+ """The `biocognia` command.
2
+
3
+ biocognia [--port N] [--browser | --no-browser] [--home ID] open the app
4
+ biocognia list [--json] [--licence] the products installed
5
+ biocognia open PRODUCT [ARGS ...] a product's own application
6
+ biocognia serve [--host H] [--port N] [--home ID] [--desktop]
7
+ biocognia license [--product ID] VERB the shared licence verbs
8
+
9
+ Each subcommand imports what it needs: `--help` and `list` load neither the
10
+ runtime nor a product.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+
19
+
20
+ def build_parser() -> argparse.ArgumentParser:
21
+ parser = argparse.ArgumentParser(
22
+ prog="biocognia", description="Biocognia Intelligence: one AI workspace over the BioCognia products "
23
+ "installed. Alone, it opens in a browser; Ctrl-C stops it.")
24
+ parser.add_argument("--version", action="store_true", help="print the version and stop")
25
+ _serving(parser)
26
+ browser = parser.add_mutually_exclusive_group()
27
+ browser.add_argument("--browser", dest="browser", action="store_const", const="yes", default="auto",
28
+ help="open a browser even where this looks like a cluster, CI or SSH")
29
+ browser.add_argument("--no-browser", dest="browser", action="store_const", const="no",
30
+ help="print the app's address instead of opening it")
31
+ commands = parser.add_subparsers(dest="command")
32
+ listed = commands.add_parser("list", help="the BioCognia products installed")
33
+ listed.add_argument("--json", action="store_true", dest="as_json")
34
+ listed.add_argument("--licence", "--license", action="store_true", dest="licence",
35
+ help="and whether each one's AI licence lets a conversation run (imports each product)")
36
+ opened = commands.add_parser("open", help="a product's own application, in its own window")
37
+ opened.add_argument("product")
38
+ opened.add_argument("args", nargs=argparse.REMAINDER, help="passed to the application")
39
+ served = commands.add_parser("serve", help="the app without a browser (--desktop: a desktop shell's sidecar)")
40
+ _serving(served)
41
+ served.add_argument("--host", help="the address to serve on (default 127.0.0.1)")
42
+ served.add_argument("--desktop", action="store_true", help="print one ready line and stop when stdin closes")
43
+ licence = commands.add_parser("license", help="this device's licences, for every product installed")
44
+ from . import licence as licence_verbs
45
+
46
+ licence_verbs.add_arguments(licence)
47
+ return parser
48
+
49
+
50
+ def _serving(parser) -> None:
51
+ parser.add_argument("--port", type=int, default=0, help="the port to serve on (default 8480, or a free one)")
52
+ parser.add_argument("--home", help="the product a new workspace belongs to unless the page says "
53
+ "(`biocognia list` names them)")
54
+
55
+
56
+ def main(argv=None) -> int:
57
+ args = build_parser().parse_args(argv)
58
+ write = lambda line: print(line, flush=True) # noqa: E731
59
+ error = lambda line: print(line, file=sys.stderr, flush=True) # noqa: E731
60
+ if args.version:
61
+ from . import __version__
62
+
63
+ write(__version__)
64
+ return 0
65
+ if args.command == "list":
66
+ return list_products(args, write=write)
67
+ if args.command == "open":
68
+ return open_product(args, error=error)
69
+ if args.command == "license":
70
+ from . import licence
71
+
72
+ return licence.run(args, out=write, err=error)
73
+ from . import app
74
+
75
+ if args.command == "serve":
76
+ return app.serve(args, write=write, error=error)
77
+ return app.launch(args, write=write, error=error)
78
+
79
+
80
+ def rows(*, licence: bool = False) -> list:
81
+ """The products installed, as `biocognia list` says them."""
82
+ from . import registry
83
+
84
+ found = []
85
+ for product in registry.products():
86
+ row = {"id": product.id, "name": product.name, "version": registry.version_of(product.distribution),
87
+ "home": callable(getattr(product, "home", None)), "app": True}
88
+ if licence:
89
+ from .routing import _ai
90
+
91
+ row["ai"] = _ai(product)
92
+ found.append(row)
93
+ for stale in registry.too_old():
94
+ found.append({"id": stale["id"], "name": stale["name"], "version": stale["version"], "home": False,
95
+ "app": True, "too_old": True, "registers_from": stale["registers_from"]})
96
+ return found
97
+
98
+
99
+ def list_products(args, *, write) -> int:
100
+ found = rows(licence=args.licence)
101
+ if args.as_json:
102
+ write(json.dumps(found, indent=1))
103
+ return 0
104
+ if not found:
105
+ write('No BioCognia product is installed. pip install "biocognia-intelligence[scimappro]"')
106
+ return 0
107
+ width = max(len(f"{row['name']} {row['version'] or ''}") for row in found)
108
+ for row in found:
109
+ name = f"{row['name']} {row['version'] or ''}".ljust(width)
110
+ if row.get("too_old"):
111
+ says = f"too old to open here (registers from {row['registers_from']}); `biocognia open {row['id']}`"
112
+ else:
113
+ says = "workspaces open here" if row["home"] else "its own application"
114
+ if "ai" in row:
115
+ says += " · " + ("AI included" if row["ai"]["ok"] else f"no AI: {row['ai']['reason']}")
116
+ write(f"{name} {says}")
117
+ return 0
118
+
119
+
120
+ def open_product(args, *, error) -> int:
121
+ """A product's own application, in the foreground: this command waits for it."""
122
+ import subprocess
123
+
124
+ from . import registry
125
+
126
+ product = registry.find(args.product)
127
+ if product is not None:
128
+ argv = product.app()
129
+ else:
130
+ stale = next((row for row in registry.too_old() if row["id"] == args.product), None)
131
+ argv = [sys.executable, *stale["app"]] if stale else None
132
+ if not argv:
133
+ known = [row["id"] for row in rows()]
134
+ error(f"biocognia: no application {args.product!r} is installed"
135
+ + (f"; installed: {', '.join(known)}" if known else ""))
136
+ return 2
137
+ try:
138
+ return subprocess.call([*argv, *args.args])
139
+ except KeyboardInterrupt:
140
+ return 130