Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-prompt/src/lexigram/ai/prompt/rendering/engine.py: 43%

61 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 07:19 +0800

1"""Prompt rendering engine — f-string (default) with optional Jinja2 support.""" 

2 

3from __future__ import annotations 

4 

5from enum import StrEnum 

6import re 

7from string import Template 

8from typing import Any 

9 

10from lexigram.ai.prompt.exceptions import PromptRenderError 

11 

12 

13class RenderFormat(StrEnum): 

14 """Supported template substitution formats.""" 

15 

16 F_STRING = "f_string" 

17 """``{variable_name}`` style (default).""" 

18 JINJA2 = "jinja2" 

19 """Jinja2 ``{{ variable_name }}`` style (requires ``jinja2`` extra).""" 

20 DOLLAR = "dollar" 

21 """``$variable_name`` / ``${variable_name}`` style (stdlib ``string.Template``).""" 

22 SIMPLE = "simple" 

23 """Literal template — no substitution performed.""" 

24 

25 

26class PromptRenderer: 

27 """Renders a template string by substituting named variables. 

28 

29 Supports three formats controlled by :class:`RenderFormat`. 

30 

31 Args: 

32 format: Substitution format. Defaults to :attr:`RenderFormat.F_STRING`. 

33 """ 

34 

35 def __init__(self, format: RenderFormat = RenderFormat.F_STRING) -> None: 

36 self._format = format 

37 

38 @property 

39 def format(self) -> RenderFormat: 

40 """The active rendering format.""" 

41 return self._format 

42 

43 def render(self, template: str, variables: dict[str, Any]) -> str: 

44 """Substitute *variables* into *template*. 

45 

46 Args: 

47 template: Raw template string. 

48 variables: Variable name → value mapping. 

49 

50 Returns: 

51 Rendered string. 

52 

53 Raises: 

54 :class:`~lexigram.ai.prompt.exceptions.PromptRenderError`: 

55 A required placeholder has no matching variable. 

56 """ 

57 try: 

58 if self._format == RenderFormat.SIMPLE: 

59 return template 

60 if self._format == RenderFormat.JINJA2: 

61 return self._render_jinja2(template, variables) 

62 if self._format == RenderFormat.DOLLAR: 

63 return Template(template).substitute(variables) 

64 # F_STRING (default) 

65 return template.format_map(_DefaultFormatMap(variables)) 

66 except PromptRenderError: 

67 raise 

68 except KeyError as exc: 

69 raise PromptRenderError(f"Missing variable {exc} in template.") from exc 

70 except ValueError as exc: 

71 raise PromptRenderError(str(exc)) from exc 

72 

73 @staticmethod 

74 def _render_jinja2(template: str, variables: dict[str, Any]) -> str: 

75 """Render a Jinja2 template string.""" 

76 try: 

77 from jinja2 import StrictUndefined 

78 from jinja2 import Template as Jinja2Template 

79 from jinja2.exceptions import TemplateError 

80 except ImportError as exc: 

81 raise PromptRenderError( 

82 "Jinja2 is not installed. Add the 'jinja2' extra: " 

83 "pip install lexigram-ai-platform[jinja2]" 

84 ) from exc 

85 try: 

86 tmpl = Jinja2Template(template, undefined=StrictUndefined) 

87 return tmpl.render(**variables) 

88 except TemplateError as exc: 

89 raise PromptRenderError(str(exc)) from exc 

90 

91 def get_variables(self, template: str) -> list[str]: 

92 """Extract placeholder names from *template*. 

93 

94 Args: 

95 template: Raw template string. 

96 

97 Returns: 

98 Unique list of variable names detected in the template. 

99 """ 

100 if self._format == RenderFormat.SIMPLE: 

101 return [] 

102 if self._format == RenderFormat.JINJA2: 

103 # Find {{ var }} style (simple names only — not expressions) 

104 return list(dict.fromkeys(re.findall(r"\{\{\s*(\w+)\s*\}\}", template))) 

105 if self._format == RenderFormat.DOLLAR: 

106 return list(dict.fromkeys(re.findall(r"\$\{?(\w+)\}?", template))) 

107 # F_STRING 

108 return list(dict.fromkeys(re.findall(r"\{(\w+)\}", template))) 

109 

110 

111class _DefaultFormatMap(dict): 

112 """``str.format_map`` helper that raises ``KeyError`` for missing keys.""" 

113 

114 def __missing__(self, key: str) -> str: 

115 raise KeyError(key) 

116 

117 

118__all__ = ["PromptRenderer", "RenderFormat"]