cfabric-browser 0.1.0__py3-none-any.whl
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.
- cfabric_browser/__init__.py +3 -0
- cfabric_browser/__main__.py +12 -0
- cfabric_browser/app.py +178 -0
- cfabric_browser/assistant/__init__.py +8 -0
- cfabric_browser/assistant/guides/bhsa.md +123 -0
- cfabric_browser/assistant/mcp.py +225 -0
- cfabric_browser/assistant/proposals.py +99 -0
- cfabric_browser/assistant/running.py +69 -0
- cfabric_browser/assistant/stdio.py +276 -0
- cfabric_browser/assistant/tools.py +243 -0
- cfabric_browser/books.py +107 -0
- cfabric_browser/cancel.py +84 -0
- cfabric_browser/cli.py +400 -0
- cfabric_browser/compare.py +644 -0
- cfabric_browser/concordance.py +322 -0
- cfabric_browser/corpora.py +631 -0
- cfabric_browser/dictionaries/__init__.py +1 -0
- cfabric_browser/dictionaries/bdb.py +216 -0
- cfabric_browser/dictionaries/build.py +672 -0
- cfabric_browser/dictionaries/corrections.yaml +43 -0
- cfabric_browser/dictionaries/joins.yaml +24 -0
- cfabric_browser/dictionaries/link.py +254 -0
- cfabric_browser/dictionaries/manage.py +134 -0
- cfabric_browser/dictionaries/sefaria.py +240 -0
- cfabric_browser/dictionaries/store.py +192 -0
- cfabric_browser/display/__init__.py +61 -0
- cfabric_browser/display/config.py +500 -0
- cfabric_browser/display/sections.py +158 -0
- cfabric_browser/display/tree.py +704 -0
- cfabric_browser/display_settings/DT-UCPH/sp.yaml +7 -0
- cfabric_browser/display_settings/DT-UCPH/sp@7.2.4.yaml +16 -0
- cfabric_browser/downloads.py +347 -0
- cfabric_browser/formats.py +53 -0
- cfabric_browser/guard.py +86 -0
- cfabric_browser/helpers.py +51 -0
- cfabric_browser/interfaces.py +49 -0
- cfabric_browser/interlinear.py +185 -0
- cfabric_browser/jobs.py +105 -0
- cfabric_browser/lexicon.py +225 -0
- cfabric_browser/net.py +104 -0
- cfabric_browser/queries.py +377 -0
- cfabric_browser/registry.py +183 -0
- cfabric_browser/routes/__init__.py +59 -0
- cfabric_browser/routes/common.py +234 -0
- cfabric_browser/routes/lexicon.py +146 -0
- cfabric_browser/routes/library.py +367 -0
- cfabric_browser/routes/parallel.py +249 -0
- cfabric_browser/routes/proposals.py +72 -0
- cfabric_browser/routes/reading.py +511 -0
- cfabric_browser/routes/search.py +530 -0
- cfabric_browser/static/assets/App-Dnb20mEJ.js +1 -0
- cfabric_browser/static/assets/BookView-Cz8ooLry.js +1 -0
- cfabric_browser/static/assets/ConcordanceApp-DAJGIi6L.js +1 -0
- cfabric_browser/static/assets/InterfaceMenu-C0115Vat.js +9 -0
- cfabric_browser/static/assets/InterfaceMenu-Dl65sCtJ.css +1 -0
- cfabric_browser/static/assets/InterlinearApp-B-znfkr9.js +1 -0
- cfabric_browser/static/assets/LexiconView-BVsZI_sa.js +1 -0
- cfabric_browser/static/assets/Library-BMUS5-nO.js +2 -0
- cfabric_browser/static/assets/Mark-CJT08VRv.js +1 -0
- cfabric_browser/static/assets/Navigate-BXzUyAFT.js +1 -0
- cfabric_browser/static/assets/ParallelApp-BSNAzyxT.js +2 -0
- cfabric_browser/static/assets/QueryEditor-DzhpEhTk.js +14 -0
- cfabric_browser/static/assets/ReaderApp-CcJS5D-d.js +1 -0
- cfabric_browser/static/assets/Search-BQulV0gk.js +1 -0
- cfabric_browser/static/assets/SearchView-CMq5k7Qs.js +6 -0
- cfabric_browser/static/assets/Texts-DSY6lKNw.js +1 -0
- cfabric_browser/static/assets/amiri-arabic-400-normal-C_1DQziU.woff +0 -0
- cfabric_browser/static/assets/amiri-arabic-400-normal-D0NIBXga.woff2 +0 -0
- cfabric_browser/static/assets/amiri-latin-400-normal-Bv6IM0kz.woff +0 -0
- cfabric_browser/static/assets/amiri-latin-400-normal-Czk41Rjn.woff2 +0 -0
- cfabric_browser/static/assets/amiri-latin-ext-400-normal-C9hoIjPx.woff2 +0 -0
- cfabric_browser/static/assets/amiri-latin-ext-400-normal-CGn-6a7O.woff +0 -0
- cfabric_browser/static/assets/gentium-plus-cyrillic-400-normal-2gl_A-Tn.woff +0 -0
- cfabric_browser/static/assets/gentium-plus-cyrillic-400-normal-BKY-Jqah.woff2 +0 -0
- cfabric_browser/static/assets/gentium-plus-cyrillic-ext-400-normal-B9Nl3F-2.woff +0 -0
- cfabric_browser/static/assets/gentium-plus-cyrillic-ext-400-normal-CJ4YKexy.woff2 +0 -0
- cfabric_browser/static/assets/gentium-plus-greek-400-normal-CYsP5XPD.woff +0 -0
- cfabric_browser/static/assets/gentium-plus-greek-400-normal-FlMNHri1.woff2 +0 -0
- cfabric_browser/static/assets/gentium-plus-greek-ext-400-normal-DOc93t8p.woff2 +0 -0
- cfabric_browser/static/assets/gentium-plus-greek-ext-400-normal-Vs3SB95H.woff +0 -0
- cfabric_browser/static/assets/gentium-plus-latin-400-normal-Bx4sytbw.woff2 +0 -0
- cfabric_browser/static/assets/gentium-plus-latin-400-normal-CBUjaU8k.woff +0 -0
- cfabric_browser/static/assets/gentium-plus-latin-ext-400-normal-B46t2ITS.woff +0 -0
- cfabric_browser/static/assets/gentium-plus-latin-ext-400-normal-BID1L8QP.woff2 +0 -0
- cfabric_browser/static/assets/gentium-plus-vietnamese-400-normal-C_3yQKPY.woff +0 -0
- cfabric_browser/static/assets/gentium-plus-vietnamese-400-normal-lfG8qd7w.woff2 +0 -0
- cfabric_browser/static/assets/index-CerWo5pG.css +1 -0
- cfabric_browser/static/assets/index-DcB4YcRU.js +50 -0
- cfabric_browser/static/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- cfabric_browser/static/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- cfabric_browser/static/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- cfabric_browser/static/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- cfabric_browser/static/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- cfabric_browser/static/assets/noto-sans-syriac-latin-400-normal-D2_119oK.woff2 +0 -0
- cfabric_browser/static/assets/noto-sans-syriac-latin-400-normal-DpW_KDWJ.woff +0 -0
- cfabric_browser/static/assets/noto-sans-syriac-latin-ext-400-normal-CKWjGUw2.woff2 +0 -0
- cfabric_browser/static/assets/noto-sans-syriac-latin-ext-400-normal-G7wAZA1M.woff +0 -0
- cfabric_browser/static/assets/noto-sans-syriac-syriac-400-normal-Bz33PzP0.woff +0 -0
- cfabric_browser/static/assets/noto-sans-syriac-syriac-400-normal-CWH9OPix.woff2 +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-hebrew-400-normal-jyUp5Tfv.woff +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-hebrew-400-normal-v_xU1ROG.woff2 +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-hebrew-600-normal-5Q1AJcd_.woff2 +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-hebrew-600-normal-BBqYD5TV.woff +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-latin-400-normal-BWkL_LQQ.woff +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-latin-400-normal-D22OmSTw.woff2 +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-latin-600-normal-DUOQhjy6.woff2 +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-latin-600-normal-fgAR10AF.woff +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-latin-ext-400-normal-BiXgBxzW.woff2 +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-latin-ext-400-normal-DPDY_J25.woff +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-latin-ext-600-normal-D4x26vgI.woff +0 -0
- cfabric_browser/static/assets/noto-serif-hebrew-latin-ext-600-normal-DSAqAqQ_.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-cyrillic-ext-wght-italic-y6hTrs-Y.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-cyrillic-ext-wght-normal-DHTKMUil.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-cyrillic-wght-italic-CRzqOTyj.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-cyrillic-wght-normal-B1g8a0vD.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-greek-wght-italic-CUp-f2uw.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-greek-wght-normal-BpXpdlRW.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-latin-ext-wght-italic-B_cmBfNX.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-latin-ext-wght-normal-BVZ5su8S.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-latin-wght-italic-B-rCj6ir.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-latin-wght-normal-DMe4EKeb.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-vietnamese-wght-italic-D3EGHrWW.woff2 +0 -0
- cfabric_browser/static/assets/source-serif-4-vietnamese-wght-normal-BsaNuasd.woff2 +0 -0
- cfabric_browser/static/assets/terms-DEvE2HBo.js +2 -0
- cfabric_browser/static/favicon.svg +4 -0
- cfabric_browser/static/guide/adding-texts.html +67 -0
- cfabric_browser/static/guide/asking-claude.html +127 -0
- cfabric_browser/static/guide/concordance.html +75 -0
- cfabric_browser/static/guide/datasets.html +142 -0
- cfabric_browser/static/guide/getting-started.html +146 -0
- cfabric_browser/static/guide/images/bdb.png +0 -0
- cfabric_browser/static/guide/images/collocations.png +0 -0
- cfabric_browser/static/guide/images/concordance.png +0 -0
- cfabric_browser/static/guide/images/differences.png +0 -0
- cfabric_browser/static/guide/images/hover.png +0 -0
- cfabric_browser/static/guide/images/inspector.png +0 -0
- cfabric_browser/static/guide/images/interlinear.png +0 -0
- cfabric_browser/static/guide/images/lexicon.png +0 -0
- cfabric_browser/static/guide/images/library.png +0 -0
- cfabric_browser/static/guide/images/menu.png +0 -0
- cfabric_browser/static/guide/images/navigate.png +0 -0
- cfabric_browser/static/guide/images/panes.png +0 -0
- cfabric_browser/static/guide/images/parallel.png +0 -0
- cfabric_browser/static/guide/images/proposal.png +0 -0
- cfabric_browser/static/guide/images/reader-dark.png +0 -0
- cfabric_browser/static/guide/images/reader.png +0 -0
- cfabric_browser/static/guide/images/search-terms.png +0 -0
- cfabric_browser/static/guide/images/search.png +0 -0
- cfabric_browser/static/guide/images/statistics.png +0 -0
- cfabric_browser/static/guide/images/workspaces.png +0 -0
- cfabric_browser/static/guide/index.html +62 -0
- cfabric_browser/static/guide/interfaces.html +115 -0
- cfabric_browser/static/guide/notes-and-citing.html +74 -0
- cfabric_browser/static/guide/problems.html +71 -0
- cfabric_browser/static/guide/reading.html +105 -0
- cfabric_browser/static/guide/searching.html +223 -0
- cfabric_browser/static/guide/words.html +107 -0
- cfabric_browser/static/index.html +14 -0
- cfabric_browser/summary.py +202 -0
- cfabric_browser/translations/__init__.py +12 -0
- cfabric_browser/translations/build.py +505 -0
- cfabric_browser/translations/catalog.py +164 -0
- cfabric_browser/translations/hebrew_verses.json +41 -0
- cfabric_browser/translations/manage.py +123 -0
- cfabric_browser/window.py +81 -0
- cfabric_browser/witnesses.py +254 -0
- cfabric_browser/words.py +68 -0
- cfabric_browser/workers.py +627 -0
- cfabric_browser-0.1.0.dist-info/METADATA +399 -0
- cfabric_browser-0.1.0.dist-info/RECORD +173 -0
- cfabric_browser-0.1.0.dist-info/WHEEL +5 -0
- cfabric_browser-0.1.0.dist-info/entry_points.txt +3 -0
- cfabric_browser-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""`python -m cfabric_browser`, and the entry point of the standalone program."""
|
|
2
|
+
|
|
3
|
+
import multiprocessing
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from cfabric_browser.cli import main
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
# In a frozen program (PyInstaller), each search worker starts the program
|
|
10
|
+
# again; this turns that start into the worker instead of a second server.
|
|
11
|
+
multiprocessing.freeze_support()
|
|
12
|
+
sys.exit(main())
|
cfabric_browser/app.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""The browser's web application: the JSON API plus the single-page interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from importlib import metadata
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI, HTTPException
|
|
11
|
+
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
|
12
|
+
from fastapi.staticfiles import StaticFiles
|
|
13
|
+
from mcp.server.fastmcp.server import StreamableHTTPASGIApp
|
|
14
|
+
from starlette.routing import Route
|
|
15
|
+
|
|
16
|
+
from cfabric_browser import __version__
|
|
17
|
+
from cfabric_browser.assistant.mcp import build_server as build_mcp
|
|
18
|
+
from cfabric_browser.assistant.mcp import local
|
|
19
|
+
from cfabric_browser.assistant.tools import Tools
|
|
20
|
+
from cfabric_browser.corpora import CorpusService
|
|
21
|
+
from cfabric_browser.dictionaries.manage import DictionaryJobs
|
|
22
|
+
from cfabric_browser.downloads import DownloadJobs
|
|
23
|
+
from cfabric_browser.guard import Guard
|
|
24
|
+
from cfabric_browser.routes import build_router
|
|
25
|
+
from cfabric_browser.translations.manage import Jobs
|
|
26
|
+
from cfabric_browser.workers import SearchPool
|
|
27
|
+
|
|
28
|
+
STATIC = Path(__file__).parent / "static"
|
|
29
|
+
|
|
30
|
+
NOT_BUILT = """<!doctype html>
|
|
31
|
+
<html lang="en"><head><meta charset="utf-8"><title>CACCHT Explorer</title></head>
|
|
32
|
+
<body style="font-family: system-ui; max-width: 40rem; margin: 4rem auto">
|
|
33
|
+
<h1>CACCHT Explorer</h1>
|
|
34
|
+
<p>The server is running, but the interface has not been built.</p>
|
|
35
|
+
<p>Build it with <code>npm install</code> and <code>npm run build</code> in
|
|
36
|
+
<code>libs/explorer/frontend</code>, or use the API at <a href="/docs">/docs</a>.</p>
|
|
37
|
+
</body></html>
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def create_app(
|
|
42
|
+
service: CorpusService,
|
|
43
|
+
pool: SearchPool,
|
|
44
|
+
static_dir: Path = STATIC,
|
|
45
|
+
manage_pool: bool = True,
|
|
46
|
+
library: bool = True,
|
|
47
|
+
ui: str = "classic",
|
|
48
|
+
hidden_uis: list[str] | None = None,
|
|
49
|
+
translations: Jobs | None = None,
|
|
50
|
+
downloads: DownloadJobs | None = None,
|
|
51
|
+
ask_downloads: bool = False,
|
|
52
|
+
dictionaries: DictionaryJobs | None = None,
|
|
53
|
+
assistant: Tools | None = None,
|
|
54
|
+
hosts: list[str] | None = None,
|
|
55
|
+
) -> FastAPI:
|
|
56
|
+
"""Build the application.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
service: CorpusService
|
|
61
|
+
The corpora to serve.
|
|
62
|
+
pool: SearchPool
|
|
63
|
+
The search workers.
|
|
64
|
+
static_dir: Path
|
|
65
|
+
Directory with the built interface (`index.html` and `assets/`).
|
|
66
|
+
manage_pool: bool
|
|
67
|
+
Start the pool when the app starts and stop it when it stops.
|
|
68
|
+
library: bool
|
|
69
|
+
Let people open other corpora found on this computer. Switch this off
|
|
70
|
+
when the browser is reachable by others.
|
|
71
|
+
ui: str
|
|
72
|
+
The interface to start in when the user has not chosen one yet:
|
|
73
|
+
`classic` (panes), `parallel`, `reader`, `interlinear` or
|
|
74
|
+
`concordance` (see `cfabric_browser.interfaces`).
|
|
75
|
+
hidden_uis: list[str] | None
|
|
76
|
+
Interfaces to leave out of the menu until the user chooses otherwise.
|
|
77
|
+
translations: Jobs | None
|
|
78
|
+
Where translations are built and how (`cfabric_browser.translations`);
|
|
79
|
+
by default into the first place the library looks.
|
|
80
|
+
downloads: DownloadJobs | None
|
|
81
|
+
Where corpora are downloaded to (`cfabric_browser.downloads`); by
|
|
82
|
+
default the first place the library looks.
|
|
83
|
+
ask_downloads: bool
|
|
84
|
+
At the start, offer to download the corpora that can be downloaded and
|
|
85
|
+
are missing (the standalone program).
|
|
86
|
+
dictionaries: DictionaryJobs | None
|
|
87
|
+
Where dictionaries (BDB) are kept (`cfabric_browser.dictionaries`); by
|
|
88
|
+
default `~/.cfabric/dictionaries`.
|
|
89
|
+
assistant: Tools | None
|
|
90
|
+
The tools of searching in plain language and their proposals
|
|
91
|
+
(`cfabric_browser.assistant`); by default on these corpora and pool.
|
|
92
|
+
hosts: list[str] | None
|
|
93
|
+
The host names the server answers to (`cfabric_browser.guard`), such
|
|
94
|
+
as `guard.LOCAL_HOSTS` when it listens only on this computer; `None`
|
|
95
|
+
for any. Requests from other web sites are refused either way.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# searching in plain language: the tools, also as an MCP server at /mcp
|
|
99
|
+
tools = assistant if assistant is not None else Tools(service, pool)
|
|
100
|
+
mcpServer = build_mcp(local(tools))
|
|
101
|
+
mcpServer.streamable_http_app() # makes its session manager
|
|
102
|
+
|
|
103
|
+
@asynccontextmanager
|
|
104
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
105
|
+
if manage_pool:
|
|
106
|
+
pool.start(wait=False)
|
|
107
|
+
try:
|
|
108
|
+
async with mcpServer.session_manager.run():
|
|
109
|
+
yield
|
|
110
|
+
finally:
|
|
111
|
+
if manage_pool:
|
|
112
|
+
pool.shutdown()
|
|
113
|
+
|
|
114
|
+
app = FastAPI(
|
|
115
|
+
title="CACCHT Explorer",
|
|
116
|
+
version=__version__,
|
|
117
|
+
lifespan=lifespan,
|
|
118
|
+
)
|
|
119
|
+
app.add_middleware(Guard, hosts=hosts)
|
|
120
|
+
app.include_router(
|
|
121
|
+
build_router(
|
|
122
|
+
service,
|
|
123
|
+
pool,
|
|
124
|
+
library=library,
|
|
125
|
+
translations=translations,
|
|
126
|
+
downloads=downloads,
|
|
127
|
+
dictionaries=dictionaries,
|
|
128
|
+
assistant=tools,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
# before the interface's catch-all route
|
|
132
|
+
app.router.routes.insert(
|
|
133
|
+
0, Route("/mcp", endpoint=StreamableHTTPASGIApp(mcpServer.session_manager))
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
coreVersion = metadata.version("context-fabric")
|
|
138
|
+
except metadata.PackageNotFoundError:
|
|
139
|
+
coreVersion = None
|
|
140
|
+
|
|
141
|
+
@app.get("/api/health")
|
|
142
|
+
def health() -> dict:
|
|
143
|
+
return {
|
|
144
|
+
"ok": True,
|
|
145
|
+
"searchReady": pool.ready,
|
|
146
|
+
"version": __version__,
|
|
147
|
+
"coreVersion": coreVersion,
|
|
148
|
+
"ui": ui,
|
|
149
|
+
"hiddenUis": list(hidden_uis or []),
|
|
150
|
+
# offer the missing corpora at the start (/api/downloads)
|
|
151
|
+
"askDownloads": ask_downloads and library,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
index = static_dir / "index.html"
|
|
155
|
+
assets = static_dir / "assets"
|
|
156
|
+
if assets.is_dir():
|
|
157
|
+
app.mount("/assets", StaticFiles(directory=assets), name="assets")
|
|
158
|
+
|
|
159
|
+
@app.get("/{path:path}", include_in_schema=False)
|
|
160
|
+
def spa(path: str):
|
|
161
|
+
# the interface does its own routing: every non-API path gets the page
|
|
162
|
+
if path.startswith("api/"):
|
|
163
|
+
raise HTTPException(404, "Not found")
|
|
164
|
+
candidate = (static_dir / path).resolve()
|
|
165
|
+
inside = bool(path) and candidate.is_relative_to(static_dir.resolve())
|
|
166
|
+
if inside and candidate.is_file():
|
|
167
|
+
return FileResponse(candidate)
|
|
168
|
+
# a folder of pages (the guide, /guide/): its index.html, at an address
|
|
169
|
+
# ending in / so that the pages' relative links work
|
|
170
|
+
if inside and (candidate / "index.html").is_file():
|
|
171
|
+
if not path.endswith("/"):
|
|
172
|
+
return RedirectResponse(f"/{path}/")
|
|
173
|
+
return FileResponse(candidate / "index.html")
|
|
174
|
+
if index.is_file():
|
|
175
|
+
return FileResponse(index)
|
|
176
|
+
return HTMLResponse(NOT_BUILT)
|
|
177
|
+
|
|
178
|
+
return app
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Searching in plain language: the tools a language model gets to turn a
|
|
2
|
+
request ("all cases of >LHJM/ in the BHSA") into a search template that the
|
|
3
|
+
browser shows and runs.
|
|
4
|
+
|
|
5
|
+
`tools.py` has the tools; the model reaches them through MCP (the user's own
|
|
6
|
+
AI app, with their own subscription). The results always come from the
|
|
7
|
+
browser's search, never from the model's text.
|
|
8
|
+
"""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Searching the BHSA
|
|
2
|
+
|
|
3
|
+
The BHSA (Biblia Hebraica Stuttgartensia Amstelodamensis, ETCBC, version 2021) is the Hebrew Bible with linguistic annotations on words, phrases, clauses and sentences. A search template describes objects, one per line; indentation means "inside". The browser runs it; you only write it.
|
|
4
|
+
|
|
5
|
+
## Object types (how many), from large to small
|
|
6
|
+
|
|
7
|
+
book (39) > chapter (929) > verse (23,213) > half_verse (45,179) > sentence (63,717) > clause (88,131) > phrase (253,203) > word (426,590).
|
|
8
|
+
|
|
9
|
+
Also: `sentence_atom`, `clause_atom`, `phrase_atom`, `subphrase` (finer divisions), and `lex` (9,230 lexemes). A `lex` node contains all the words of that lexeme, so `lex gloss=create` with `word` indented under it finds the words of lexemes glossed "create".
|
|
10
|
+
|
|
11
|
+
## The main features
|
|
12
|
+
|
|
13
|
+
**Words** (`word`):
|
|
14
|
+
|
|
15
|
+
- `lex`: the lexeme in ETCBC transliteration (see *Conventions*): `>LHJM/`, `HLK[`, `JHWH/`.
|
|
16
|
+
- `sp`: part of speech: `subs` noun, `verb`, `prep` preposition, `conj` conjunction, `nmpr` proper noun, `art` article, `adjv` adjective, `nega` negation, `prps` personal pronoun, `advb` adverb, `prde` demonstrative pronoun, `intj` interjection, `inrg` interrogative particle, `prin` interrogative pronoun.
|
|
17
|
+
- `vs`: verbal stem: `qal`, `nif` niphal, `piel`, `pual`, `hit` hitpael, `hif` hiphil, `hof` hophal, and rarer ones (`hsht`, `poel`, ...); Aramaic `peal`, `pael`, `haf`, `afel`, `peil`, `htpe`, `htpa`, ... `NA` for words that aren't verbs.
|
|
18
|
+
- `vt`: verbal tense: `perf` perfect (qatal), `impf` imperfect (yiqtol), `wayq` wayyiqtol, `impv` imperative, `infc` infinitive construct, `infa` infinitive absolute, `ptca` active participle, `ptcp` passive participle.
|
|
19
|
+
- `ps` person (`p1`, `p2`, `p3`), `gn` gender (`m`, `f`), `nu` number (`sg`, `pl`, `du`); `unknown` where it can't be told (common gender, for instance), `NA` where it doesn't apply.
|
|
20
|
+
- `st`: state: `a` absolute, `c` construct, `e` emphatic (Aramaic).
|
|
21
|
+
- Pronominal suffix: `prs` its consonants (`W`, `K`, `HM`, ...; `absent` without one), with `prs_ps`, `prs_gn`, `prs_nu`.
|
|
22
|
+
- `language`: `Hebrew` or `Aramaic` (6,106 Aramaic words, in Daniel, Ezra and a few verses elsewhere).
|
|
23
|
+
- `nametype` of proper nouns: `pers` person, `topo` place, `gens` people, `ppde`, `mens`, and combinations such as `pers,gens,topo`: use `nametype~topo` to include those.
|
|
24
|
+
- `ls`: lexical set: `card` cardinal, `ordn` ordinal, `gntl` gentilic, `quot` quotation verb (אמר), `ques`, `nmdi` distributive noun, ...
|
|
25
|
+
- Text: `g_cons` (consonants in transliteration, without vowels: `>LHJM`), `g_word_utf8` (the word in Hebrew with vowels), `g_lex_utf8`.
|
|
26
|
+
- `gloss` (English) is on `lex` nodes, not on words.
|
|
27
|
+
|
|
28
|
+
**Phrases** (`phrase`):
|
|
29
|
+
|
|
30
|
+
- `function`: what the phrase does in its clause: `Pred` predicate, `Subj` subject, `Objc` object, `Cmpl` complement, `Adju` adjunct, `Time`, `Loca` location, `Modi` modifier, `Conj` conjunction, `Nega` negation, `Ques` question, `Rela` relative, `Voct` vocative, `Intj`, `PreC` predicate complement, `PreO` predicate with object suffix, `PreS` predicate with subject suffix, `Frnt` fronted element, `Supp` supplementary constituent, ...
|
|
31
|
+
- `typ`: phrase type: `VP` verbal, `NP` nominal, `PP` prepositional, `CP` conjunctive, `PrNP` proper-noun phrase, `AdvP`, `NegP`, `InrP`, `AdjP`, `PPrP` personal pronoun, `DPrP` demonstrative pronoun, ...
|
|
32
|
+
- `det`: determination: `det`, `und` (undetermined).
|
|
33
|
+
|
|
34
|
+
**Clauses** (`clause`):
|
|
35
|
+
|
|
36
|
+
- `typ`: clause type, by the form of its verb and what comes before it: `Way0` wayyiqtol first, `WayX` wayyiqtol after something, `WQt0` weqatal first, `xQt0` qatal after a word, `xQtX` qatal after a constituent, `XQtl` qatal after a non-subject, `ZQt0`, `ZYq0` (no conjunction), `xYq0`, `WYq0` (yiqtol), `ZIm0` imperative, `NmCl` nominal clause, `AjCl` adjectival, `InfC` infinitive construct clause, `Ptcp` participle clause, `Ellp` ellipsis, `Voct`, `Defc` defective, `CPen` casus pendens, `MSyn` macrosyntactic sign, ...
|
|
37
|
+
- `rela`: relation to another clause: `Objc` object clause, `Subj` subject clause, `Attr` attributive (relative), `Adju` adjunct, `Cmpl`, `PreC`, `RgRc` regens/rectum, `Resu` resumptive, `Coor` coordinated, `Spec`, `Appo`, `Link`, ...; `NA` for main clauses.
|
|
38
|
+
- `kind`: `VC` verbal clause, `NC` nominal clause, `WP` without predicate.
|
|
39
|
+
- `domain`: `N` narrative, `Q` quotation (direct speech), `D` discursive, `?` unknown.
|
|
40
|
+
- `txt`: text type by levels of embedding (`N`, `Q`, `NQ`, `NQQ`, ...).
|
|
41
|
+
|
|
42
|
+
**Sections**: `book book=Genesis`, `chapter chapter=1`, `verse verse=3`, indented in that order.
|
|
43
|
+
|
|
44
|
+
## Conventions
|
|
45
|
+
|
|
46
|
+
- **Lexemes** are written in ETCBC transliteration: `>` א, `B` ב, `G` ג, `D` ד, `H` ה, `W` ו, `Z` ז, `X` ח, `V` ט, `J` י, `K` כ, `L` ל, `M` מ, `N` נ, `S` ס, `<` ע, `P` פ, `Y` צ, `Q` ק, `R` ר, `F` שׂ, `C` שׁ, `T` ת (final forms the same).
|
|
47
|
+
- A verb's lexeme ends in `[` (`HLK[` to go, `BR>[` to create), a noun's or adjective's in `/` (`>LHJM/` god(s), `MLK/` king, `VWB/` good), particles have neither (`L` to, `>CR` which, `KJ` that, `W` and, `H` the article, `H=` the interrogative he).
|
|
48
|
+
- `=` marks homonyms: `DBR[` speak and `DBR/` word; `>MR[` say; `H` the article but `H=` the interrogative; `QW=/`, `<RB===/`. When a lexeme seems to give too few results, check its homonyms with `describe_feature` on `lex`, or look for its gloss: `lex gloss=word`.
|
|
49
|
+
- **Book names** in the `book` feature are Latin, with Roman numerals after the name: Genesis, Exodus, Leviticus, Numeri, Deuteronomium, Josua, Judices, Samuel_I, Samuel_II, Reges_I, Reges_II, Jesaia, Jeremia, Ezechiel, Hosea, Joel, Amos, Obadia, Jona, Micha, Nahum, Habakuk, Zephania, Haggai, Sacharia, Maleachi, Psalmi, Iob, Proverbia, Ruth, Canticum, Ecclesiastes, Threni, Esther, Daniel, Esra, Nehemia, Chronica_I, Chronica_II. So Psalms is `book book=Psalmi`, 1 Kings is `book book=Reges_I`.
|
|
50
|
+
- Values are exact: `vt=infc`, not `vt=inf` or `vt=infinitive`. Several values with `|`: `prs_ps=p1|p2`. Not a value with `#`: `prs#absent` (with a suffix). A pattern with `~`: `nametype~topo`, `gloss~^king` (starting with "king").
|
|
51
|
+
- Hebrew and Aramaic words have the same features; add `language=Aramaic` (or `Hebrew`) to keep to one.
|
|
52
|
+
|
|
53
|
+
## Templates in short
|
|
54
|
+
|
|
55
|
+
- One object per line; an indented line lies inside the object above it.
|
|
56
|
+
- Features after the type: `word sp=verb vs=hif`.
|
|
57
|
+
- Two objects at the same level in no particular order, unless related: name them and add a relation on a line of its own: `p1:phrase function=Pred`, `p2:phrase function=Subj`, then `p1 < p2` (p1 before p2). `<:` means "right before" (adjacent).
|
|
58
|
+
- Quantifiers, each closed with `/-/`: `/with/` (has this inside), `/without/` (none of these inside), `/where/ … /have/` (every object of the `/where/` part also fits the `/have/` part). In `/have/`, name the object of `/where/` and refer to it: `w:word` under `/where/`, `w sp=nmpr` under `/have/`. A new line such as `word sp=nmpr` under `/have/` is another object, so it only asks that there is one. `/where/` also lets through what has no such object at all; "only proper names" is shorter as `/without/` with `word sp#nmpr`.
|
|
59
|
+
- Results are tuples, one node per line of the template; the count is the number of tuples. Ask for the object the user wants counted at the top, or as the only line.
|
|
60
|
+
|
|
61
|
+
## Worked examples (with their number of results in the BHSA)
|
|
62
|
+
|
|
63
|
+
| Request | Template | Results |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| all cases of >LHJM/ | `word lex=>LHJM/` | 2,601 |
|
|
66
|
+
| the verb HLK[ in all its forms | `word lex=HLK[` | 1,554 |
|
|
67
|
+
| HLK[ in the qal | `word lex=HLK[ vs=qal` | 1,412 |
|
|
68
|
+
| infinitives construct of HLK[ | `word lex=HLK[ vt=infc` | 144 |
|
|
69
|
+
| active participles of >MR[ | `word lex=>MR[ vt=ptca` | 138 |
|
|
70
|
+
| the niphal of DBR[ | `word lex=DBR[ vs=nif` | 4 |
|
|
71
|
+
| hiphil imperfects | `word vs=hif vt=impf` | 2,264 |
|
|
72
|
+
| perfects, second person feminine singular | `word vt=perf ps=p2 gn=f nu=sg` | 222 |
|
|
73
|
+
| the name YHWH | `word lex=JHWH/` | 6,828 |
|
|
74
|
+
| Aramaic words | `word language=Aramaic` | 6,106 |
|
|
75
|
+
| place names | `word sp=nmpr nametype=topo` | 6,371 |
|
|
76
|
+
| place names, also those that are also personal names | `word sp=nmpr nametype~topo` | 10,916 |
|
|
77
|
+
| feminine plural nouns | `word sp=subs gn=f nu=pl` | 5,992 |
|
|
78
|
+
| nouns in the construct state | `word sp=subs st=c` | 36,679 |
|
|
79
|
+
| verbs with a suffix of the first person | `word sp=verb prs_ps=p1` | 1,649 |
|
|
80
|
+
| the interrogative he | `word lex=H=` | 743 |
|
|
81
|
+
| words with the consonants >LHJM, of any lexeme | `word g_cons=>LHJM` | 1,177 |
|
|
82
|
+
| the words of lexemes glossed "create" | `lex gloss=create` / ` word` | 54 |
|
|
83
|
+
| the words of lexemes whose gloss starts with "king" | `lex gloss~^king` / ` word` | 3,001 |
|
|
84
|
+
| wayyiqtol forms in Genesis | `book book=Genesis` / ` word vt=wayq` | 2,106 |
|
|
85
|
+
| imperatives in the Psalms | `book book=Psalmi` / ` word vt=impv` | 701 |
|
|
86
|
+
| >LHJM/ in Genesis 1 | `book book=Genesis` / ` chapter chapter=1` / ` word lex=>LHJM/` | 32 |
|
|
87
|
+
| phrases that are the subject | `phrase function=Subj` | 31,979 |
|
|
88
|
+
| determined noun phrases | `phrase typ=NP det=det` | 20,476 |
|
|
89
|
+
| clauses with a predicate and a subject | `clause` / ` phrase function=Pred` / ` phrase function=Subj` | 18,966 |
|
|
90
|
+
| clauses with the predicate before the subject | `clause` / ` p1:phrase function=Pred` / ` p2:phrase function=Subj` / `p1 < p2` | 13,938 |
|
|
91
|
+
| clauses with the subject before the predicate | `clause` / ` p1:phrase function=Subj` / ` p2:phrase function=Pred` / `p1 < p2` | 5,028 |
|
|
92
|
+
| clauses whose subject is >LHJM/, with a verb as predicate | `clause` / ` phrase function=Pred` / ` word sp=verb` / ` phrase function=Subj` / ` word lex=>LHJM/` | 614 |
|
|
93
|
+
| clauses where >LHJM/ is the subject of >MR[ | `clause` / ` phrase function=Pred` / ` word lex=>MR[` / ` phrase function=Subj` / ` word lex=>LHJM/` | 147 |
|
|
94
|
+
| clauses with HJH[ as predicate | `clause` / ` phrase function=Pred` / ` word lex=HJH[` | 3,521 |
|
|
95
|
+
| nominal clauses | `clause typ=NmCl` | 11,377 |
|
|
96
|
+
| wayyiqtol clauses with nothing before the verb | `clause typ=Way0` | 9,038 |
|
|
97
|
+
| qatal after a constituent (x-qatal-X) | `clause typ=xQtX` | 2,797 |
|
|
98
|
+
| object clauses | `clause rela=Objc` | 1,327 |
|
|
99
|
+
| relative clauses with >CR | `clause` / ` phrase function=Rela` / ` word lex=>CR` | 4,738 |
|
|
100
|
+
| negated clauses | `clause` / ` phrase function=Nega` | 6,044 |
|
|
101
|
+
| clauses in direct speech | `clause domain=Q` | 52,937 |
|
|
102
|
+
| an infinitive construct right after the preposition L | `phrase` / ` w1:word lex=L` / ` w2:word vt=infc` / `w1 <: w2` | 4,572 |
|
|
103
|
+
| a noun followed right away by an adjective | `phrase typ=NP` / ` w1:word sp=subs` / ` w2:word sp=adjv` / `w1 <: w2` | 1,579 |
|
|
104
|
+
| >MR[ followed in its clause by L with a suffix | `clause` / ` w1:word lex=>MR[` / ` w2:word lex=L prs#absent` / `w1 < w2` | 347 |
|
|
105
|
+
| verses with both >LHJM/ and BR>[ (21 combinations of a verse and the two words, in 12 verses) | `verse` / ` word lex=>LHJM/` / ` word lex=BR>[` | 21 |
|
|
106
|
+
| verses without a verb | `verse` / `/without/` / ` word sp=verb` / `/-/` | 1,607 |
|
|
107
|
+
| subjects that are only proper names | `phrase function=Subj` / `/where/` / ` w:word` / `/have/` / ` w sp=nmpr` / `/-/` | 5,893 |
|
|
108
|
+
| the same, shorter | `phrase function=Subj` / `/without/` / ` word sp#nmpr` / `/-/` | 5,893 |
|
|
109
|
+
| subjects with at least one proper name | `phrase function=Subj` / `/with/` / ` word sp=nmpr` / `/-/` | 10,484 |
|
|
110
|
+
| verses with both >HB[ and JHWH/, each verse once | `verse` / `/with/` / ` word lex=>HB[` / `/-/` / `/with/` / ` word lex=JHWH/` / `/-/` | 60 |
|
|
111
|
+
|
|
112
|
+
(`/` separates the lines of a template here; write each on its own line, with the indentation shown.)
|
|
113
|
+
|
|
114
|
+
## Common mistakes
|
|
115
|
+
|
|
116
|
+
- English or Hebrew values: `sp=noun`, `vt=infinitive`, `vs=hiphil` find nothing; use the codes above (`subs`, `infc`, `hif`).
|
|
117
|
+
- A lexeme without its ending: `lex=HLK` finds nothing; it is `HLK[`. `try_search` names a value that a feature never has.
|
|
118
|
+
- English book names: `book=Psalms` finds nothing; it is `Psalmi`.
|
|
119
|
+
- `gloss` on a word: it is on `lex` nodes (`lex gloss=create`, with `word` under it).
|
|
120
|
+
- `function` on a word or a clause: it is a feature of phrases.
|
|
121
|
+
- The article `H` for the interrogative `H=`, `DBR[` (speak) for `DBR/` (word).
|
|
122
|
+
- `/where/` with a new object under `/have/` (`/where/` / ` word` / `/have/` / ` word sp=nmpr`): that finds subjects with at least one proper name (10,484), not subjects of proper names only (5,893). Name the object and refer to it, or use `/without/`.
|
|
123
|
+
- Counting: `verse` with two words under it gives a result for every pair of matching words in the verse, not one per verse. To count verses, keep the verse alone with its conditions inside a quantifier, or tell the user that the count is of combinations.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""The tools as an MCP server, for the user's own AI app (Claude Code,
|
|
2
|
+
Claude Desktop, the ChatGPT desktop app, Codex, ...).
|
|
3
|
+
|
|
4
|
+
They are served at `/mcp` on the browser (Streamable HTTP, stateless, with
|
|
5
|
+
JSON answers), for apps that reach a local HTTP server (Claude Code, Codex). It
|
|
6
|
+
accepts only 127.0.0.1/localhost as host and no web page as origin, so
|
|
7
|
+
another tab in a web browser can't use it. Apps that start their own server
|
|
8
|
+
over stdio (Claude Desktop, the ChatGPT desktop app) go through a small proxy that passes each message
|
|
9
|
+
on to `/mcp`: `cfabric-browser mcp` (`stdio.py`), or the Desktop Extension's
|
|
10
|
+
own (`packaging/mcpb/`).
|
|
11
|
+
|
|
12
|
+
All tools but `propose_search` are marked read-only, so that apps which ask
|
|
13
|
+
before each tool that changes something (Codex) ask only for the proposal.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Awaitable, Callable
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from anyio import to_thread
|
|
22
|
+
from mcp.server.fastmcp import Context, FastMCP
|
|
23
|
+
from mcp.server.transport_security import TransportSecuritySettings
|
|
24
|
+
from mcp.types import ToolAnnotations
|
|
25
|
+
|
|
26
|
+
# calls a tool by name with its arguments; returns what the tool returns
|
|
27
|
+
Call = Callable[[str, dict[str, Any]], Awaitable[Any]]
|
|
28
|
+
|
|
29
|
+
INSTRUCTIONS = """\
|
|
30
|
+
You are helping a user search corpora (the Hebrew Bible as the BHSA, and
|
|
31
|
+
others) that are open in CACCHT Explorer. The user says in their
|
|
32
|
+
own words what they want to find; you write a search template for it.
|
|
33
|
+
|
|
34
|
+
The results always come from the browser's search, never from your own
|
|
35
|
+
knowledge: don't answer with counts or lists of passages yourself.
|
|
36
|
+
|
|
37
|
+
1. list_corpora, then corpus_guide for the corpus (it explains the features
|
|
38
|
+
and conventions, with examples); describe_corpus, list_features and
|
|
39
|
+
describe_feature for more.
|
|
40
|
+
2. Write a template (search_syntax_guide explains the language) and check it
|
|
41
|
+
with check_template.
|
|
42
|
+
3. Run it as a trial with try_search: look at the count and the examples. If
|
|
43
|
+
there are 0 results, read the hints; if far too many, narrow it. Repeat.
|
|
44
|
+
4. propose_search with the template, the user's request in their own words
|
|
45
|
+
and a short explanation of each line. The browser shows it to the user,
|
|
46
|
+
who runs it with Run. Tell the user it is there, with the trial's count.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# the header in which `cfabric-browser mcp` passes on the app's name, from
|
|
50
|
+
# its `initialize` (`/mcp` is stateless, so it never sees that itself)
|
|
51
|
+
CLIENT_HEADER = "X-Cfabric-Client"
|
|
52
|
+
|
|
53
|
+
# the names apps give themselves (`clientInfo.name`, or in their User-Agent),
|
|
54
|
+
# as the user knows them; the first that occurs in it
|
|
55
|
+
CLIENTS = (
|
|
56
|
+
("chatgpt", "ChatGPT"),
|
|
57
|
+
("codex", "Codex"),
|
|
58
|
+
("claude", "Claude"),
|
|
59
|
+
("antigravity", "Gemini"),
|
|
60
|
+
("gemini", "Gemini"),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
READS = ToolAnnotations(readOnlyHint=True, openWorldHint=False)
|
|
64
|
+
# it shows something in the browser, and changes nothing else
|
|
65
|
+
PROPOSES = ToolAnnotations(
|
|
66
|
+
readOnlyHint=False, destructiveHint=False, idempotentHint=False, openWorldHint=False
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# the tools, by name
|
|
70
|
+
TOOLS = (
|
|
71
|
+
"list_corpora",
|
|
72
|
+
"corpus_guide",
|
|
73
|
+
"describe_corpus",
|
|
74
|
+
"list_features",
|
|
75
|
+
"describe_feature",
|
|
76
|
+
"search_syntax_guide",
|
|
77
|
+
"check_template",
|
|
78
|
+
"try_search",
|
|
79
|
+
"propose_search",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_server(call: Call, name: str = "CACCHT Explorer") -> FastMCP:
|
|
84
|
+
server = FastMCP(
|
|
85
|
+
name,
|
|
86
|
+
instructions=INSTRUCTIONS,
|
|
87
|
+
stateless_http=True,
|
|
88
|
+
json_response=True,
|
|
89
|
+
streamable_http_path="/mcp",
|
|
90
|
+
transport_security=TransportSecuritySettings(
|
|
91
|
+
enable_dns_rebinding_protection=True,
|
|
92
|
+
allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"],
|
|
93
|
+
# no web page may call it, not even the browser's own
|
|
94
|
+
allowed_origins=[],
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@server.tool(annotations=READS)
|
|
99
|
+
async def list_corpora() -> dict[str, Any]:
|
|
100
|
+
"""The corpora open in the browser: their names (to pass to the other
|
|
101
|
+
tools), titles, ids, and section types (book, chapter, verse)."""
|
|
102
|
+
return await call("list_corpora", {})
|
|
103
|
+
|
|
104
|
+
@server.tool(annotations=READS)
|
|
105
|
+
async def corpus_guide(corpus: str) -> dict[str, Any]:
|
|
106
|
+
"""A guide to searching this corpus: its object types, its main
|
|
107
|
+
features in plain words, conventions (such as how lexemes are
|
|
108
|
+
written), worked examples and common mistakes. Read it first."""
|
|
109
|
+
return await call("corpus_guide", {"corpus": corpus})
|
|
110
|
+
|
|
111
|
+
@server.tool(annotations=READS)
|
|
112
|
+
async def describe_corpus(corpus: str) -> dict[str, Any]:
|
|
113
|
+
"""The corpus' object types (word, phrase, clause, verse, ...) with
|
|
114
|
+
their numbers, and its sections."""
|
|
115
|
+
return await call("describe_corpus", {"corpus": corpus})
|
|
116
|
+
|
|
117
|
+
@server.tool(annotations=READS)
|
|
118
|
+
async def list_features(
|
|
119
|
+
corpus: str, node_types: list[str] | None = None
|
|
120
|
+
) -> dict[str, Any]:
|
|
121
|
+
"""The features of the corpus, with descriptions; only those of
|
|
122
|
+
`node_types` (such as ["word"]) if given."""
|
|
123
|
+
return await call("list_features", {"corpus": corpus, "node_types": node_types})
|
|
124
|
+
|
|
125
|
+
@server.tool(annotations=READS)
|
|
126
|
+
async def describe_feature(corpus: str, features: list[str]) -> dict[str, Any]:
|
|
127
|
+
"""Features in detail: their most frequent values with counts, so
|
|
128
|
+
that a template uses values the corpus has."""
|
|
129
|
+
return await call("describe_feature", {"corpus": corpus, "features": features})
|
|
130
|
+
|
|
131
|
+
@server.tool(annotations=READS)
|
|
132
|
+
async def search_syntax_guide(section: str | None = None) -> dict[str, Any]:
|
|
133
|
+
"""The search template language: objects on lines, indentation for
|
|
134
|
+
what lies inside what, features, relations and quantifiers. Without
|
|
135
|
+
`section`, an overview with the sections there are."""
|
|
136
|
+
return await call("search_syntax_guide", {"section": section})
|
|
137
|
+
|
|
138
|
+
@server.tool(annotations=READS)
|
|
139
|
+
async def check_template(corpus: str, template: str) -> dict[str, Any]:
|
|
140
|
+
"""Check a template without searching: its problems (syntax, unknown
|
|
141
|
+
object types or features), each with its line; `ok` when none."""
|
|
142
|
+
return await call("check_template", {"corpus": corpus, "template": template})
|
|
143
|
+
|
|
144
|
+
@server.tool(annotations=READS)
|
|
145
|
+
async def try_search(
|
|
146
|
+
corpus: str, template: str, examples: int = 5
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""Run a template as a trial: the number of results (`atLeast` when
|
|
149
|
+
there are more than the trial counts), a few examples with their
|
|
150
|
+
passage and text, and for 0 results hints such as a value a feature
|
|
151
|
+
never has. For checking your template, not as the answer."""
|
|
152
|
+
return await call(
|
|
153
|
+
"try_search", {"corpus": corpus, "template": template, "examples": examples}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@server.tool(annotations=PROPOSES)
|
|
157
|
+
async def propose_search(
|
|
158
|
+
corpus: str,
|
|
159
|
+
template: str,
|
|
160
|
+
request: str,
|
|
161
|
+
explanation: str,
|
|
162
|
+
model: str | None = None,
|
|
163
|
+
ctx: Context | None = None,
|
|
164
|
+
) -> dict[str, Any]:
|
|
165
|
+
"""Show the search in the user's browser, where they run it with Run,
|
|
166
|
+
edit it, or dismiss it. `request`: what the user asked, in their own
|
|
167
|
+
words; `explanation`: what each line of the template does, briefly;
|
|
168
|
+
`model`: the name of the AI app or model, as the user should see it
|
|
169
|
+
(such as "Claude" or "ChatGPT").
|
|
170
|
+
A template with problems is not shown: they come back to be fixed."""
|
|
171
|
+
return await call(
|
|
172
|
+
"propose_search",
|
|
173
|
+
{
|
|
174
|
+
"corpus": corpus,
|
|
175
|
+
"template": template,
|
|
176
|
+
"request": request,
|
|
177
|
+
"explanation": explanation,
|
|
178
|
+
"model": model or client_name(ctx),
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return server
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def readable(name: str) -> str | None:
|
|
186
|
+
"""The name of an app as the user knows it ("codex-mcp-client" is
|
|
187
|
+
Codex), None for one we don't know."""
|
|
188
|
+
lowered = name.lower()
|
|
189
|
+
for part, known in CLIENTS:
|
|
190
|
+
if part in lowered:
|
|
191
|
+
return known
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def client_name(ctx: Context | None) -> str | None:
|
|
196
|
+
"""The name of the app that called, for a proposal without `model`: from
|
|
197
|
+
the stdio helper's header, else the User-Agent."""
|
|
198
|
+
try:
|
|
199
|
+
request = ctx.request_context.request if ctx is not None else None
|
|
200
|
+
except ValueError: # not in a request
|
|
201
|
+
return None
|
|
202
|
+
if request is None:
|
|
203
|
+
return None
|
|
204
|
+
headers = request.headers
|
|
205
|
+
return readable(headers.get(CLIENT_HEADER) or headers.get("user-agent") or "")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def local(tools: Any) -> Call:
|
|
209
|
+
"""Calls on the browser's own tools (for `/mcp`), in a thread: a trial
|
|
210
|
+
search waits for the pool."""
|
|
211
|
+
from cfabric_browser.assistant.tools import ToolError
|
|
212
|
+
|
|
213
|
+
async def call(name: str, args: dict[str, Any]) -> Any:
|
|
214
|
+
if name not in TOOLS:
|
|
215
|
+
raise ValueError(f"No tool {name}")
|
|
216
|
+
|
|
217
|
+
def run() -> Any:
|
|
218
|
+
try:
|
|
219
|
+
return getattr(tools, name)(**args)
|
|
220
|
+
except ToolError as e:
|
|
221
|
+
return {"ok": False, "error": str(e)}
|
|
222
|
+
|
|
223
|
+
return await to_thread.run_sync(run)
|
|
224
|
+
|
|
225
|
+
return call
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Searches proposed by a language model, kept until the user runs or
|
|
2
|
+
dismisses them.
|
|
3
|
+
|
|
4
|
+
A proposal has the user's request in their own words, the template, the
|
|
5
|
+
model's explanation, which model made it and when, and the trial's count.
|
|
6
|
+
The open page follows the proposals live (`/api/proposals/events`); nothing
|
|
7
|
+
runs until the user presses *Run*.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import datetime
|
|
13
|
+
import threading
|
|
14
|
+
import uuid
|
|
15
|
+
from dataclasses import asdict, dataclass, field
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
NEW = "new"
|
|
19
|
+
RUN = "run"
|
|
20
|
+
DISMISSED = "dismissed"
|
|
21
|
+
STATES = (NEW, RUN, DISMISSED)
|
|
22
|
+
KEEP = 50 # proposals kept, the newest
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Proposal:
|
|
27
|
+
id: str
|
|
28
|
+
corpus: str
|
|
29
|
+
template: str
|
|
30
|
+
request: str
|
|
31
|
+
explanation: str
|
|
32
|
+
model: str | None
|
|
33
|
+
created: str
|
|
34
|
+
# the trial before proposing: its count, and whether that is a lower bound
|
|
35
|
+
count: int | None = None
|
|
36
|
+
atLeast: bool = False
|
|
37
|
+
state: str = NEW
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ProposalStore:
|
|
42
|
+
proposals: list[Proposal] = field(default_factory=list)
|
|
43
|
+
version: int = 0
|
|
44
|
+
_cond: threading.Condition = field(default_factory=threading.Condition, repr=False)
|
|
45
|
+
|
|
46
|
+
def add(
|
|
47
|
+
self,
|
|
48
|
+
corpus: str,
|
|
49
|
+
template: str,
|
|
50
|
+
request: str,
|
|
51
|
+
explanation: str,
|
|
52
|
+
model: str | None = None,
|
|
53
|
+
count: int | None = None,
|
|
54
|
+
atLeast: bool = False,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
proposal = Proposal(
|
|
57
|
+
id=uuid.uuid4().hex[:12],
|
|
58
|
+
corpus=corpus,
|
|
59
|
+
template=template,
|
|
60
|
+
request=request,
|
|
61
|
+
explanation=explanation,
|
|
62
|
+
model=model,
|
|
63
|
+
created=datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds"),
|
|
64
|
+
count=count,
|
|
65
|
+
atLeast=atLeast,
|
|
66
|
+
)
|
|
67
|
+
with self._cond:
|
|
68
|
+
self.proposals.append(proposal)
|
|
69
|
+
del self.proposals[:-KEEP]
|
|
70
|
+
self.version += 1
|
|
71
|
+
self._cond.notify_all()
|
|
72
|
+
return asdict(proposal)
|
|
73
|
+
|
|
74
|
+
def set_state(self, pid: str, state: str) -> dict[str, Any] | None:
|
|
75
|
+
if state not in STATES:
|
|
76
|
+
raise ValueError(f"Unknown state {state!r}")
|
|
77
|
+
with self._cond:
|
|
78
|
+
for p in self.proposals:
|
|
79
|
+
if p.id == pid:
|
|
80
|
+
p.state = state
|
|
81
|
+
self.version += 1
|
|
82
|
+
self._cond.notify_all()
|
|
83
|
+
return asdict(p)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def snapshot(self) -> dict[str, Any]:
|
|
87
|
+
"""All proposals, the newest first, and the version they are at."""
|
|
88
|
+
with self._cond:
|
|
89
|
+
return {
|
|
90
|
+
"version": self.version,
|
|
91
|
+
"proposals": [asdict(p) for p in reversed(self.proposals)],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def wait_change(self, version: int, timeout: float) -> int:
|
|
95
|
+
"""Block until the proposals change after `version`, or the timeout."""
|
|
96
|
+
with self._cond:
|
|
97
|
+
if self.version == version:
|
|
98
|
+
self._cond.wait(timeout)
|
|
99
|
+
return self.version
|