@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.
Files changed (33) hide show
  1. package/README.md +42 -6
  2. package/lib/create.mjs +228 -14
  3. package/package.json +2 -1
  4. package/project/python/values_extract/tests/fake_lbb.py +226 -0
  5. package/project/python/values_extract/tests/test_workflow.py +150 -0
  6. package/python/values_extract/README.md +52 -0
  7. package/python/values_extract/__init__.py +3 -0
  8. package/python/values_extract/__main__.py +97 -0
  9. package/python/values_extract/samples/expected.json +149 -0
  10. package/python/values_extract/samples/records.json +26 -0
  11. package/python/values_extract/samples.py +37 -0
  12. package/python/values_extract/workflow.py +1044 -0
  13. package/python/versions.json +3 -0
  14. package/templates/_shared/testing.ts +259 -5
  15. package/templates/ontology-fit/README.md +41 -32
  16. package/templates/ontology-fit/workflow.test.ts +56 -5
  17. package/templates/ontology-fit/workflow.ts +708 -207
  18. package/templates/review/README.md +56 -29
  19. package/templates/review/decision.ts +118 -37
  20. package/templates/review/workflow.test.ts +30 -10
  21. package/templates/review/workflow.ts +123 -76
  22. package/templates/rows/README.md +8 -1
  23. package/templates/rows/workflow.test.ts +56 -0
  24. package/templates/rows/workflow.ts +182 -148
  25. package/templates/sections/README.md +8 -1
  26. package/templates/sections/workflow.test.ts +51 -1
  27. package/templates/sections/workflow.ts +137 -98
  28. package/templates/values-extract/README.md +92 -23
  29. package/templates/values-extract/samples/expected.json +1 -0
  30. package/templates/values-extract/test-examples.ts +155 -0
  31. package/templates/values-extract/workflow.test.ts +532 -27
  32. package/templates/values-extract/workflow.ts +719 -179
  33. 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,3 @@
1
+ """values-extract: the Python port of the workflow template
2
+ ``sdk/templates/values-extract``. Run it with ``python -m values_extract``
3
+ from the folder that holds this package (see the README)."""
@@ -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)