metaobjects 0.9.0__py3-none-any.whl

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 (181) hide show
  1. metaobjects/__init__.py +75 -0
  2. metaobjects/agent_context/__init__.py +55 -0
  3. metaobjects/agent_context/_content/README.md +14 -0
  4. metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
  5. metaobjects/agent_context/_content/servers/java.meta.json +5 -0
  6. metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
  7. metaobjects/agent_context/_content/servers/python.meta.json +5 -0
  8. metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
  9. metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
  10. metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
  11. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
  12. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
  13. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
  14. metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
  15. metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
  16. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
  17. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
  18. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
  19. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
  20. metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
  21. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
  22. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
  23. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
  24. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
  25. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
  26. metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
  27. metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
  28. metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
  29. metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
  30. metaobjects/agent_context/assemble.py +133 -0
  31. metaobjects/agent_context/content_root.py +54 -0
  32. metaobjects/agent_context/scaffold.py +191 -0
  33. metaobjects/agent_context/types.py +44 -0
  34. metaobjects/attr_class_map.py +23 -0
  35. metaobjects/cli.py +696 -0
  36. metaobjects/codegen/__init__.py +0 -0
  37. metaobjects/codegen/config.py +11 -0
  38. metaobjects/codegen/constants.py +13 -0
  39. metaobjects/codegen/extract_delegate_emitter.py +384 -0
  40. metaobjects/codegen/extract_schema_emitter.py +139 -0
  41. metaobjects/codegen/format.py +31 -0
  42. metaobjects/codegen/fr010_field_mapping.py +220 -0
  43. metaobjects/codegen/generator.py +62 -0
  44. metaobjects/codegen/generator_registry.py +163 -0
  45. metaobjects/codegen/generators/__init__.py +0 -0
  46. metaobjects/codegen/generators/entity_model.py +263 -0
  47. metaobjects/codegen/generators/extractor_generator.py +317 -0
  48. metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
  49. metaobjects/codegen/generators/m2m_codegen.py +192 -0
  50. metaobjects/codegen/generators/output_parser_generator.py +272 -0
  51. metaobjects/codegen/generators/output_prompt_generator.py +192 -0
  52. metaobjects/codegen/generators/payload_vo_generator.py +672 -0
  53. metaobjects/codegen/generators/render_helper_generator.py +451 -0
  54. metaobjects/codegen/generators/router_generator.py +635 -0
  55. metaobjects/codegen/generators/template_generator.py +70 -0
  56. metaobjects/codegen/generators/tph_plan.py +120 -0
  57. metaobjects/codegen/generators/trace_helper_generator.py +336 -0
  58. metaobjects/codegen/instance_artifacts.py +15 -0
  59. metaobjects/codegen/output_format_spec_emitter.py +79 -0
  60. metaobjects/codegen/overwrite_policy.py +27 -0
  61. metaobjects/codegen/runner.py +110 -0
  62. metaobjects/codegen/runtime/__init__.py +6 -0
  63. metaobjects/codegen/runtime/filter_parser.py +193 -0
  64. metaobjects/codegen/type_map.py +84 -0
  65. metaobjects/core_types.py +809 -0
  66. metaobjects/datatype.py +19 -0
  67. metaobjects/documentation/__init__.py +28 -0
  68. metaobjects/documentation/doc_constants.py +20 -0
  69. metaobjects/documentation/doc_provider.py +20 -0
  70. metaobjects/documentation/doc_schema.py +24 -0
  71. metaobjects/errors.py +124 -0
  72. metaobjects/loader/__init__.py +0 -0
  73. metaobjects/loader/merge.py +287 -0
  74. metaobjects/loader/meta_data_loader.py +245 -0
  75. metaobjects/loader/sources/__init__.py +24 -0
  76. metaobjects/loader/sources/directory_source.py +50 -0
  77. metaobjects/loader/sources/file_source.py +41 -0
  78. metaobjects/loader/sources/meta_data_source.py +67 -0
  79. metaobjects/loader/sources/uri_source.py +56 -0
  80. metaobjects/loader/validate_discriminator.py +181 -0
  81. metaobjects/loader/validate_field_readonly.py +146 -0
  82. metaobjects/loader/validate_source_parameter_ref.py +159 -0
  83. metaobjects/loader/validate_source_physical_names.py +140 -0
  84. metaobjects/loader/validation_passes.py +1513 -0
  85. metaobjects/meta/__init__.py +1 -0
  86. metaobjects/meta/core/__init__.py +0 -0
  87. metaobjects/meta/core/attr/__init__.py +0 -0
  88. metaobjects/meta/core/attr/attr_constants.py +31 -0
  89. metaobjects/meta/core/attr/meta_attr.py +136 -0
  90. metaobjects/meta/core/field/__init__.py +0 -0
  91. metaobjects/meta/core/field/field_constants.py +105 -0
  92. metaobjects/meta/core/field/meta_field.py +76 -0
  93. metaobjects/meta/core/identity/__init__.py +0 -0
  94. metaobjects/meta/core/identity/identity_constants.py +19 -0
  95. metaobjects/meta/core/identity/meta_identity.py +8 -0
  96. metaobjects/meta/core/object/__init__.py +0 -0
  97. metaobjects/meta/core/object/meta_object.py +65 -0
  98. metaobjects/meta/core/object/meta_object_aware.py +43 -0
  99. metaobjects/meta/core/object/object_class_registry.py +56 -0
  100. metaobjects/meta/core/object/object_constants.py +13 -0
  101. metaobjects/meta/core/object/object_extract.py +400 -0
  102. metaobjects/meta/core/object/value_object.py +70 -0
  103. metaobjects/meta/core/relationship/__init__.py +0 -0
  104. metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
  105. metaobjects/meta/core/relationship/meta_relationship.py +54 -0
  106. metaobjects/meta/core/relationship/relationship_constants.py +51 -0
  107. metaobjects/meta/core/validator/__init__.py +0 -0
  108. metaobjects/meta/core/validator/validator_constants.py +18 -0
  109. metaobjects/meta/meta_data.py +206 -0
  110. metaobjects/meta/meta_root.py +8 -0
  111. metaobjects/meta/persistence/__init__.py +0 -0
  112. metaobjects/meta/persistence/db/__init__.py +1 -0
  113. metaobjects/meta/persistence/db/db_constants.py +41 -0
  114. metaobjects/meta/persistence/db/db_provider.py +60 -0
  115. metaobjects/meta/persistence/origin/__init__.py +0 -0
  116. metaobjects/meta/persistence/origin/meta_origin.py +8 -0
  117. metaobjects/meta/persistence/origin/origin_constants.py +20 -0
  118. metaobjects/meta/persistence/source/__init__.py +0 -0
  119. metaobjects/meta/persistence/source/meta_source.py +137 -0
  120. metaobjects/meta/persistence/source/source_constants.py +115 -0
  121. metaobjects/meta/presentation/__init__.py +0 -0
  122. metaobjects/meta/presentation/layout/__init__.py +0 -0
  123. metaobjects/meta/presentation/layout/layout_constants.py +13 -0
  124. metaobjects/meta/presentation/layout/meta_layout.py +8 -0
  125. metaobjects/meta/presentation/view/__init__.py +0 -0
  126. metaobjects/meta/presentation/view/meta_view.py +8 -0
  127. metaobjects/meta/presentation/view/view_constants.py +22 -0
  128. metaobjects/meta/template/__init__.py +0 -0
  129. metaobjects/meta/template/meta_template.py +46 -0
  130. metaobjects/meta/template/template_constants.py +112 -0
  131. metaobjects/meta/template/template_provider.py +43 -0
  132. metaobjects/parser.py +380 -0
  133. metaobjects/parser_yaml.py +82 -0
  134. metaobjects/provider.py +111 -0
  135. metaobjects/py.typed +0 -0
  136. metaobjects/registry.py +210 -0
  137. metaobjects/registry_manifest.py +223 -0
  138. metaobjects/render/__init__.py +74 -0
  139. metaobjects/render/email_document.py +14 -0
  140. metaobjects/render/escapers.py +109 -0
  141. metaobjects/render/extract/__init__.py +59 -0
  142. metaobjects/render/extract/coerce.py +279 -0
  143. metaobjects/render/extract/extract.py +211 -0
  144. metaobjects/render/extract/extract_map.py +61 -0
  145. metaobjects/render/extract/json_forgiving_reader.py +203 -0
  146. metaobjects/render/extract/locate.py +65 -0
  147. metaobjects/render/extract/normalize.py +96 -0
  148. metaobjects/render/extract/strip.py +20 -0
  149. metaobjects/render/extract/types.py +332 -0
  150. metaobjects/render/extract/xml_forgiving_reader.py +162 -0
  151. metaobjects/render/filesystem_provider.py +51 -0
  152. metaobjects/render/prompt/__init__.py +32 -0
  153. metaobjects/render/prompt/output_format_renderer.py +340 -0
  154. metaobjects/render/prompt/output_format_spec.py +28 -0
  155. metaobjects/render/prompt/prompt_field.py +29 -0
  156. metaobjects/render/prompt/prompt_overrides.py +29 -0
  157. metaobjects/render/prompt/prompt_style.py +38 -0
  158. metaobjects/render/renderer.py +358 -0
  159. metaobjects/render/verify.py +266 -0
  160. metaobjects/runtime/__init__.py +39 -0
  161. metaobjects/runtime/llm_recorder.py +210 -0
  162. metaobjects/runtime/n2m_resolver.py +155 -0
  163. metaobjects/runtime/object_manager.py +715 -0
  164. metaobjects/runtime/tph.py +50 -0
  165. metaobjects/serializer_json.py +172 -0
  166. metaobjects/shared/__init__.py +0 -0
  167. metaobjects/shared/base_types.py +16 -0
  168. metaobjects/shared/separators.py +4 -0
  169. metaobjects/shared/structural.py +9 -0
  170. metaobjects/source/__init__.py +79 -0
  171. metaobjects/source/error_source.py +266 -0
  172. metaobjects/source/json_path.py +106 -0
  173. metaobjects/source/semantic_diff.py +98 -0
  174. metaobjects/source/yaml_positions.py +174 -0
  175. metaobjects/super_resolve.py +128 -0
  176. metaobjects/yaml_desugar.py +481 -0
  177. metaobjects-0.9.0.dist-info/METADATA +97 -0
  178. metaobjects-0.9.0.dist-info/RECORD +181 -0
  179. metaobjects-0.9.0.dist-info/WHEEL +4 -0
  180. metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
  181. metaobjects-0.9.0.dist-info/licenses/LICENSE +189 -0
@@ -0,0 +1,272 @@
1
+ """Output parser codegen — one ``<template_name>_output_parser.py`` per
2
+ ``template.output`` declaration.
3
+
4
+ FR-006 (Python) per ADR-0010 and ``docs/superpowers/specs/2026-05-25-fr6-python-template-output-parser.md``.
5
+
6
+ Single-API throw-only convention matches the Python ecosystem norm: Pydantic
7
+ raises ``pydantic.ValidationError`` on bad input; callers wrap in ``try/except``
8
+ as needed. No dual API — TS uses ``parseX``/``safeParseX`` because Zod's
9
+ ``safeParse`` is idiomatic; C# uses ``Parse``/``TryParse`` per BCL convention;
10
+ Python's ecosystem (Pydantic, Instructor, FastAPI, LangChain structured-output)
11
+ is throw-only and a dual surface would feel un-Pythonic.
12
+
13
+ Import-style emit: the parser module is a thin ``parse_<name>(text) -> Payload``
14
+ wrapper that imports the Pydantic ``<TemplateName>Payload`` model from the
15
+ sibling ``<template_name_snake>_payload.py`` (emitted by ``payload_vo_generator``).
16
+ This matches the cross-port story where a single payload-VO class is reused by
17
+ both prompt rendering and output parsing — TS / C# / Kotlin all do the same.
18
+
19
+ The generator emits an empty file list when ``payload_vo_generator`` would have
20
+ emitted nothing for the same template (defensive parity with the loader's
21
+ ``@payloadRef`` validation pass)."""
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Callable
25
+
26
+ from metaobjects.codegen import extract_delegate_emitter as rde
27
+ from metaobjects.codegen.constants import generated_header
28
+ from metaobjects.codegen.format import ruff_format
29
+ from metaobjects.codegen.generator import EmittedFile, GenContext, Generator
30
+ from metaobjects.codegen.generators.payload_vo_generator import (
31
+ payload_class_name,
32
+ payload_module_name,
33
+ resolve_payload_vo,
34
+ )
35
+ from metaobjects.meta.core.object.meta_object import MetaObject
36
+ from metaobjects.meta.meta_data import MetaData
37
+ from metaobjects.meta.template import template_constants as tc
38
+ from metaobjects.shared.base_types import TYPE_TEMPLATE
39
+
40
+ # FR-010: only structured formats get a tolerant extract() alongside the strict parser.
41
+ _EXTRACT_FORMATS = frozenset({tc.TEMPLATE_FORMAT_JSON, tc.TEMPLATE_FORMAT_XML})
42
+
43
+
44
+ _GENERATOR_NAME = "output-parser-generator"
45
+
46
+
47
+ def _snake_case(name: str) -> str:
48
+ """``NpcResponseOutput`` → ``npc_response_output``. Trivial PascalCase →
49
+ snake_case (no acronym handling; matches the cross-port convention used by
50
+ ``router_generator._snake_case``)."""
51
+ out: list[str] = []
52
+ for i, ch in enumerate(name):
53
+ if ch.isupper() and i > 0:
54
+ out.append("_")
55
+ out.append(ch.lower())
56
+ return "".join(out)
57
+
58
+
59
+ def render_output_parser(template: MetaData, root: MetaData) -> str | None:
60
+ """Render one parser module for a ``template.output`` node.
61
+
62
+ The emitted module imports ``<TemplateName>Payload`` from the sibling
63
+ payload module (emitted by ``payload_vo_generator``) and exposes a
64
+ throw-only ``parse_<name>(text)`` entry point.
65
+
66
+ Returns ``None`` when the ``@payloadRef`` can't be resolved (defensive;
67
+ the loader's template-validation pass would normally catch this first)."""
68
+ payload_ref = template.attr(tc.TEMPLATE_ATTR_PAYLOAD_REF)
69
+ if not isinstance(payload_ref, str) or not payload_ref:
70
+ return None
71
+ payload = resolve_payload_vo(root, payload_ref)
72
+ if payload is None:
73
+ return None
74
+
75
+ template_name = template.name
76
+ snake = _snake_case(template_name)
77
+ payload_class = payload_class_name(template_name) # <Name>Payload
78
+ payload_module = payload_module_name(template_name) # <name>_payload
79
+ parse_fn = f"parse_{snake}"
80
+
81
+ fqn = (
82
+ f"{payload.package}::{template_name}"
83
+ if payload.package
84
+ else template_name
85
+ )
86
+
87
+ # FR-010: emit the tolerant extract() API alongside the strict parser when the
88
+ # template targets json/xml. Otherwise only the FR-006 strict parser is emitted
89
+ # (text-format outputs get no extract). The mirror is a nullable twin of the
90
+ # payload, so the strict ``parse_*`` is left exactly as FR-006 shipped it.
91
+ fmt = template.attr(tc.TEMPLATE_ATTR_FORMAT)
92
+ fmt_str = fmt if isinstance(fmt, str) else tc.TEMPLATE_FORMAT_DEFAULT
93
+ emit_extract_lenient = fmt_str.lower() in _EXTRACT_FORMATS
94
+ extracted_class = f"{payload_class}Extracted"
95
+ extract_lenient_fn = f"extract_lenient_{snake}"
96
+
97
+ lines: list[str] = [
98
+ generated_header(template_name, fqn),
99
+ "from __future__ import annotations\n",
100
+ ]
101
+
102
+ if emit_extract_lenient:
103
+ lines.append("from dataclasses import dataclass")
104
+ lines.append("")
105
+ lines.append("from metaobjects.render import (")
106
+ lines.append(" Format,")
107
+ lines.append(" ExtractOptions,")
108
+ lines.append(" ExtractionResult,")
109
+ lines.append(")")
110
+ # FR-010: the single metadata-driven extract path resolves the payload
111
+ # MetaObject from a loaded MetaRoot and delegates to the runtime extract
112
+ # (which assembles the FULL nested object graph reflection-free by reading
113
+ # the live metadata directly). Codegen-wrapping-runtime — mirrors the
114
+ # Java/Kotlin/TS pilots.
115
+ lines.append(
116
+ "from metaobjects.meta.core.object.meta_object import MetaObject"
117
+ )
118
+ lines.append(
119
+ "from metaobjects.meta.core.object.object_extract import extract_object"
120
+ )
121
+ lines.append("from metaobjects.meta.meta_root import MetaRoot")
122
+ lines.append("")
123
+
124
+ lines.extend(
125
+ [
126
+ f"from .{payload_module} import {payload_class}",
127
+ "",
128
+ "",
129
+ f"def {parse_fn}(text: str) -> {payload_class}:",
130
+ f' """Parse an LLM response into a typed ``{payload_class}``.',
131
+ "",
132
+ " Raises:",
133
+ " pydantic.ValidationError: when the input does not match the schema.",
134
+ ' """',
135
+ f" return {payload_class}.model_validate_json(text)",
136
+ "",
137
+ "",
138
+ ]
139
+ )
140
+
141
+ if emit_extract_lenient:
142
+ # FR-010 nested-AWARE extracted mirror: the payload mirror keeps the canonical
143
+ # ``<Name>PayloadExtracted`` name, and a mirror dataclass is emitted for every
144
+ # reachable nested value-object. The single (delegating) extract path returns it.
145
+ lines.extend(rde.nested_mirror_dataclasses(payload, root, extracted_class))
146
+ lines.append("")
147
+ lines.append("")
148
+
149
+ # ---- Runtime-delegating extract (the single metadata-driven extract path) ----
150
+ # The baked PAYLOAD_NAME is the resolved payload VO's SIMPLE name: the
151
+ # delegating entry resolves the MetaObject from a loaded MetaRoot by it
152
+ # (root child named ``payload.name``), then delegates to the runtime
153
+ # ``extract_object`` (FULL nested graph, reflection-free) and maps the
154
+ # assembled ValueObject graph into the typed nullable mirror graph.
155
+ format_enum = "Format.XML" if fmt_str.lower() == "xml" else "Format.JSON"
156
+ root_mapper = rde.root_mapper_name(template_name)
157
+ extract_lenient_with_fn = f"{extract_lenient_fn}_with_loader"
158
+ lines.append("#: Payload value-object name this parser extracts — resolved")
159
+ lines.append("#: against a loaded MetaRoot at runtime.")
160
+ lines.append(f'PAYLOAD_NAME = "{payload.name}"')
161
+ lines.append("")
162
+ lines.append("")
163
+ lines.extend(rde.nested_mappers(payload, root, root_mapper, extracted_class))
164
+ lines.append("")
165
+ lines.append("")
166
+ lines.extend(rde.delegate_helpers(rde.used_helpers(payload, root)))
167
+ lines.append("")
168
+ lines.append("")
169
+ lines.append(
170
+ f"def {extract_lenient_with_fn}("
171
+ "root: MetaRoot, text: str, opts: ExtractOptions | None = None"
172
+ f") -> ExtractionResult[{extracted_class}]:"
173
+ )
174
+ lines.append(
175
+ ' """Runtime-delegating tolerant best-effort extraction; never raises.'
176
+ )
177
+ lines.append(" FULLY populates nested-object and array-of-object components by")
178
+ lines.append(" delegating to the metadata-driven runtime ``extract_object`` (which")
179
+ lines.append(" assembles the whole graph reflection-free via the Phase A object")
180
+ lines.append(" model, reading the live metadata directly), then maps the assembled")
181
+ lines.append(f" graph into the typed ``{extracted_class}`` mirror.")
182
+ lines.append("")
183
+ lines.append(" :param root: a loaded ``MetaRoot`` that declares the")
184
+ lines.append(f' ``{payload.name}`` value-object."""')
185
+ lines.append(" mo = None")
186
+ lines.append(" for child in root.own_children():")
187
+ lines.append(" if (")
188
+ lines.append(" isinstance(child, MetaObject)")
189
+ lines.append(" and child.name == PAYLOAD_NAME")
190
+ lines.append(" ):")
191
+ lines.append(" mo = child")
192
+ lines.append(" break")
193
+ lines.append(" if mo is None:")
194
+ lines.append(" raise ValueError(")
195
+ lines.append(
196
+ f' f"{extract_lenient_with_fn}: payload \'{{PAYLOAD_NAME}}\' not found "'
197
+ )
198
+ lines.append(' "in the supplied MetaRoot"')
199
+ lines.append(" )")
200
+ lines.append(
201
+ f" outcome = extract_object(mo, text, {format_enum}, opts)"
202
+ )
203
+ lines.append(f" data = {root_mapper}(outcome.data)")
204
+ lines.append(" return ExtractionResult(data=data, report=outcome.report)")
205
+ lines.append("")
206
+ lines.append("")
207
+ lines.append(
208
+ f'__all__ = ["{parse_fn}", "{extract_lenient_with_fn}", '
209
+ f'"{extracted_class}", "PAYLOAD_NAME"]'
210
+ )
211
+ else:
212
+ lines.append(f'__all__ = ["{parse_fn}"]')
213
+
214
+ lines.append("")
215
+ return "\n".join(lines)
216
+
217
+
218
+ class OutputParserGenerator:
219
+ """Generator wrapping ``render_output_parser``. Emits one file per
220
+ ``template.output`` declared at root level."""
221
+
222
+ name = _GENERATOR_NAME
223
+
224
+ def __init__(self, *, filter: Callable[[MetaObject], bool] | None = None) -> None:
225
+ # The ``filter`` arg matches the cross-generator contract even though
226
+ # this generator iterates templates (not entities) and doesn't apply
227
+ # entity-level filters today.
228
+ self.filter = filter
229
+
230
+ def _render_module(self, template: MetaData, root: MetaData) -> str | None:
231
+ """EXTENSION SEAM — render the whole parser module for one ``template.output``.
232
+ Defaults to :func:`render_output_parser` (the strict ``parse_*`` + the FR-010
233
+ tolerant ``extract_lenient_*`` twins). Override to pre/post-process the
234
+ emitted source, or to replace the strict-parser / lenient-extractor emission
235
+ entirely. Output is byte-identical to the default when not overridden."""
236
+ return render_output_parser(template, root)
237
+
238
+ def generate(self, ctx: GenContext) -> list[EmittedFile]:
239
+ root = ctx.loaded_root
240
+ if root is None:
241
+ return []
242
+ files: list[EmittedFile] = []
243
+ outputs = sorted(
244
+ (
245
+ c
246
+ for c in root.own_children()
247
+ if c.type == TYPE_TEMPLATE and c.sub_type == tc.TEMPLATE_SUBTYPE_OUTPUT
248
+ ),
249
+ key=lambda c: c.name,
250
+ )
251
+ for tmpl in outputs:
252
+ content = self._render_module(tmpl, root)
253
+ if content is None:
254
+ ctx.warn(
255
+ f"{_GENERATOR_NAME}: skipping template.output "
256
+ f"'{tmpl.name}' (no resolvable @payloadRef)."
257
+ )
258
+ continue
259
+ files.append(
260
+ EmittedFile(
261
+ path=f"{_snake_case(tmpl.name)}_output_parser.py",
262
+ content=ruff_format(content),
263
+ )
264
+ )
265
+ return files
266
+
267
+
268
+ def output_parser_generator(
269
+ *, filter: Callable[[MetaObject], bool] | None = None
270
+ ) -> Generator:
271
+ """Factory mirroring the TS ``outputParser()`` and C# ``OutputParserGenerator``."""
272
+ return OutputParserGenerator(filter=filter)
@@ -0,0 +1,192 @@
1
+ """Output-prompt codegen — one ``<name>_output_prompt.py`` per json/xml
2
+ ``template.output`` declaration.
3
+
4
+ FR-010 artifact 1 (Python port). For each ``template.output`` whose ``@format`` is
5
+ ``json`` or ``xml`` and whose ``@payloadRef`` resolves to an ``object.value``, emits a
6
+ module exposing ``render_<name>_format(overrides=None) -> str`` backed by the render
7
+ engine's :func:`~metaobjects.render.render_output_format` (the "produce your answer
8
+ like this" fragment).
9
+
10
+ The baked :class:`~metaobjects.render.OutputFormatSpec`'s ``root_name`` is the payload
11
+ class name, so the prompt fragment and the ``extract_<name>()`` codegen agree on the
12
+ root element/object name. Mirrors the C# ``OutputPromptGenerator`` / Java
13
+ ``SpringOutputPromptGenerator``. Skips: ``template.prompt``, missing/unresolved
14
+ ``@payloadRef``, and ``@format`` values other than json/xml.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Callable
19
+
20
+ from metaobjects.codegen import output_format_spec_emitter as ofs
21
+ from metaobjects.codegen.constants import generated_header
22
+ from metaobjects.codegen.format import ruff_format
23
+ from metaobjects.codegen.generator import EmittedFile, GenContext, Generator
24
+ from metaobjects.codegen.generators.payload_vo_generator import (
25
+ payload_class_name,
26
+ resolve_payload_vo,
27
+ )
28
+ from metaobjects.meta.core.object.meta_object import MetaObject
29
+ from metaobjects.meta.meta_data import MetaData
30
+ from metaobjects.meta.template import template_constants as tc
31
+ from metaobjects.shared.base_types import TYPE_TEMPLATE
32
+
33
+ _GENERATOR_NAME = "output-prompt-generator"
34
+
35
+ # Only structured formats get a renderable output-format fragment.
36
+ _PROMPT_FORMATS = frozenset({tc.TEMPLATE_FORMAT_JSON, tc.TEMPLATE_FORMAT_XML})
37
+
38
+
39
+ def _snake_case(name: str) -> str:
40
+ """``NpcResponseOutput`` → ``npc_response_output`` — matches the sibling
41
+ generators' local helper (no acronym handling)."""
42
+ out: list[str] = []
43
+ for i, ch in enumerate(name):
44
+ if ch.isupper() and i > 0:
45
+ out.append("_")
46
+ out.append(ch.lower())
47
+ return "".join(out)
48
+
49
+
50
+ def _emit_format_spec(
51
+ payload: MetaObject, template: MetaData, root_name: str
52
+ ) -> str:
53
+ """The baked ``OutputFormatSpec`` literal. Module-level back-compat shim; the
54
+ override seam is :meth:`OutputPromptGenerator._emit_format_spec`."""
55
+ return ofs.spec_literal(payload, template, root_name)
56
+
57
+
58
+ def render_output_prompt(
59
+ template: MetaData,
60
+ root: MetaData,
61
+ *,
62
+ generator: "OutputPromptGenerator | None" = None,
63
+ ) -> str | None:
64
+ """Render one output-prompt module for a json/xml ``template.output`` node.
65
+
66
+ When *generator* is supplied, its ``_emit_format_spec`` override is used to bake
67
+ the ``OutputFormatSpec`` literal (the extension seam); when ``None`` the
68
+ module-level default is used (byte-identical back-compat path).
69
+
70
+ Returns ``None`` when the format is unsupported (not json/xml) or the
71
+ ``@payloadRef`` can't be resolved to an ``object.value`` (defensive — the loader
72
+ validation pass / the parser generator share this contract)."""
73
+ fmt = template.attr(tc.TEMPLATE_ATTR_FORMAT)
74
+ fmt_str = fmt if isinstance(fmt, str) else tc.TEMPLATE_FORMAT_DEFAULT
75
+ if fmt_str.lower() not in _PROMPT_FORMATS:
76
+ return None
77
+
78
+ payload_ref = template.attr(tc.TEMPLATE_ATTR_PAYLOAD_REF)
79
+ if not isinstance(payload_ref, str) or not payload_ref:
80
+ return None
81
+ payload = resolve_payload_vo(root, payload_ref)
82
+ if payload is None:
83
+ return None
84
+
85
+ template_name = template.name
86
+ snake = _snake_case(template_name)
87
+ render_fn = f"render_{snake}_format"
88
+ # root_name == payload class name so the fragment and extract() agree.
89
+ root_name = payload_class_name(template_name)
90
+ emit_spec = generator._emit_format_spec if generator is not None else _emit_format_spec
91
+ spec_literal = emit_spec(payload, template, root_name)
92
+
93
+ fqn = (
94
+ f"{payload.package}::{template_name}"
95
+ if payload.package
96
+ else template_name
97
+ )
98
+
99
+ lines: list[str] = [
100
+ generated_header(template_name, fqn),
101
+ "from __future__ import annotations\n",
102
+ "from metaobjects.render import (",
103
+ " PROMPT_OVERRIDES_NONE,",
104
+ " FieldKind,",
105
+ " Format,",
106
+ " OutputFormatSpec,",
107
+ " PromptField,",
108
+ " PromptOverrides,",
109
+ " PromptStyle,",
110
+ " render_output_format,",
111
+ ")",
112
+ "",
113
+ "",
114
+ "# FR-010 artifact 1 — the baked output-format descriptor for this template.",
115
+ f"_SPEC: OutputFormatSpec = {spec_literal}",
116
+ "",
117
+ "",
118
+ f"def {render_fn}(overrides: PromptOverrides | None = None) -> str:",
119
+ ' """The output-format instruction fragment ("produce your answer like this").',
120
+ "",
121
+ " A comment-free guide / inline / example-only fragment teaching an LLM how to",
122
+ " shape its answer. Pass ``overrides`` to swap the style or override a field's",
123
+ ' example / instruction at render time."""',
124
+ " return render_output_format(_SPEC, overrides or PROMPT_OVERRIDES_NONE)",
125
+ "",
126
+ "",
127
+ f'__all__ = ["{render_fn}"]',
128
+ "",
129
+ ]
130
+ return "\n".join(lines)
131
+
132
+
133
+ class OutputPromptGenerator:
134
+ """Generator wrapping :func:`render_output_prompt`. Emits one file per json/xml
135
+ ``template.output`` declared at root level."""
136
+
137
+ name = _GENERATOR_NAME
138
+
139
+ def __init__(self, *, filter: Callable[[MetaObject], bool] | None = None) -> None:
140
+ # ``filter`` matches the cross-generator contract even though this generator
141
+ # iterates templates (not entities).
142
+ self.filter = filter
143
+
144
+ def _emit_format_spec(
145
+ self, payload: MetaObject, template: MetaData, root_name: str
146
+ ) -> str:
147
+ """EXTENSION SEAM — the baked ``OutputFormatSpec`` literal. Defaults to the
148
+ module-level :func:`_emit_format_spec`; override to inject custom field
149
+ examples / instructions / style into the prompt fragment descriptor."""
150
+ return _emit_format_spec(payload, template, root_name)
151
+
152
+ def _render_module(self, template: MetaData, root: MetaData) -> str | None:
153
+ """EXTENSION SEAM — render the whole output-prompt module for one
154
+ ``template.output``. Defaults to :func:`render_output_prompt` (passing this
155
+ instance so the ``_emit_format_spec`` override is honored). Override to
156
+ pre/post-process the emitted source or replace the render path."""
157
+ return render_output_prompt(template, root, generator=self)
158
+
159
+ def generate(self, ctx: GenContext) -> list[EmittedFile]:
160
+ root = ctx.loaded_root
161
+ if root is None:
162
+ return []
163
+ files: list[EmittedFile] = []
164
+ outputs = sorted(
165
+ (
166
+ c
167
+ for c in root.own_children()
168
+ if c.type == TYPE_TEMPLATE and c.sub_type == tc.TEMPLATE_SUBTYPE_OUTPUT
169
+ ),
170
+ key=lambda c: c.name,
171
+ )
172
+ for tmpl in outputs:
173
+ content = self._render_module(tmpl, root)
174
+ if content is None:
175
+ # Not an error — text-format outputs and unresolved payloads are
176
+ # simply skipped (no prompt fragment), matching the C# contract.
177
+ continue
178
+ files.append(
179
+ EmittedFile(
180
+ path=f"{_snake_case(tmpl.name)}_output_prompt.py",
181
+ content=ruff_format(content),
182
+ )
183
+ )
184
+ return files
185
+
186
+
187
+ def output_prompt_generator(
188
+ *, filter: Callable[[MetaObject], bool] | None = None
189
+ ) -> Generator:
190
+ """Factory mirroring the C# ``OutputPromptGenerator`` / Java
191
+ ``SpringOutputPromptGenerator``."""
192
+ return OutputPromptGenerator(filter=filter)