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.
- metaobjects/__init__.py +75 -0
- metaobjects/agent_context/__init__.py +55 -0
- metaobjects/agent_context/_content/README.md +14 -0
- metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
- metaobjects/agent_context/_content/servers/java.meta.json +5 -0
- metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
- metaobjects/agent_context/_content/servers/python.meta.json +5 -0
- metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
- metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
- metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
- metaobjects/agent_context/assemble.py +133 -0
- metaobjects/agent_context/content_root.py +54 -0
- metaobjects/agent_context/scaffold.py +191 -0
- metaobjects/agent_context/types.py +44 -0
- metaobjects/attr_class_map.py +23 -0
- metaobjects/cli.py +696 -0
- metaobjects/codegen/__init__.py +0 -0
- metaobjects/codegen/config.py +11 -0
- metaobjects/codegen/constants.py +13 -0
- metaobjects/codegen/extract_delegate_emitter.py +384 -0
- metaobjects/codegen/extract_schema_emitter.py +139 -0
- metaobjects/codegen/format.py +31 -0
- metaobjects/codegen/fr010_field_mapping.py +220 -0
- metaobjects/codegen/generator.py +62 -0
- metaobjects/codegen/generator_registry.py +163 -0
- metaobjects/codegen/generators/__init__.py +0 -0
- metaobjects/codegen/generators/entity_model.py +263 -0
- metaobjects/codegen/generators/extractor_generator.py +317 -0
- metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
- metaobjects/codegen/generators/m2m_codegen.py +192 -0
- metaobjects/codegen/generators/output_parser_generator.py +272 -0
- metaobjects/codegen/generators/output_prompt_generator.py +192 -0
- metaobjects/codegen/generators/payload_vo_generator.py +672 -0
- metaobjects/codegen/generators/render_helper_generator.py +451 -0
- metaobjects/codegen/generators/router_generator.py +635 -0
- metaobjects/codegen/generators/template_generator.py +70 -0
- metaobjects/codegen/generators/tph_plan.py +120 -0
- metaobjects/codegen/generators/trace_helper_generator.py +336 -0
- metaobjects/codegen/instance_artifacts.py +15 -0
- metaobjects/codegen/output_format_spec_emitter.py +79 -0
- metaobjects/codegen/overwrite_policy.py +27 -0
- metaobjects/codegen/runner.py +110 -0
- metaobjects/codegen/runtime/__init__.py +6 -0
- metaobjects/codegen/runtime/filter_parser.py +193 -0
- metaobjects/codegen/type_map.py +84 -0
- metaobjects/core_types.py +809 -0
- metaobjects/datatype.py +19 -0
- metaobjects/documentation/__init__.py +28 -0
- metaobjects/documentation/doc_constants.py +20 -0
- metaobjects/documentation/doc_provider.py +20 -0
- metaobjects/documentation/doc_schema.py +24 -0
- metaobjects/errors.py +124 -0
- metaobjects/loader/__init__.py +0 -0
- metaobjects/loader/merge.py +287 -0
- metaobjects/loader/meta_data_loader.py +245 -0
- metaobjects/loader/sources/__init__.py +24 -0
- metaobjects/loader/sources/directory_source.py +50 -0
- metaobjects/loader/sources/file_source.py +41 -0
- metaobjects/loader/sources/meta_data_source.py +67 -0
- metaobjects/loader/sources/uri_source.py +56 -0
- metaobjects/loader/validate_discriminator.py +181 -0
- metaobjects/loader/validate_field_readonly.py +146 -0
- metaobjects/loader/validate_source_parameter_ref.py +159 -0
- metaobjects/loader/validate_source_physical_names.py +140 -0
- metaobjects/loader/validation_passes.py +1513 -0
- metaobjects/meta/__init__.py +1 -0
- metaobjects/meta/core/__init__.py +0 -0
- metaobjects/meta/core/attr/__init__.py +0 -0
- metaobjects/meta/core/attr/attr_constants.py +31 -0
- metaobjects/meta/core/attr/meta_attr.py +136 -0
- metaobjects/meta/core/field/__init__.py +0 -0
- metaobjects/meta/core/field/field_constants.py +105 -0
- metaobjects/meta/core/field/meta_field.py +76 -0
- metaobjects/meta/core/identity/__init__.py +0 -0
- metaobjects/meta/core/identity/identity_constants.py +19 -0
- metaobjects/meta/core/identity/meta_identity.py +8 -0
- metaobjects/meta/core/object/__init__.py +0 -0
- metaobjects/meta/core/object/meta_object.py +65 -0
- metaobjects/meta/core/object/meta_object_aware.py +43 -0
- metaobjects/meta/core/object/object_class_registry.py +56 -0
- metaobjects/meta/core/object/object_constants.py +13 -0
- metaobjects/meta/core/object/object_extract.py +400 -0
- metaobjects/meta/core/object/value_object.py +70 -0
- metaobjects/meta/core/relationship/__init__.py +0 -0
- metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
- metaobjects/meta/core/relationship/meta_relationship.py +54 -0
- metaobjects/meta/core/relationship/relationship_constants.py +51 -0
- metaobjects/meta/core/validator/__init__.py +0 -0
- metaobjects/meta/core/validator/validator_constants.py +18 -0
- metaobjects/meta/meta_data.py +206 -0
- metaobjects/meta/meta_root.py +8 -0
- metaobjects/meta/persistence/__init__.py +0 -0
- metaobjects/meta/persistence/db/__init__.py +1 -0
- metaobjects/meta/persistence/db/db_constants.py +41 -0
- metaobjects/meta/persistence/db/db_provider.py +60 -0
- metaobjects/meta/persistence/origin/__init__.py +0 -0
- metaobjects/meta/persistence/origin/meta_origin.py +8 -0
- metaobjects/meta/persistence/origin/origin_constants.py +20 -0
- metaobjects/meta/persistence/source/__init__.py +0 -0
- metaobjects/meta/persistence/source/meta_source.py +137 -0
- metaobjects/meta/persistence/source/source_constants.py +115 -0
- metaobjects/meta/presentation/__init__.py +0 -0
- metaobjects/meta/presentation/layout/__init__.py +0 -0
- metaobjects/meta/presentation/layout/layout_constants.py +13 -0
- metaobjects/meta/presentation/layout/meta_layout.py +8 -0
- metaobjects/meta/presentation/view/__init__.py +0 -0
- metaobjects/meta/presentation/view/meta_view.py +8 -0
- metaobjects/meta/presentation/view/view_constants.py +22 -0
- metaobjects/meta/template/__init__.py +0 -0
- metaobjects/meta/template/meta_template.py +46 -0
- metaobjects/meta/template/template_constants.py +112 -0
- metaobjects/meta/template/template_provider.py +43 -0
- metaobjects/parser.py +380 -0
- metaobjects/parser_yaml.py +82 -0
- metaobjects/provider.py +111 -0
- metaobjects/py.typed +0 -0
- metaobjects/registry.py +210 -0
- metaobjects/registry_manifest.py +223 -0
- metaobjects/render/__init__.py +74 -0
- metaobjects/render/email_document.py +14 -0
- metaobjects/render/escapers.py +109 -0
- metaobjects/render/extract/__init__.py +59 -0
- metaobjects/render/extract/coerce.py +279 -0
- metaobjects/render/extract/extract.py +211 -0
- metaobjects/render/extract/extract_map.py +61 -0
- metaobjects/render/extract/json_forgiving_reader.py +203 -0
- metaobjects/render/extract/locate.py +65 -0
- metaobjects/render/extract/normalize.py +96 -0
- metaobjects/render/extract/strip.py +20 -0
- metaobjects/render/extract/types.py +332 -0
- metaobjects/render/extract/xml_forgiving_reader.py +162 -0
- metaobjects/render/filesystem_provider.py +51 -0
- metaobjects/render/prompt/__init__.py +32 -0
- metaobjects/render/prompt/output_format_renderer.py +340 -0
- metaobjects/render/prompt/output_format_spec.py +28 -0
- metaobjects/render/prompt/prompt_field.py +29 -0
- metaobjects/render/prompt/prompt_overrides.py +29 -0
- metaobjects/render/prompt/prompt_style.py +38 -0
- metaobjects/render/renderer.py +358 -0
- metaobjects/render/verify.py +266 -0
- metaobjects/runtime/__init__.py +39 -0
- metaobjects/runtime/llm_recorder.py +210 -0
- metaobjects/runtime/n2m_resolver.py +155 -0
- metaobjects/runtime/object_manager.py +715 -0
- metaobjects/runtime/tph.py +50 -0
- metaobjects/serializer_json.py +172 -0
- metaobjects/shared/__init__.py +0 -0
- metaobjects/shared/base_types.py +16 -0
- metaobjects/shared/separators.py +4 -0
- metaobjects/shared/structural.py +9 -0
- metaobjects/source/__init__.py +79 -0
- metaobjects/source/error_source.py +266 -0
- metaobjects/source/json_path.py +106 -0
- metaobjects/source/semantic_diff.py +98 -0
- metaobjects/source/yaml_positions.py +174 -0
- metaobjects/super_resolve.py +128 -0
- metaobjects/yaml_desugar.py +481 -0
- metaobjects-0.9.0.dist-info/METADATA +97 -0
- metaobjects-0.9.0.dist-info/RECORD +181 -0
- metaobjects-0.9.0.dist-info/WHEEL +4 -0
- metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
- 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)
|