Coverage for mcp_chatkit_widget / rendering.py: 100%

31 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-11 06:37 +0000

1"""Schema parsing and template rendering helpers.""" 

2 

3from __future__ import annotations 

4import json 

5from functools import cache 

6from typing import Any 

7from chatkit.widgets import WidgetRoot, WidgetTemplate 

8from pydantic import BaseModel 

9from mcp_chatkit_widget.naming import _sanitize_tool_name, _to_camel_case 

10from mcp_chatkit_widget.pydantic_conversion import json_schema_to_pydantic 

11from mcp_chatkit_widget.widget_loader import WidgetDefinition 

12 

13 

14def _model_metadata(widget_name: str) -> tuple[str, str]: 

15 """Return consistent Pydantic metadata derived from a widget name. 

16 

17 Args: 

18 widget_name: The widget's identifier, which may include unsafe characters. 

19 

20 Returns: 

21 A tuple containing the generated model class name and schema title that are 

22 safe for Pydantic and tooling (e.g., ``FooBarModel`` and ``FooBarArguments``). 

23 """ 

24 camel_name = _to_camel_case(_sanitize_tool_name(widget_name)) 

25 return f"{camel_name}Model", f"{camel_name}Arguments" 

26 

27 

28@cache 

29def _build_pydantic_model(schema_dump: str, widget_name: str) -> type[BaseModel]: 

30 """Return a cached Pydantic model class for the given schema snapshot. 

31 

32 Args: 

33 schema_dump: A JSON string representation of the widget's schema that is 

34 already normalized (e.g., via ``json.dumps(..., sort_keys=True)``) 

35 so hashing remains stable across calls. 

36 widget_name: The name used to generate consistent model metadata for the 

37 schema (see :func:`_model_metadata`). 

38 

39 Returns: 

40 The generated :class:`pydantic.BaseModel` subclass that enforces the schema. 

41 """ 

42 schema = json.loads(schema_dump) 

43 model_name, schema_title = _model_metadata(widget_name) 

44 return json_schema_to_pydantic(schema, model_name, schema_title) 

45 

46 

47def build_widget_model(widget_def: WidgetDefinition) -> type[BaseModel]: 

48 """Convert a widget definition's JSON schema into a Pydantic model.""" 

49 schema_dump = json.dumps(widget_def.json_schema, sort_keys=True) 

50 return _build_pydantic_model(schema_dump, widget_def.name) 

51 

52 

53@cache 

54def _load_widget_template(template_path: str) -> WidgetTemplate: 

55 """Load and cache a WidgetTemplate from disk.""" 

56 return WidgetTemplate.from_file(template_path) 

57 

58 

59def render_widget_definition(widget_def: WidgetDefinition, **kwargs: Any) -> WidgetRoot: 

60 """Validate inputs, render the widget's template, and return a WidgetRoot.""" 

61 model = build_widget_model(widget_def) 

62 validated = model(**kwargs) 

63 template = _load_widget_template(str(widget_def.file_path)) 

64 render_context = validated.model_dump() 

65 render_context["undefined"] = None 

66 return template.build(render_context) 

67 

68 

69__all__ = [ 

70 "build_widget_model", 

71 "render_widget_definition", 

72]