Skip to content

Commit 5f0a91a

Browse files
authored
Merge pull request #2096 from tisnik/lcore-2755-model-dumper
LCORE-2755: Model dumper
2 parents 6323e67 + 3794f77 commit 5f0a91a

4 files changed

Lines changed: 165 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,7 @@ options:
858858
-d, --dump-configuration
859859
dump actual configuration into JSON file and quit
860860
-s, --dump-schema dump configuration schema into OpenAPI-compatible file and quit
861+
-m, --dump-models dump schemas for all models into OpenAPI-compatible file and quit
861862
-c, --config CONFIG_FILE
862863
path to configuration file (default: lightspeed-stack.yaml)
863864
--synthesized-config-output SYNTHESIZED_CONFIG_OUTPUT

src/lightspeed_stack.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from log import get_logger, setup_logging
1515
from runners.quota_scheduler import start_quota_scheduler
1616
from runners.uvicorn import start_uvicorn
17-
from utils import schema_dumper
17+
from utils import models_dumper, schema_dumper
1818

1919
setup_logging()
2020
logger = get_logger(__name__)
@@ -61,6 +61,14 @@ def create_argument_parser() -> ArgumentParser:
6161
action="store_true",
6262
default=False,
6363
)
64+
parser.add_argument(
65+
"-m",
66+
"--dump-models",
67+
dest="dump_models",
68+
help="dump schemas for all models into OpenAPI-compatible file and quit",
69+
action="store_true",
70+
default=False,
71+
)
6472
parser.add_argument(
6573
"-c",
6674
"--config",
@@ -105,6 +113,7 @@ def create_argument_parser() -> ArgumentParser:
105113
return parser
106114

107115

116+
# pylint: disable=too-many-branches, too-many-statements
108117
def main() -> None:
109118
"""Entry point to the web service.
110119
@@ -193,6 +202,17 @@ def main() -> None:
193202
raise SystemExit(1) from e
194203
return
195204

205+
# -m or --dump-models CLI flags are used to dump schema for all models
206+
# into a JSON file that is compatible with OpenAPI schema specification
207+
if args.dump_models:
208+
try:
209+
models_dumper.dump_models("models.json")
210+
logger.info("Schema for all models dumped to models.json")
211+
except Exception as e:
212+
logger.error("Failed to dump schema for models: %s", e)
213+
raise SystemExit(1) from e
214+
return
215+
196216
# Store config path in env so each uvicorn worker can load it
197217
# (step is needed because process context isn't shared).
198218
os.environ[constants.CONFIG_PATH_ENV_VAR] = args.config_file

src/utils/models_dumper.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Function to dump the schema of all data models into OpenAPI-compatible format."""
2+
3+
import json
4+
5+
from pydantic.json_schema import models_json_schema
6+
7+
import models.compaction as models_compaction
8+
from utils.json_schema_updater import recursive_update
9+
10+
11+
def dump_models(filename: str) -> None:
12+
"""Dump the schema of all models into OpenAPI-compatible JSON file.
13+
14+
Parameters:
15+
----------
16+
- filename: str - name of file to export the schema to
17+
18+
Returns:
19+
-------
20+
- None
21+
22+
Raises:
23+
------
24+
IOError: If the file cannot be written.
25+
"""
26+
with open(filename, "w", encoding="utf-8") as fout:
27+
# retrieve the schema
28+
_, schemas = models_json_schema(
29+
[
30+
(model, "validation")
31+
for model in [models_compaction.ConversationSummary]
32+
],
33+
ref_template="#/components/schemas/{model}",
34+
)
35+
36+
# fix the schema
37+
schemas = recursive_update(schemas)
38+
39+
# add all required metadata
40+
openapi_schema = {
41+
"openapi": "3.0.0",
42+
"info": {
43+
"title": "Lightspeed Core Stack",
44+
"version": "0.3.0",
45+
},
46+
"components": {
47+
"schemas": schemas.get("$defs", {}),
48+
},
49+
"paths": {},
50+
}
51+
json.dump(openapi_schema, fout, indent=4)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Unit tests for utils/models_dumper module."""
2+
3+
from json import load
4+
from pathlib import Path
5+
6+
from utils.models_dumper import dump_models
7+
8+
9+
def test_dump_models(tmpdir: Path) -> None:
10+
"""Test that models can be dump into a JSON file.
11+
12+
An example of schema dump:
13+
{
14+
"openapi": "3.0.0",
15+
"info": {
16+
"title": "Lightspeed Core Stack",
17+
"version": "0.3.0"
18+
},
19+
"components": {
20+
"schemas": {
21+
"ConversationSummary": {
22+
"description": "A single compaction-produced summary chunk.\n\nAttributes:\n",
23+
"properties": {
24+
"summary_text": {
25+
"description": "Natural-language summary produced by the ...",
26+
"title": "Summary text",
27+
"type": "string"
28+
},
29+
"summarized_through_turn": {
30+
"description": "Running total of conversation items consumed by this ...",
31+
"minimum": 0,
32+
"title": "Summarized through turn",
33+
"type": "integer"
34+
},
35+
"token_count": {
36+
"description": "Number of tokens in summary_text.",
37+
"minimum": 0,
38+
"title": "Token count",
39+
"type": "integer"
40+
},
41+
"created_at": {
42+
"description": "ISO 8601 timestamp recording when this summary ...",
43+
"title": "Created at",
44+
"type": "string"
45+
},
46+
"model_used": {
47+
"description": "Fully-qualified model identifier used for the ...",
48+
"title": "Model used",
49+
"type": "string"
50+
}
51+
},
52+
"required": [
53+
"summary_text",
54+
"summarized_through_turn",
55+
"token_count",
56+
"created_at",
57+
"model_used"
58+
],
59+
"title": "ConversationSummary",
60+
"type": "object"
61+
}
62+
}
63+
},
64+
"paths": {}
65+
}
66+
"""
67+
filename = tmpdir / "foo.json"
68+
dump_models(str(filename))
69+
70+
with open(filename, "r", encoding="utf-8") as fin:
71+
# schema should be stored in JSON format
72+
content = load(fin)
73+
assert content is not None
74+
75+
# top-level keys test
76+
keys = ("openapi", "info", "components", "paths")
77+
for key in keys:
78+
assert key in content
79+
80+
# components should be top-level node
81+
components = content["components"]
82+
assert components is not None
83+
84+
# schemas should be a node stored inside components node
85+
assert "schemas" in components
86+
schemas = components["schemas"]
87+
assert schemas is not None
88+
89+
# list of schemas expected in a dump
90+
expected_schemas = ("ConversationSummary",)
91+
for expected_schema in expected_schemas:
92+
assert expected_schema in schemas

0 commit comments

Comments
 (0)