@littlebigbrain/create 0.4.0 → 0.6.0
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.
- package/README.md +42 -6
- package/lib/create.mjs +228 -14
- package/package.json +2 -1
- package/project/python/values_extract/tests/fake_lbb.py +226 -0
- package/project/python/values_extract/tests/test_workflow.py +150 -0
- package/python/values_extract/README.md +52 -0
- package/python/values_extract/__init__.py +3 -0
- package/python/values_extract/__main__.py +97 -0
- package/python/values_extract/samples/expected.json +149 -0
- package/python/values_extract/samples/records.json +26 -0
- package/python/values_extract/samples.py +37 -0
- package/python/values_extract/workflow.py +1044 -0
- package/python/versions.json +3 -0
- package/templates/_shared/testing.ts +259 -5
- package/templates/ontology-fit/README.md +41 -32
- package/templates/ontology-fit/workflow.test.ts +56 -5
- package/templates/ontology-fit/workflow.ts +708 -207
- package/templates/review/README.md +56 -29
- package/templates/review/decision.ts +118 -37
- package/templates/review/workflow.test.ts +30 -10
- package/templates/review/workflow.ts +123 -76
- package/templates/rows/README.md +8 -1
- package/templates/rows/workflow.test.ts +56 -0
- package/templates/rows/workflow.ts +182 -148
- package/templates/sections/README.md +8 -1
- package/templates/sections/workflow.test.ts +51 -1
- package/templates/sections/workflow.ts +137 -98
- package/templates/values-extract/README.md +92 -23
- package/templates/values-extract/samples/expected.json +1 -0
- package/templates/values-extract/test-examples.ts +155 -0
- package/templates/values-extract/workflow.test.ts +532 -27
- package/templates/values-extract/workflow.ts +719 -179
- package/templates/versions.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""The workflow on its samples, with mock models and no server: the SDK's
|
|
2
|
+
``WorkflowWorker`` runs it against a fake LBB (``fake_lbb.py``).
|
|
3
|
+
|
|
4
|
+
The mock reasoning model reads the first size (``60x40 cm`` or ``Breite 60
|
|
5
|
+
cm``) with its unit, the first material and the water supply of each text.
|
|
6
|
+
The mock decision model says a quote from a sentence about an accessory
|
|
7
|
+
(``Zubehör``) or a conditional variant (``Bei Einsatz``) does not support its
|
|
8
|
+
value. The values the workflow must write, and the values it must leave out,
|
|
9
|
+
come from the answer key ``samples/expected.json``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from fake_lbb import FakeLbb, iri_of, message, serve
|
|
19
|
+
|
|
20
|
+
from values_extract.samples import SAMPLES
|
|
21
|
+
from values_extract.workflow import DEFAULTS
|
|
22
|
+
|
|
23
|
+
RECORDS: list[dict[str, str]] = json.loads((SAMPLES / "records.json").read_text(encoding="utf-8"))
|
|
24
|
+
EXPECTED: dict[str, Any] = json.loads((SAMPLES / "expected.json").read_text(encoding="utf-8"))
|
|
25
|
+
MATERIALS = [
|
|
26
|
+
"Sanitärkeramik",
|
|
27
|
+
"Stahl emailliert",
|
|
28
|
+
"Mineralguss",
|
|
29
|
+
"Edelstahl",
|
|
30
|
+
"Kristallglas",
|
|
31
|
+
"Keramik",
|
|
32
|
+
"Glas",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_values(text: str) -> list[dict[str, str]]:
|
|
37
|
+
"""The mock reasoning model: the first size, material and water supply of a text."""
|
|
38
|
+
values: list[dict[str, str]] = []
|
|
39
|
+
size = re.search(r"(?:ca\. )?(\d+)x(\d+) cm", text)
|
|
40
|
+
width = re.search(r"Breite (\d+) cm", text)
|
|
41
|
+
if size:
|
|
42
|
+
values += [
|
|
43
|
+
{"property": "width_cm", "value": size[1], "unit": "cm", "quote": size[0]},
|
|
44
|
+
{"property": "depth_cm", "value": size[2], "unit": "cm", "quote": size[0]},
|
|
45
|
+
]
|
|
46
|
+
elif width:
|
|
47
|
+
values.append({"property": "width_cm", "value": width[1], "unit": "cm", "quote": width[0]})
|
|
48
|
+
found = [
|
|
49
|
+
(m.start(), name)
|
|
50
|
+
for name in MATERIALS
|
|
51
|
+
if (m := re.search(rf"(?<![^\W\d_]){re.escape(name)}(?![^\W\d_])", text))
|
|
52
|
+
]
|
|
53
|
+
if found:
|
|
54
|
+
material = min(found)[1]
|
|
55
|
+
values.append({"property": "material", "value": material, "unit": "", "quote": material})
|
|
56
|
+
water = re.search(r"Anschluss[^.]*?(Kalt- und Warmwasser|Kaltwasser)", text)
|
|
57
|
+
if water:
|
|
58
|
+
values.append(
|
|
59
|
+
{
|
|
60
|
+
"property": "water_supply",
|
|
61
|
+
"value": "kalt" if water[1] == "Kaltwasser" else "kalt und warm",
|
|
62
|
+
"unit": "",
|
|
63
|
+
"quote": water[0],
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
return values
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def support(text: str, instructions: str, options: list[str]) -> dict[str, Any]:
|
|
70
|
+
"""The mock decision model: the support of each quote, from the sentence that holds it."""
|
|
71
|
+
found = re.search(r'Quote: "(.*)"$', instructions, re.S)
|
|
72
|
+
assert found, instructions
|
|
73
|
+
quote = found[1]
|
|
74
|
+
sentence = next((s for s in re.split(r"(?<=\.)\s+", text) if quote in s), "")
|
|
75
|
+
p = 0.05 if re.search("Zubehör|Bei Einsatz", sentence) else 0.6 if "etwa" in sentence else 0.92
|
|
76
|
+
return {
|
|
77
|
+
"choice": "supported" if p >= 0.5 else "not_supported",
|
|
78
|
+
"confidence": max(p, 1 - p),
|
|
79
|
+
"probabilities": {o: p if o == "supported" else (1 - p) / 2 for o in options},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def fake_for(records: list[dict[str, str]]) -> FakeLbb:
|
|
84
|
+
"""A fake LBB over ``records``, with the mock models."""
|
|
85
|
+
|
|
86
|
+
def rows(query: str) -> list[dict[str, str]]:
|
|
87
|
+
return [
|
|
88
|
+
{"s": iri_of(r["name"]), "label": r["name"], "text": r["text"]}
|
|
89
|
+
for r in records
|
|
90
|
+
if iri_of(r["name"]) in query
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
def reason(body: dict[str, Any]) -> dict[str, Any]:
|
|
94
|
+
return {"values": read_values(json.loads(body["prompt"])["text"])}
|
|
95
|
+
|
|
96
|
+
def decide(body: dict[str, Any]) -> dict[str, Any]:
|
|
97
|
+
text = body["state"]["text"]
|
|
98
|
+
return {
|
|
99
|
+
qid: support(text, q["instructions"], list(q["criteria"]))
|
|
100
|
+
for qid, q in body["questions"].items()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return FakeLbb(rows, reason=reason, decide=decide)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run_samples() -> FakeLbb:
|
|
107
|
+
"""The workflow on one message with every sample record."""
|
|
108
|
+
fake = fake_for(RECORDS)
|
|
109
|
+
fake.queue(message([iri_of(r["name"]) for r in RECORDS]))
|
|
110
|
+
serve(fake)
|
|
111
|
+
return fake
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def matches(got: Any, want: dict[str, Any]) -> bool:
|
|
115
|
+
"""A number compares as a number; a text in lower case, trimmed, against ``accept``."""
|
|
116
|
+
if isinstance(want["value"], (int, float)):
|
|
117
|
+
return bool(got == want["value"])
|
|
118
|
+
accepted = want.get("accept") or [str(want["value"]).lower()]
|
|
119
|
+
return str(got).strip().lower() in accepted
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_the_samples_give_the_values_of_the_answer_key() -> None:
|
|
123
|
+
fake = run_samples()
|
|
124
|
+
for record in EXPECTED["records"]:
|
|
125
|
+
values = fake.written(record["name"])
|
|
126
|
+
for want in record["values"]:
|
|
127
|
+
assert matches(values.get(want["property"]), want), (record["name"], want)
|
|
128
|
+
for out in record["leave_out"]:
|
|
129
|
+
if out["value"] is None:
|
|
130
|
+
assert out["property"] not in values, (record["name"], out)
|
|
131
|
+
else:
|
|
132
|
+
assert not matches(values.get(out["property"]), out), (record["name"], out)
|
|
133
|
+
assert sorted(values) == sorted(v["property"] for v in record["values"]), record["name"]
|
|
134
|
+
(done,) = fake.completed
|
|
135
|
+
state = done["state"]
|
|
136
|
+
assert state["instances"] == len(RECORDS)
|
|
137
|
+
assert state["written"] == sum(len(r["values"]) for r in EXPECTED["records"])
|
|
138
|
+
assert state["failed"] == 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_each_value_has_a_source_whose_evidence_is_a_quote_of_the_text() -> None:
|
|
142
|
+
fake = run_samples()
|
|
143
|
+
texts = {r["name"]: r["text"] for r in RECORDS}
|
|
144
|
+
edges = [edge for commit in fake.commits.values() for edge in commit.get("triplets", [])]
|
|
145
|
+
assert len(edges) == sum(len(r["values"]) for r in EXPECTED["records"])
|
|
146
|
+
for edge in edges:
|
|
147
|
+
assert edge["relation"] == "HAS_VALUE_SOURCE"
|
|
148
|
+
assert edge["target"]["type"] == "ValueSource"
|
|
149
|
+
assert edge["evidence"]["text"] in texts[edge["source"]["name"]]
|
|
150
|
+
assert edge["confidence"] >= DEFAULTS.gate
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# values-extract (Python)
|
|
2
|
+
|
|
3
|
+
The Python port of the workflow template `values-extract`
|
|
4
|
+
(`sdk/templates/values-extract` in the LBB repository). It fills properties of
|
|
5
|
+
a class from the text of its instances. Each value comes with the words of the
|
|
6
|
+
text that state it: `width_cm` 60 and `depth_cm` 40 from `ca. 60x40 cm`.
|
|
7
|
+
|
|
8
|
+
The rules are those of the TypeScript template and of LBB's own
|
|
9
|
+
`values.extract@v1`: the same prompts, checks, review items and counters. On
|
|
10
|
+
the samples with the mock models of its test, the port writes the values of
|
|
11
|
+
the answer key (`samples/expected.json`).
|
|
12
|
+
|
|
13
|
+
A value between 0.5 and the gate becomes a `value` review item, one per
|
|
14
|
+
property of an instance, under LBB's key (`value_item_key`). A person's
|
|
15
|
+
answer wins over the model, for the evidence it was given on
|
|
16
|
+
(`evidence_of`: the quote and the unit the text states). An accepted or
|
|
17
|
+
corrected value is written as answered; a rejected one, or one the graph
|
|
18
|
+
refused, is not written again. New evidence with another value asks again,
|
|
19
|
+
and the answered value and its source link stay until a new answer. A value
|
|
20
|
+
an earlier version filed as a suggestion (`legacy_value_key`) keeps working.
|
|
21
|
+
|
|
22
|
+
## Run it
|
|
23
|
+
|
|
24
|
+
From `sdk/python/examples`, with the Python SDK installed
|
|
25
|
+
(`pip install littlebigbrain`):
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
LBB_URL=http://127.0.0.1:7400 LBB_API_KEY=lbb_sk_test_local python -m values_extract try
|
|
29
|
+
LBB_URL=http://127.0.0.1:7400 LBB_API_KEY=lbb_sk_test_local LBB_GRAPH=try-values-extract python -m values_extract start
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Command | Does |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `python -m values_extract try` | Creates a graph (`LBB_GRAPH`, default `try-values-extract`), puts the six sample records on it, and creates the workflow there |
|
|
35
|
+
| `python -m values_extract setup` | Creates the workflow on your graph (`LBB_GRAPH`, default `main`) |
|
|
36
|
+
| `python -m values_extract start` | Serves the workflow until SIGTERM or Ctrl-C. `LBB_WORKER_ID` names the worker, `LBB_CONCURRENCY` sets how many turns run at once |
|
|
37
|
+
|
|
38
|
+
In the LBB repository, `npm run dev:stack -- --worker python:values-extract`
|
|
39
|
+
starts a local stack with mock models and runs `try` and `start` on it.
|
|
40
|
+
|
|
41
|
+
## The files
|
|
42
|
+
|
|
43
|
+
| File | Holds |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `workflow.py` | The settings, the trigger, the ontology, the checks and the workflow |
|
|
46
|
+
| `samples.py` | The loader of the samples |
|
|
47
|
+
| `samples/` | The sample records and their answer key, the template's |
|
|
48
|
+
| `__main__.py` | The commands |
|
|
49
|
+
|
|
50
|
+
The test of the port is `tests/test_values_extract_example.py` of the Python
|
|
51
|
+
SDK. It runs the workflow with the real worker against a fake LBB, and checks
|
|
52
|
+
that a worker stopped during its write replays its steps and writes once.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""The commands of values-extract, as the TypeScript template project has
|
|
2
|
+
them:
|
|
3
|
+
|
|
4
|
+
python -m values_extract try # a graph with the samples and the workflow
|
|
5
|
+
python -m values_extract setup # the workflow on your graph
|
|
6
|
+
python -m values_extract start # serve it: claim and run its turns
|
|
7
|
+
|
|
8
|
+
They read ``LBB_URL`` (your stack's endpoint, default
|
|
9
|
+
``http://127.0.0.1:7400``), ``LBB_API_KEY`` (a key of the stack) and
|
|
10
|
+
``LBB_GRAPH`` (``try`` uses ``try-values-extract`` by default, the others
|
|
11
|
+
``main``). ``start`` also reads ``LBB_WORKER_ID`` and ``LBB_CONCURRENCY``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from lbb import AsyncLbbClient, LbbError
|
|
21
|
+
from lbb.workflows import WorkflowWorker, lbb_activities
|
|
22
|
+
|
|
23
|
+
from .samples import load_samples
|
|
24
|
+
from .workflow import create_workflow, ontology, trigger
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def client(default_graph: str) -> AsyncLbbClient:
|
|
28
|
+
return AsyncLbbClient(
|
|
29
|
+
os.environ.get("LBB_URL", "http://127.0.0.1:7400"),
|
|
30
|
+
api_key=os.environ.get("LBB_API_KEY"),
|
|
31
|
+
graph=os.environ.get("LBB_GRAPH", default_graph),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def try_samples() -> None:
|
|
36
|
+
"""Create a graph, put the samples on it, and create the workflow there.
|
|
37
|
+
Then serve it with ``start`` and the same ``LBB_GRAPH``."""
|
|
38
|
+
async with client("try-values-extract") as lbb_client:
|
|
39
|
+
graph = lbb_client._graph
|
|
40
|
+
try:
|
|
41
|
+
await lbb_client.create_graph()
|
|
42
|
+
print(f"Created the graph {graph}")
|
|
43
|
+
except LbbError as error:
|
|
44
|
+
# 409: the graph exists already.
|
|
45
|
+
if error.status_code != 409:
|
|
46
|
+
raise
|
|
47
|
+
count = await load_samples(lbb_client)
|
|
48
|
+
print(f"Committed {count} sample records")
|
|
49
|
+
created = await lbb_client.use_workflow_starter(**trigger())
|
|
50
|
+
print(f"Created {created['name']} on the graph {graph}: {created['status']['state']}.")
|
|
51
|
+
print(f"Serve it with LBB_GRAPH={graph} python -m values_extract start")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def set_up() -> None:
|
|
55
|
+
"""Add the classes and properties the workflow writes, then create the
|
|
56
|
+
triggered workflow. Each call is safe to repeat."""
|
|
57
|
+
async with client("main") as lbb_client:
|
|
58
|
+
await lbb_activities(lbb_client).evolve(ontology())
|
|
59
|
+
created = await lbb_client.use_workflow_starter(**trigger())
|
|
60
|
+
graph = lbb_client._graph
|
|
61
|
+
print(f"Created {created['name']} on the graph {graph}: {created['status']['state']}.")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def start() -> None:
|
|
65
|
+
"""Serve the workflow until SIGTERM or Ctrl-C."""
|
|
66
|
+
lbb_client = client("main")
|
|
67
|
+
workflow = create_workflow(lbb_activities(lbb_client))
|
|
68
|
+
worker = WorkflowWorker(
|
|
69
|
+
lbb_client,
|
|
70
|
+
[workflow],
|
|
71
|
+
worker=os.environ.get("LBB_WORKER_ID", f"{workflow.name}-py-1"),
|
|
72
|
+
concurrency=int(os.environ.get("LBB_CONCURRENCY", "1")),
|
|
73
|
+
)
|
|
74
|
+
print(
|
|
75
|
+
f"Serving {workflow.name}@{workflow.version} for the graph {lbb_client._graph}. "
|
|
76
|
+
"Stop with Ctrl-C.",
|
|
77
|
+
flush=True,
|
|
78
|
+
)
|
|
79
|
+
worker.run()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main(argv: list[str]) -> int:
|
|
83
|
+
command = argv[0] if argv else ""
|
|
84
|
+
if command == "try":
|
|
85
|
+
asyncio.run(try_samples())
|
|
86
|
+
elif command == "setup":
|
|
87
|
+
asyncio.run(set_up())
|
|
88
|
+
elif command == "start":
|
|
89
|
+
start()
|
|
90
|
+
else:
|
|
91
|
+
print(__doc__)
|
|
92
|
+
return 2
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": [
|
|
3
|
+
"The answer key for the values-extract samples: the values a correct run reads from each record, with the quote that supports each, and the values it must leave out.",
|
|
4
|
+
"All text is invented. make-samples.mjs checks that every quote is in its record's text (whitespace runs as one space) and that a number's quote holds the number.",
|
|
5
|
+
"Numbers compare as numbers. A text value compares in lower case, trimmed, against accept. A run may quote more or less of the text, as long as its quote is in the text and holds the evidence.",
|
|
6
|
+
"leave_out lists values a correct run does not write. With value null, the run writes no value for that property at all."
|
|
7
|
+
],
|
|
8
|
+
"install": {
|
|
9
|
+
"properties": {
|
|
10
|
+
"width_cm": "f64",
|
|
11
|
+
"depth_cm": "f64",
|
|
12
|
+
"material": "text",
|
|
13
|
+
"water_supply": "keyword"
|
|
14
|
+
},
|
|
15
|
+
"units": { "width_cm": "cm", "depth_cm": "cm" },
|
|
16
|
+
"context": "Entries of the sanitary equipment list of a school building tender, in German."
|
|
17
|
+
},
|
|
18
|
+
"records": [
|
|
19
|
+
{
|
|
20
|
+
"name": "S 4.1",
|
|
21
|
+
"case": "An approximate size (ca.) is still the item's size.",
|
|
22
|
+
"values": [
|
|
23
|
+
{ "property": "width_cm", "value": 60, "quote": "ca. 60x40 cm" },
|
|
24
|
+
{ "property": "depth_cm", "value": 40, "quote": "ca. 60x40 cm" },
|
|
25
|
+
{
|
|
26
|
+
"property": "material",
|
|
27
|
+
"value": "Sanitärkeramik",
|
|
28
|
+
"accept": ["sanitärkeramik", "keramik", "sanitärkeramik, weiß", "sanitärkeramik weiß"],
|
|
29
|
+
"quote": "aus Sanitärkeramik, weiß"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"property": "water_supply",
|
|
33
|
+
"value": "kalt und warm",
|
|
34
|
+
"accept": ["kalt und warm", "warm und kalt", "kalt- und warmwasser", "kaltwasser und warmwasser"],
|
|
35
|
+
"quote": "Anschluss an Kalt- und Warmwasser"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"leave_out": []
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "S 4.2",
|
|
42
|
+
"case": "An accessory of another material: the Klapprost.",
|
|
43
|
+
"values": [
|
|
44
|
+
{ "property": "width_cm", "value": 50, "quote": "Stahl emailliert, 50x35 cm" },
|
|
45
|
+
{ "property": "depth_cm", "value": 35, "quote": "Stahl emailliert, 50x35 cm" },
|
|
46
|
+
{
|
|
47
|
+
"property": "material",
|
|
48
|
+
"value": "Stahl emailliert",
|
|
49
|
+
"accept": ["stahl emailliert", "stahl, emailliert", "emaillierter stahl"],
|
|
50
|
+
"quote": "Ausgussbecken für Putzräume, Stahl emailliert"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"property": "water_supply",
|
|
54
|
+
"value": "kalt",
|
|
55
|
+
"accept": ["kalt", "kaltwasser", "nur kaltwasser", "nur kalt"],
|
|
56
|
+
"quote": "Anschluss nur an Kaltwasser"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"leave_out": [
|
|
60
|
+
{ "property": "material", "value": "Edelstahl", "reason": "Edelstahl is the material of the Klapprost, an accessory." }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "S 4.3",
|
|
65
|
+
"case": "The only size in the text belongs to an accessory, the Glasablage.",
|
|
66
|
+
"values": [
|
|
67
|
+
{
|
|
68
|
+
"property": "material",
|
|
69
|
+
"value": "Mineralguss",
|
|
70
|
+
"accept": ["mineralguss"],
|
|
71
|
+
"quote": "Waschtisch, unterfahrbar, aus Mineralguss"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"property": "water_supply",
|
|
75
|
+
"value": "kalt und warm",
|
|
76
|
+
"accept": ["kalt und warm", "warm und kalt", "kalt- und warmwasser", "kaltwasser und warmwasser"],
|
|
77
|
+
"quote": "Anschluss an Kalt- und Warmwasser"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"leave_out": [
|
|
81
|
+
{ "property": "width_cm", "value": null, "reason": "50x12 cm is the size of the Glasablage. The basin's size is left to the maker (Maße nach Hersteller)." },
|
|
82
|
+
{ "property": "depth_cm", "value": null, "reason": "50x12 cm is the size of the Glasablage. The basin's size is left to the maker (Maße nach Hersteller)." },
|
|
83
|
+
{ "property": "material", "value": "Glas", "reason": "Glas is the material of the Glasablage, an accessory." },
|
|
84
|
+
{ "property": "material", "value": "Edelstahl", "reason": "Edelstahl is the material of the Haltegriff, an accessory." }
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "S 4.4",
|
|
89
|
+
"case": "Every value stated once, plainly.",
|
|
90
|
+
"values": [
|
|
91
|
+
{ "property": "width_cm", "value": 40, "quote": "Edelstahl, 40x30 cm" },
|
|
92
|
+
{ "property": "depth_cm", "value": 30, "quote": "Edelstahl, 40x30 cm" },
|
|
93
|
+
{
|
|
94
|
+
"property": "material",
|
|
95
|
+
"value": "Edelstahl",
|
|
96
|
+
"accept": ["edelstahl"],
|
|
97
|
+
"quote": "Trinkbrunnen für den Flur, Edelstahl"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"property": "water_supply",
|
|
101
|
+
"value": "kalt",
|
|
102
|
+
"accept": ["kalt", "kaltwasser", "nur kaltwasser", "nur kalt"],
|
|
103
|
+
"quote": "Anschluss an Kaltwasser in Trinkwasserqualität"
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"leave_out": []
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "S 4.5",
|
|
110
|
+
"case": "A variant that holds only under a condition (Bei Einsatz von Geschirrspülern).",
|
|
111
|
+
"values": [
|
|
112
|
+
{ "property": "width_cm", "value": 100, "quote": "Edelstahl, 100x50 cm" },
|
|
113
|
+
{ "property": "depth_cm", "value": 50, "quote": "Edelstahl, 100x50 cm" },
|
|
114
|
+
{
|
|
115
|
+
"property": "material",
|
|
116
|
+
"value": "Edelstahl",
|
|
117
|
+
"accept": ["edelstahl"],
|
|
118
|
+
"quote": "Spüle für die Lehrerküche, Edelstahl"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"property": "water_supply",
|
|
122
|
+
"value": "kalt und warm",
|
|
123
|
+
"accept": ["kalt und warm", "warm und kalt", "kalt- und warmwasser", "kaltwasser und warmwasser"],
|
|
124
|
+
"quote": "Anschluss an Kalt- und Warmwasser"
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"leave_out": [
|
|
128
|
+
{ "property": "width_cm", "value": 120, "reason": "The width of the variant with two basins, which holds only bei Einsatz von Geschirrspülern." }
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "S 4.6",
|
|
133
|
+
"case": "A second size that is a height, and no water supply.",
|
|
134
|
+
"values": [
|
|
135
|
+
{ "property": "width_cm", "value": 60, "quote": "Breite 60 cm" },
|
|
136
|
+
{
|
|
137
|
+
"property": "material",
|
|
138
|
+
"value": "Kristallglas",
|
|
139
|
+
"accept": ["kristallglas", "glas"],
|
|
140
|
+
"quote": "Spiegel für Waschplätze, Kristallglas"
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
"leave_out": [
|
|
144
|
+
{ "property": "depth_cm", "value": null, "reason": "45 cm is the height (Höhe). The text gives no depth." },
|
|
145
|
+
{ "property": "water_supply", "value": null, "reason": "The text names no water supply." }
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "S 4.1",
|
|
4
|
+
"text": "S 4.1 Waschtisch für Klassenräume, aus Sanitärkeramik, weiß, ca. 60x40 cm, mit einem Hahnloch. Anschluss an Kalt- und Warmwasser über Einhebelmischer."
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"name": "S 4.2",
|
|
8
|
+
"text": "S 4.2 Ausgussbecken für Putzräume, Stahl emailliert, 50x35 cm. Zubehör: Klapprost aus Edelstahl. Anschluss nur an Kaltwasser."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "S 4.3",
|
|
12
|
+
"text": "S 4.3 Waschtisch, unterfahrbar, aus Mineralguss, Maße nach Hersteller. Zubehör: Glasablage 50x12 cm und Haltegriff aus Edelstahl. Anschluss an Kalt- und Warmwasser."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "S 4.4",
|
|
16
|
+
"text": "S 4.4 Trinkbrunnen für den Flur, Edelstahl, 40x30 cm, mit Druckknopfventil. Anschluss an Kaltwasser in Trinkwasserqualität."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "S 4.5",
|
|
20
|
+
"text": "S 4.5 Spüle für die Lehrerküche, Edelstahl, 100x50 cm, mit einem Becken und Abtropffläche. Anschluss an Kalt- und Warmwasser. Bei Einsatz von Geschirrspülern: Ausführung mit zwei Becken, 120x50 cm."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "S 4.6",
|
|
24
|
+
"text": "S 4.6 Spiegel für Waschplätze, Kristallglas, Kanten poliert, Breite 60 cm, Höhe 45 cm."
|
|
25
|
+
}
|
|
26
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""The samples of values-extract, for ``python -m values_extract try``: six
|
|
2
|
+
invented entries of a sanitary equipment list (``samples/records.json``),
|
|
3
|
+
committed as instances of the class the settings name, each with its text.
|
|
4
|
+
The same samples as the TypeScript template's."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from lbb import AsyncLbbClient
|
|
12
|
+
from lbb.workflows import lbb_activities
|
|
13
|
+
|
|
14
|
+
from .workflow import DEFAULTS, ontology
|
|
15
|
+
|
|
16
|
+
SAMPLES = Path(__file__).resolve().parent / "samples"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def load_samples(client: AsyncLbbClient, folder: Path = SAMPLES) -> int:
|
|
20
|
+
"""Add the class and its properties, then commit the sample records to the
|
|
21
|
+
client's graph. The commit's idempotency key makes a second load a replay."""
|
|
22
|
+
records = json.loads((folder / "records.json").read_text(encoding="utf-8"))
|
|
23
|
+
await lbb_activities(client).evolve(ontology())
|
|
24
|
+
await client.commit(
|
|
25
|
+
{
|
|
26
|
+
"entity_properties": [
|
|
27
|
+
{
|
|
28
|
+
"type": DEFAULTS.class_name,
|
|
29
|
+
"name": record["name"],
|
|
30
|
+
"properties": {DEFAULTS.text_field: record["text"]},
|
|
31
|
+
}
|
|
32
|
+
for record in records
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
idempotency_key="values-extract-samples",
|
|
36
|
+
)
|
|
37
|
+
return len(records)
|