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,70 @@
|
|
|
1
|
+
"""template_generator() — Python port of the TS rc.12 factory.
|
|
2
|
+
|
|
3
|
+
Walks the loaded MetaRoot -> renders shared Mustache templates via the
|
|
4
|
+
metaobjects.render engine -> returns EmittedFile[]. Same Generator Protocol
|
|
5
|
+
as the per-entity hand-coded generators; just adds the "Mustache template"
|
|
6
|
+
+ "walk that yields a data dict per output" primitives.
|
|
7
|
+
|
|
8
|
+
Design: spec/design-docs/2026-05-28-cross-port-template-generator.md.
|
|
9
|
+
Cross-port byte-equivalence verified via fixtures/render-conformance/template-generator/.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Callable, Iterable, Sequence
|
|
15
|
+
|
|
16
|
+
from metaobjects.codegen.generator import EmittedFile, GenContext, Generator
|
|
17
|
+
from metaobjects.render import escapers
|
|
18
|
+
from metaobjects.render.renderer import RenderRequest, render
|
|
19
|
+
from metaobjects.render.verify import Provider
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class _TemplateGenerator:
|
|
24
|
+
name: str
|
|
25
|
+
template: str
|
|
26
|
+
walk: Callable[[Any], Sequence[dict]]
|
|
27
|
+
provider: Provider
|
|
28
|
+
format: str = escapers.FORMAT_TEXT
|
|
29
|
+
|
|
30
|
+
def generate(self, ctx: GenContext) -> list[EmittedFile]:
|
|
31
|
+
walk_results: Iterable[dict] = self.walk(ctx.loaded_root)
|
|
32
|
+
files: list[EmittedFile] = []
|
|
33
|
+
for entry in walk_results:
|
|
34
|
+
content = render(
|
|
35
|
+
RenderRequest(
|
|
36
|
+
payload=entry["data"],
|
|
37
|
+
provider=self.provider,
|
|
38
|
+
ref=self.template,
|
|
39
|
+
format=self.format,
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
files.append(EmittedFile(path=entry["output_path"], content=content))
|
|
43
|
+
return files
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def template_generator(
|
|
47
|
+
*,
|
|
48
|
+
name: str,
|
|
49
|
+
template: str,
|
|
50
|
+
walk: Callable[[Any], Sequence[dict]],
|
|
51
|
+
provider: Provider,
|
|
52
|
+
format: str = escapers.FORMAT_TEXT,
|
|
53
|
+
) -> Generator:
|
|
54
|
+
"""Build a Generator that renders a Mustache template per walk entry.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
name: kebab-case identifier; surfaces in diagnostics.
|
|
58
|
+
template: ref resolved by the provider (e.g. "custom/hello").
|
|
59
|
+
walk: callback that takes the loaded MetaRoot and returns a list of
|
|
60
|
+
dicts shaped {"data": <payload>, "output_path": <relative path>}.
|
|
61
|
+
provider: ref-resolver for the template.
|
|
62
|
+
format: render format ("text", "html", "markdown", ...). Defaults to text.
|
|
63
|
+
"""
|
|
64
|
+
return _TemplateGenerator(
|
|
65
|
+
name=name,
|
|
66
|
+
template=template,
|
|
67
|
+
walk=walk,
|
|
68
|
+
provider=provider,
|
|
69
|
+
format=format,
|
|
70
|
+
)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""FR-017 Tier 4 — codegen-side descriptor for a table-per-hierarchy (TPH) base.
|
|
2
|
+
|
|
3
|
+
Mirrors the C# ``TphPlan`` / TS ``tph-discriminator.ts``: an ``object.entity``
|
|
4
|
+
carrying ``@discriminator`` is the TPH base; concrete entities that ``extends`` it
|
|
5
|
+
and declare ``@discriminatorValue`` are its subtypes, all sharing ONE physical
|
|
6
|
+
table (single-table inheritance). The plan is the single source of truth every
|
|
7
|
+
TPH-aware generator reads, so the route-segment rule and subtype set never drift.
|
|
8
|
+
|
|
9
|
+
The per-subtype REST route segment is the ``@discriminatorValue`` lowercased
|
|
10
|
+
(``"Bridge"`` → ``bridge``, ``"PriorAuth"`` → ``priorauth``) — derived in exactly
|
|
11
|
+
one place (:func:`route_segment`).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from metaobjects.meta.core.object.meta_object import MetaObject
|
|
18
|
+
from metaobjects.meta.core.object.object_constants import (
|
|
19
|
+
OBJECT_ATTR_DISCRIMINATOR,
|
|
20
|
+
OBJECT_ATTR_DISCRIMINATOR_VALUE,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class TphSubtypePlan:
|
|
26
|
+
#: The concrete subtype entity.
|
|
27
|
+
entity: MetaObject
|
|
28
|
+
#: Its ``@discriminatorValue`` (e.g. ``"Bridge"``).
|
|
29
|
+
value: str
|
|
30
|
+
#: The per-subtype REST route segment (e.g. ``"bridge"``).
|
|
31
|
+
route_segment: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class TphPlan:
|
|
36
|
+
#: The discriminator base entity.
|
|
37
|
+
base: MetaObject
|
|
38
|
+
#: The discriminator field name (the base's ``@discriminator``).
|
|
39
|
+
discriminator_field: str
|
|
40
|
+
#: Concrete subtypes in stable (name-sorted) order.
|
|
41
|
+
subtypes: list[TphSubtypePlan]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def route_segment(discriminator_value: str) -> str:
|
|
45
|
+
"""The per-subtype REST route segment — the ONE place this rule lives: the
|
|
46
|
+
discriminator value lowercased."""
|
|
47
|
+
return discriminator_value.lower()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _discriminator_root(obj: MetaObject) -> MetaObject | None:
|
|
51
|
+
"""The nearest ``@discriminator``-bearing ancestor (or self), walking ``extends``."""
|
|
52
|
+
cursor: MetaObject | None = obj
|
|
53
|
+
while cursor is not None:
|
|
54
|
+
field = cursor.attr(OBJECT_ATTR_DISCRIMINATOR) # own attr
|
|
55
|
+
if isinstance(field, str) and field:
|
|
56
|
+
return cursor
|
|
57
|
+
cursor = cursor.super_data # type: ignore[assignment]
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def tph_subtype_binding(obj: MetaObject) -> tuple[str, str] | None:
|
|
62
|
+
"""For a concrete TPH subtype, return ``(discriminator_field, discriminator_value)``
|
|
63
|
+
— the field NAME inherited from the ``@discriminator`` base + this subtype's own
|
|
64
|
+
``@discriminatorValue``. ``None`` when *obj* is not a subtype. Used by the entity
|
|
65
|
+
generator to pin the inherited discriminator field to a ``Literal`` on the subtype."""
|
|
66
|
+
value = obj.attr(OBJECT_ATTR_DISCRIMINATOR_VALUE) # own attr
|
|
67
|
+
if not isinstance(value, str) or not value:
|
|
68
|
+
return None
|
|
69
|
+
root = _discriminator_root(obj)
|
|
70
|
+
if root is None or root is obj:
|
|
71
|
+
return None
|
|
72
|
+
field = root.attr(OBJECT_ATTR_DISCRIMINATOR)
|
|
73
|
+
if not isinstance(field, str) or not field:
|
|
74
|
+
return None
|
|
75
|
+
return (field, value)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_tph_subtype(obj: MetaObject) -> bool:
|
|
79
|
+
"""True when *obj* is a concrete TPH subtype: it declares ``@discriminatorValue``
|
|
80
|
+
and (transitively) extends a ``@discriminator``-bearing base. Such an entity emits
|
|
81
|
+
NO standalone table/router — it is folded into the base's single table."""
|
|
82
|
+
value = obj.attr(OBJECT_ATTR_DISCRIMINATOR_VALUE) # own attr
|
|
83
|
+
if not isinstance(value, str) or not value:
|
|
84
|
+
return False
|
|
85
|
+
root = _discriminator_root(obj)
|
|
86
|
+
return root is not None and root is not obj
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def tph_plan_for(base: MetaObject, object_index: dict[str, MetaObject]) -> TphPlan | None:
|
|
90
|
+
"""The :class:`TphPlan` for a discriminator base, or ``None`` when *base* is not a
|
|
91
|
+
discriminator base (no ``@discriminator``, or no concrete subtypes)."""
|
|
92
|
+
disc_field = base.attr(OBJECT_ATTR_DISCRIMINATOR) # own attr
|
|
93
|
+
if not isinstance(disc_field, str) or not disc_field:
|
|
94
|
+
return None
|
|
95
|
+
subtypes: list[TphSubtypePlan] = []
|
|
96
|
+
for obj in object_index.values():
|
|
97
|
+
if obj is base or getattr(obj, "is_abstract", False):
|
|
98
|
+
continue
|
|
99
|
+
value = obj.attr(OBJECT_ATTR_DISCRIMINATOR_VALUE) # own attr
|
|
100
|
+
if not isinstance(value, str) or not value:
|
|
101
|
+
continue
|
|
102
|
+
# Walk the extends chain looking for `base`.
|
|
103
|
+
cursor = obj.super_data
|
|
104
|
+
found = False
|
|
105
|
+
while cursor is not None:
|
|
106
|
+
if cursor is base:
|
|
107
|
+
found = True
|
|
108
|
+
break
|
|
109
|
+
cursor = cursor.super_data
|
|
110
|
+
if found:
|
|
111
|
+
subtypes.append(TphSubtypePlan(obj, value, route_segment(value)))
|
|
112
|
+
if not subtypes:
|
|
113
|
+
return None
|
|
114
|
+
subtypes.sort(key=lambda s: s.entity.name)
|
|
115
|
+
return TphPlan(base, disc_field, subtypes)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def is_tph_base(obj: MetaObject, object_index: dict[str, MetaObject]) -> bool:
|
|
119
|
+
"""True when *obj* carries ``@discriminator`` AND has at least one concrete subtype."""
|
|
120
|
+
return tph_plan_for(obj, object_index) is not None
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Trace-helper codegen — one ``record_<entity>.py`` per concrete ``object.entity``
|
|
2
|
+
that (a) transitively ``extends`` ``metaobjects::ai::LlmCallBase`` AND (b) nests a
|
|
3
|
+
``template.prompt`` carrying ``@responseRef`` and/or ``@payloadRef``.
|
|
4
|
+
|
|
5
|
+
AI LLM-call trace persistence — Unit 2 Slice 2 (Python port). Cross-port parity
|
|
6
|
+
with the TypeScript ``trace-helper-file.ts`` (``record<Entity>`` + ``call<Entity>``)
|
|
7
|
+
and the Java ``LlmTraceHelperGenerator`` (``record<Entity>`` only). This Python
|
|
8
|
+
port emits the ``record_<entity>`` half ONLY — the render→call→record loop needs an
|
|
9
|
+
``LlmClient`` seam that is BYO / vendor-neutral (ADR-0024), so ``call<Entity>`` is
|
|
10
|
+
intentionally NOT emitted (matching Java).
|
|
11
|
+
|
|
12
|
+
The emitted helper exposes a single ``record_<snake>(recorder, input, redact=None)``
|
|
13
|
+
that:
|
|
14
|
+
|
|
15
|
+
1. runs the tolerant extract (``extract`` from ``metaobjects.render.extract``,
|
|
16
|
+
which NEVER raises) of ``input.llm_response_text`` against a baked
|
|
17
|
+
``_RESPONSE_SCHEMA`` (the response VO's field shape — emitted via the SAME
|
|
18
|
+
``extract_schema_emitter`` path the output-parser generator uses for its
|
|
19
|
+
tolerant ``extract_lenient_*`` twin);
|
|
20
|
+
2. derives ``status``/``error_detail`` from the extract report's lost-required gate
|
|
21
|
+
(a lost ``@required`` field → ``status="error"`` + a ``"lost required: …"`` detail);
|
|
22
|
+
3. builds the base trace row via the Slice-1 ``build_llm_call_row`` (the 18
|
|
23
|
+
``LlmCallBase`` base fields + raw ``llmRequest``/``llmResponse``), folding in the
|
|
24
|
+
derived status/error_detail by replacing the input;
|
|
25
|
+
4. sets the typed ``voRequest`` (``input.llm_request``) and ``voResponse``
|
|
26
|
+
(``outcome.data`` — the extracted mirror dict) columns on the row → native jsonb;
|
|
27
|
+
5. persists the row ONCE via the supplied recorder (the never-throwing Slice-1
|
|
28
|
+
``persist_llm_call_row``, which redacts then records);
|
|
29
|
+
6. returns a typed ``<Entity>TraceResult`` (``status``, ``error_detail``,
|
|
30
|
+
``vo_response``).
|
|
31
|
+
|
|
32
|
+
Skips (no helper emitted) when the entity is abstract, does not derive from
|
|
33
|
+
``LlmCallBase``, has no nested ``template.prompt``, or that prompt carries NEITHER
|
|
34
|
+
``@responseRef`` NOR ``@payloadRef`` (both gate the helper — matching the TS
|
|
35
|
+
reference). The response VO is resolved via ``resolve_payload_vo`` (short-name OR
|
|
36
|
+
FQN); a non-``object.value`` target is a hard generator error (matching Java).
|
|
37
|
+
|
|
38
|
+
STI/TPH discriminator handling is DEFERRED (matching the Java port's Slice 2 — a
|
|
39
|
+
plain trace entity is the target).
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from collections.abc import Callable
|
|
44
|
+
|
|
45
|
+
from metaobjects.codegen import extract_schema_emitter as rse
|
|
46
|
+
from metaobjects.codegen.constants import generated_header
|
|
47
|
+
from metaobjects.codegen.format import ruff_format
|
|
48
|
+
from metaobjects.codegen.generator import EmittedFile, GenContext, Generator
|
|
49
|
+
from metaobjects.codegen.generators.payload_vo_generator import resolve_payload_vo
|
|
50
|
+
from metaobjects.meta.core.object.meta_object import MetaObject
|
|
51
|
+
from metaobjects.meta.core.object.object_constants import OBJECT_SUBTYPE_ENTITY
|
|
52
|
+
from metaobjects.meta.meta_data import MetaData
|
|
53
|
+
from metaobjects.meta.template import template_constants as tc
|
|
54
|
+
from metaobjects.meta.template.meta_template import MetaTemplate
|
|
55
|
+
from metaobjects.shared.base_types import TYPE_TEMPLATE
|
|
56
|
+
|
|
57
|
+
_GENERATOR_NAME = "trace-helper"
|
|
58
|
+
|
|
59
|
+
#: The abstract base entity a trace entity must (transitively) ``extends``.
|
|
60
|
+
#: Cross-port constant — mirrors TS ``LLM_CALL_BASE`` / Java ``LLM_CALL_BASE``.
|
|
61
|
+
LLM_CALL_BASE = "LlmCallBase"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _snake_case(name: str) -> str:
|
|
65
|
+
"""``GreetingCall`` → ``greeting_call``. PascalCase → snake_case with no
|
|
66
|
+
acronym handling — matches the convention used by sibling generators
|
|
67
|
+
(``output_parser_generator._snake_case`` etc.)."""
|
|
68
|
+
out: list[str] = []
|
|
69
|
+
for i, ch in enumerate(name):
|
|
70
|
+
if ch.isupper() and i > 0:
|
|
71
|
+
out.append("_")
|
|
72
|
+
out.append(ch.lower())
|
|
73
|
+
return "".join(out)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _extends_base(entity: MetaObject) -> bool:
|
|
77
|
+
"""Walk the resolved super chain looking for a node SHORT-named
|
|
78
|
+
:data:`LLM_CALL_BASE`. ``MetaData.name`` holds the short name only (the package
|
|
79
|
+
lives on ``MetaData.package``), so a plain ``name`` compare is the short-name
|
|
80
|
+
test — mirrors the Java ``getShortName()`` walk and the TS ``superResolved``
|
|
81
|
+
walk."""
|
|
82
|
+
cur = entity.super_data
|
|
83
|
+
visited: set[int] = set()
|
|
84
|
+
while cur is not None and id(cur) not in visited:
|
|
85
|
+
if cur.name == LLM_CALL_BASE:
|
|
86
|
+
return True
|
|
87
|
+
visited.add(id(cur))
|
|
88
|
+
cur = cur.super_data
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _first_prompt(entity: MetaObject) -> MetaTemplate | None:
|
|
93
|
+
"""First OWN ``template.prompt`` child of *entity*, or ``None``. Own-only —
|
|
94
|
+
the trace prompt is declared inline on the concrete trace entity (Slice 1/3
|
|
95
|
+
derive the typed columns from it)."""
|
|
96
|
+
for child in entity.own_children():
|
|
97
|
+
if (
|
|
98
|
+
isinstance(child, MetaTemplate)
|
|
99
|
+
and child.type == TYPE_TEMPLATE
|
|
100
|
+
and child.sub_type == tc.TEMPLATE_SUBTYPE_PROMPT
|
|
101
|
+
):
|
|
102
|
+
return child
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def render_trace_helper(entity: MetaObject, root: MetaData) -> str | None:
|
|
107
|
+
"""Render one ``record_<entity>.py`` for a concrete trace ``object.entity``.
|
|
108
|
+
|
|
109
|
+
Returns ``None`` when the entity is not a trace-helper target (abstract, not
|
|
110
|
+
``LlmCallBase``-derived, no nested ``template.prompt``, or that prompt carries
|
|
111
|
+
neither ``@responseRef`` nor ``@payloadRef``).
|
|
112
|
+
|
|
113
|
+
Raises ``ValueError`` when the prompt's ``@responseRef`` does not resolve to an
|
|
114
|
+
``object.value`` (matching the Java port's hard ``GeneratorException``)."""
|
|
115
|
+
if entity.is_abstract:
|
|
116
|
+
return None
|
|
117
|
+
if not _extends_base(entity):
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
prompt = _first_prompt(entity)
|
|
121
|
+
if prompt is None:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
response_ref = prompt.response_ref()
|
|
125
|
+
payload_ref = prompt.payload_ref()
|
|
126
|
+
# Both gate the helper — at least one of @responseRef / @payloadRef must be
|
|
127
|
+
# present (matches the TS reference, which needs @responseRef to type the result
|
|
128
|
+
# and @payloadRef to type the request).
|
|
129
|
+
if response_ref is None and payload_ref is None:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# The response VO drives the baked extract schema + the typed voResponse. When
|
|
133
|
+
# only @payloadRef is set we still emit a helper (the request is typed); the
|
|
134
|
+
# extract schema falls back to an empty descriptor (no response VO to shape it).
|
|
135
|
+
response_vo = resolve_payload_vo(root, response_ref) if response_ref else None
|
|
136
|
+
if response_ref is not None and response_vo is None:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"{_GENERATOR_NAME}: entity {entity.name!r} prompt @responseRef "
|
|
139
|
+
f"{response_ref!r} does not resolve to an object.value"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
entity_name = entity.name
|
|
143
|
+
snake = _snake_case(entity_name)
|
|
144
|
+
record_fn = f"record_{snake}"
|
|
145
|
+
result_class = f"{entity_name}TraceResult"
|
|
146
|
+
|
|
147
|
+
# Derive the parse format from the prompt's @format attr (default json) — the
|
|
148
|
+
# SAME rule the output-parser / extractor generators use.
|
|
149
|
+
fmt = prompt.attr(tc.TEMPLATE_ATTR_FORMAT)
|
|
150
|
+
fmt_str = fmt if isinstance(fmt, str) else tc.TEMPLATE_FORMAT_DEFAULT
|
|
151
|
+
|
|
152
|
+
fqn = entity.fqn()
|
|
153
|
+
|
|
154
|
+
# Baked response-extract schema. REUSE the extract_schema_emitter exactly as the
|
|
155
|
+
# output-parser generator does for its tolerant ``extract_lenient_*`` twin:
|
|
156
|
+
# ``schema_literal(vo, fmt, root_name)`` emits an
|
|
157
|
+
# ``ExtractSchema(Format.X, "<root>", [FieldSpec(...), …])`` literal, and
|
|
158
|
+
# ``extract_map_imports(vo)`` returns the sorted/deduped ``extract_map`` accessor
|
|
159
|
+
# names the literal's FieldSpec helpers reference. ``root_name`` is the response
|
|
160
|
+
# VO's short name (the JSON/XML root the tolerant reader locates). When there is
|
|
161
|
+
# no response VO (only @payloadRef set) the schema is an empty descriptor whose
|
|
162
|
+
# extract yields ``{}``.
|
|
163
|
+
if response_vo is not None:
|
|
164
|
+
schema_literal = rse.schema_literal(response_vo, fmt_str, response_vo.name)
|
|
165
|
+
helpers = rse.extract_map_imports(response_vo)
|
|
166
|
+
else:
|
|
167
|
+
schema_literal = f'ExtractSchema({_format_enum(fmt_str)}, "response", [])'
|
|
168
|
+
helpers = []
|
|
169
|
+
|
|
170
|
+
request_doc = payload_ref if payload_ref else "the structured request object"
|
|
171
|
+
|
|
172
|
+
lines: list[str] = [
|
|
173
|
+
generated_header(entity_name, fqn),
|
|
174
|
+
"from __future__ import annotations",
|
|
175
|
+
"",
|
|
176
|
+
"import dataclasses",
|
|
177
|
+
"from dataclasses import dataclass",
|
|
178
|
+
"",
|
|
179
|
+
"from metaobjects.render.extract import (",
|
|
180
|
+
" ExtractSchema,",
|
|
181
|
+
" FieldKind,",
|
|
182
|
+
" FieldSpec,",
|
|
183
|
+
" Format,",
|
|
184
|
+
" extract,",
|
|
185
|
+
")",
|
|
186
|
+
]
|
|
187
|
+
if helpers:
|
|
188
|
+
lines.append("from metaobjects.render.extract.extract_map import (")
|
|
189
|
+
for h in helpers:
|
|
190
|
+
lines.append(f" {h},")
|
|
191
|
+
lines.append(")")
|
|
192
|
+
lines.extend(
|
|
193
|
+
[
|
|
194
|
+
"from metaobjects.runtime import (",
|
|
195
|
+
" LlmCallInput,",
|
|
196
|
+
" LlmCallRecorder,",
|
|
197
|
+
" LlmCallRow,",
|
|
198
|
+
" STATUS_ERROR,",
|
|
199
|
+
" STATUS_OK,",
|
|
200
|
+
" build_llm_call_row,",
|
|
201
|
+
" persist_llm_call_row,",
|
|
202
|
+
")",
|
|
203
|
+
"",
|
|
204
|
+
"from collections.abc import Callable",
|
|
205
|
+
"",
|
|
206
|
+
"",
|
|
207
|
+
"# AI-trace baked response-extract descriptor — the format/root/field shape",
|
|
208
|
+
"# the tolerant parser repairs the model's raw response text against.",
|
|
209
|
+
f"_RESPONSE_SCHEMA: ExtractSchema = {schema_literal}",
|
|
210
|
+
"",
|
|
211
|
+
"",
|
|
212
|
+
"@dataclass(frozen=True, slots=True)",
|
|
213
|
+
f"class {result_class}:",
|
|
214
|
+
f' """Typed result of ``{record_fn}``: the derived call outcome plus the',
|
|
215
|
+
" best-effort extracted response VO.",
|
|
216
|
+
"",
|
|
217
|
+
" * ``status`` — ``STATUS_OK`` | ``STATUS_ERROR`` (a lost ``@required``",
|
|
218
|
+
" response field → ``STATUS_ERROR``).",
|
|
219
|
+
" * ``error_detail`` — a ``\"lost required: …\"`` summary when ``status`` is",
|
|
220
|
+
" ``STATUS_ERROR``, else ``None``.",
|
|
221
|
+
" * ``vo_response`` — the extracted response mirror dict (``None`` only when",
|
|
222
|
+
' extraction produced nothing)."""',
|
|
223
|
+
" status: str",
|
|
224
|
+
" error_detail: str | None",
|
|
225
|
+
" vo_response: dict | None",
|
|
226
|
+
"",
|
|
227
|
+
"",
|
|
228
|
+
f"def {record_fn}(",
|
|
229
|
+
" recorder: LlmCallRecorder,",
|
|
230
|
+
" input: LlmCallInput,",
|
|
231
|
+
" redact: Callable[[LlmCallRow], LlmCallRow] | None = None,",
|
|
232
|
+
f") -> {result_class}:",
|
|
233
|
+
f' """Record a single ``{entity_name}`` LLM call: extract the typed response',
|
|
234
|
+
" VO from ``input.llm_response_text`` and persist ONE trace row (the base",
|
|
235
|
+
" envelope + raw I/O + typed voRequest/voResponse) via ``recorder`` —",
|
|
236
|
+
" regardless of whether extraction succeeded.",
|
|
237
|
+
"",
|
|
238
|
+
" The tolerant ``extract`` NEVER raises; a lost ``@required`` response field",
|
|
239
|
+
" drives ``status``/``error_detail`` (it does not abort the persist). The",
|
|
240
|
+
f" request payload (``input.llm_request``, typed as ``{request_doc}`` at the",
|
|
241
|
+
" call site) is threaded through as the typed ``voRequest`` column.",
|
|
242
|
+
"",
|
|
243
|
+
" :param recorder: the never-throwing write-side seam (Slice-1 recorder).",
|
|
244
|
+
" :param input: the LLM call fields (envelope + raw request/response text).",
|
|
245
|
+
" :param redact: optional row redaction applied before the single record.",
|
|
246
|
+
' """',
|
|
247
|
+
" outcome = extract(input.llm_response_text, _RESPONSE_SCHEMA)",
|
|
248
|
+
" failed = outcome.report.has_lost_required()",
|
|
249
|
+
" status = STATUS_ERROR if failed else STATUS_OK",
|
|
250
|
+
" error_detail = (",
|
|
251
|
+
' "lost required: " + ", ".join(outcome.report.lost_required())',
|
|
252
|
+
" if failed",
|
|
253
|
+
" else None",
|
|
254
|
+
" )",
|
|
255
|
+
" # Extraction owns the derived status/error_detail — fold them into the input",
|
|
256
|
+
" # before building the base row (dataclasses.replace, leaving the original",
|
|
257
|
+
" # input untouched).",
|
|
258
|
+
" effective = dataclasses.replace(",
|
|
259
|
+
" input, status=status, error_detail=error_detail",
|
|
260
|
+
" )",
|
|
261
|
+
" row = build_llm_call_row(effective)",
|
|
262
|
+
" # Typed columns → native jsonb (pg8000 binds dict/list straight to jsonb).",
|
|
263
|
+
" row[\"voResponse\"] = outcome.data",
|
|
264
|
+
" row[\"voRequest\"] = input.llm_request",
|
|
265
|
+
" # Persist ONCE (redact-then-record; the recorder never raises).",
|
|
266
|
+
" persist_llm_call_row(recorder, row, redact)",
|
|
267
|
+
f" return {result_class}(status, error_detail, outcome.data)",
|
|
268
|
+
"",
|
|
269
|
+
"",
|
|
270
|
+
f'__all__ = ["{record_fn}", "{result_class}"]',
|
|
271
|
+
"",
|
|
272
|
+
]
|
|
273
|
+
)
|
|
274
|
+
return "\n".join(lines)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _format_enum(fmt: str) -> str:
|
|
278
|
+
"""``"xml"`` → ``Format.XML``; anything else → ``Format.JSON``. Matches the
|
|
279
|
+
extract_schema_emitter's private ``_format_enum`` (kept local to avoid reaching
|
|
280
|
+
into a private helper)."""
|
|
281
|
+
return "Format.XML" if fmt.lower() == tc.TEMPLATE_FORMAT_XML else "Format.JSON"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class TraceHelperGenerator:
|
|
285
|
+
"""Generator wrapping :func:`render_trace_helper`. Emits one
|
|
286
|
+
``record_<entity>.py`` per concrete ``object.entity`` that derives from
|
|
287
|
+
``LlmCallBase`` and nests a ``template.prompt`` with ``@responseRef`` /
|
|
288
|
+
``@payloadRef`` (skips everything else)."""
|
|
289
|
+
|
|
290
|
+
name = _GENERATOR_NAME
|
|
291
|
+
|
|
292
|
+
def __init__(self, *, filter: Callable[[MetaObject], bool] | None = None) -> None:
|
|
293
|
+
# The ``filter`` arg matches the cross-generator contract.
|
|
294
|
+
self.filter = filter
|
|
295
|
+
|
|
296
|
+
def _render_module(self, entity: MetaObject, root: MetaData) -> str | None:
|
|
297
|
+
"""EXTENSION SEAM — render the whole ``record_<entity>`` module for one trace
|
|
298
|
+
entity. Defaults to :func:`render_trace_helper`. Override to pre/post-process
|
|
299
|
+
the emitted source or replace the render path entirely."""
|
|
300
|
+
return render_trace_helper(entity, root)
|
|
301
|
+
|
|
302
|
+
def generate(self, ctx: GenContext) -> list[EmittedFile]:
|
|
303
|
+
root = ctx.loaded_root
|
|
304
|
+
if root is None:
|
|
305
|
+
return []
|
|
306
|
+
files: list[EmittedFile] = []
|
|
307
|
+
# Stable name order — deterministic emission (matches the other generators).
|
|
308
|
+
entities = sorted(
|
|
309
|
+
(
|
|
310
|
+
c
|
|
311
|
+
for c in root.own_children()
|
|
312
|
+
if isinstance(c, MetaObject) and c.sub_type == OBJECT_SUBTYPE_ENTITY
|
|
313
|
+
),
|
|
314
|
+
key=lambda c: c.name,
|
|
315
|
+
)
|
|
316
|
+
for entity in entities:
|
|
317
|
+
if self.filter is not None and not self.filter(entity):
|
|
318
|
+
continue
|
|
319
|
+
content = self._render_module(entity, root)
|
|
320
|
+
if content is None:
|
|
321
|
+
continue
|
|
322
|
+
files.append(
|
|
323
|
+
EmittedFile(
|
|
324
|
+
path=f"record_{_snake_case(entity.name)}.py",
|
|
325
|
+
content=ruff_format(content),
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
return files
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def trace_helper_generator(
|
|
332
|
+
*, filter: Callable[[MetaObject], bool] | None = None
|
|
333
|
+
) -> Generator:
|
|
334
|
+
"""Factory mirroring the TS ``traceHelperFile()`` and the Java
|
|
335
|
+
``LlmTraceHelperGenerator``. Stable cross-port name ``trace-helper``."""
|
|
336
|
+
return TraceHelperGenerator(filter=filter)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Guard for the abstract concept (mirrors the TS instance-artifacts module).
|
|
2
|
+
|
|
3
|
+
An abstract entity must never produce instance/write artifacts (routers, filter
|
|
4
|
+
allowlists, CREATE TABLE DDL). The Pydantic base *model* is a separate, configurable
|
|
5
|
+
shape concern (emit_abstract_shapes, default on) handled in entity_model.
|
|
6
|
+
"""
|
|
7
|
+
from metaobjects.meta.core.object.meta_object import MetaObject
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_abstract(entity: MetaObject) -> bool:
|
|
11
|
+
return entity.is_abstract is True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def emits_instance_artifacts(entity: MetaObject) -> bool:
|
|
15
|
+
return not is_abstract(entity)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Turns a payload value-object + its ``template.output`` node into a Python source
|
|
2
|
+
literal for an :class:`~metaobjects.render.OutputFormatSpec` — the artifact-1 prompt
|
|
3
|
+
descriptor used by the FR-010 output-prompt codegen.
|
|
4
|
+
|
|
5
|
+
Emits ``OutputFormatSpec(Format.X, "rootName", PromptStyle.X, [PromptField(...), …])``.
|
|
6
|
+
Mirrors the C# / Java ``OutputFormatSpecEmitter`` adapted to Python (keyword args for
|
|
7
|
+
the long ``PromptField`` ctor to keep the emitted source readable + order-robust).
|
|
8
|
+
Bounded scope: scalar / enum. Nested object → ``FieldKind.OBJECT`` placeholder.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from metaobjects.codegen import fr010_field_mapping as fm
|
|
13
|
+
from metaobjects.meta.core.field import field_constants as fc
|
|
14
|
+
from metaobjects.meta.meta_data import MetaData
|
|
15
|
+
from metaobjects.meta.template import template_constants as tc
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _format_enum(template: MetaData) -> str:
|
|
19
|
+
fmt = template.attr(tc.TEMPLATE_ATTR_FORMAT)
|
|
20
|
+
return (
|
|
21
|
+
"Format.XML"
|
|
22
|
+
if isinstance(fmt, str) and fmt.lower() == "xml"
|
|
23
|
+
else "Format.JSON"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _prompt_style_enum(template: MetaData) -> str:
|
|
28
|
+
style = template.attr(tc.TEMPLATE_ATTR_PROMPT_STYLE)
|
|
29
|
+
if style == tc.PROMPT_STYLE_INLINE:
|
|
30
|
+
return "PromptStyle.INLINE"
|
|
31
|
+
if style == tc.PROMPT_STYLE_EXAMPLE_ONLY:
|
|
32
|
+
return "PromptStyle.EXAMPLE_ONLY"
|
|
33
|
+
return "PromptStyle.GUIDE"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _opt_string_attr(field: MetaData, attr_name: str) -> str:
|
|
37
|
+
v = field.attr(attr_name)
|
|
38
|
+
return fm.py_string_literal(v) if isinstance(v, str) else "None"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _prompt_field_literal(field: MetaData) -> str:
|
|
42
|
+
name = field.name
|
|
43
|
+
req = "True" if fm.is_required(field) else "False"
|
|
44
|
+
array = "True" if fm.is_array(field) else "False"
|
|
45
|
+
|
|
46
|
+
if field.sub_type == fc.FIELD_SUBTYPE_OBJECT:
|
|
47
|
+
return (
|
|
48
|
+
f'PromptField("{name}", FieldKind.OBJECT, {req}, array={array})'
|
|
49
|
+
" # FR-010: nested prompt deferred"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
example = _opt_string_attr(field, fc.FIELD_ATTR_EXAMPLE)
|
|
53
|
+
instruction = _opt_string_attr(field, fc.FIELD_ATTR_INSTRUCTION)
|
|
54
|
+
|
|
55
|
+
if field.sub_type == fc.FIELD_SUBTYPE_ENUM:
|
|
56
|
+
values_lit = fm.string_list_literal(fm.enum_values(field))
|
|
57
|
+
enum_doc_lit = fm.properties_map_literal(field.attr(fc.FIELD_ATTR_ENUM_DOC))
|
|
58
|
+
return (
|
|
59
|
+
f'PromptField("{name}", FieldKind.ENUM, {req}, array={array}, '
|
|
60
|
+
f"enum_values={values_lit}, enum_doc={enum_doc_lit}, "
|
|
61
|
+
f"example={example}, instruction={instruction})"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
kind = fm.scalar_kind(field.sub_type) or "STRING"
|
|
65
|
+
return (
|
|
66
|
+
f'PromptField("{name}", FieldKind.{kind}, {req}, array={array}, '
|
|
67
|
+
f"example={example}, instruction={instruction})"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def spec_literal(vo: MetaData, template: MetaData, root_name: str) -> str:
|
|
72
|
+
"""Emit ``OutputFormatSpec(Format.X, "rootName", PromptStyle.X, [PromptField(...), …])``."""
|
|
73
|
+
format_enum = _format_enum(template)
|
|
74
|
+
style_enum = _prompt_style_enum(template)
|
|
75
|
+
field_lits = [_prompt_field_literal(f) for f in fm.fields(vo)]
|
|
76
|
+
body = ", ".join(field_lits)
|
|
77
|
+
return (
|
|
78
|
+
f'OutputFormatSpec({format_enum}, "{root_name}", {style_enum}, [{body}])'
|
|
79
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Per-file write decision based on the @generated marker.
|
|
2
|
+
Mirrors codegen-ts/src/overwrite-policy.ts. Three-way merge is a later enhancement."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from .constants import GENERATED_MARKER
|
|
8
|
+
|
|
9
|
+
# status: "new" | "overwrite" | "refused" | "skipped"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def decide_and_write(path: str, content: str, strategy: str = "overwrite") -> str:
|
|
13
|
+
if not os.path.exists(path):
|
|
14
|
+
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
15
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
16
|
+
fh.write(content)
|
|
17
|
+
return "new"
|
|
18
|
+
|
|
19
|
+
with open(path, encoding="utf-8") as fh:
|
|
20
|
+
current = fh.read()
|
|
21
|
+
if GENERATED_MARKER not in current:
|
|
22
|
+
return "refused"
|
|
23
|
+
if strategy == "skip-existing":
|
|
24
|
+
return "skipped"
|
|
25
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
26
|
+
fh.write(content)
|
|
27
|
+
return "overwrite"
|