11from __future__ import annotations
22
3+ import copy
34import hashlib
45import json
56import pathlib
@@ -65,6 +66,97 @@ def _prompt_release() -> tuple[dict[str, Any], str, str]:
6566 return manifest , sha256_digest (manifest ), system_prompt
6667
6768
69+ def build_generation_schema (
70+ context : Mapping [str , Any ], catalog : Mapping [str , Mapping [str , Any ]]
71+ ) -> dict [str , Any ]:
72+ """Bind provider grammar to IDs and action shapes in the frozen context."""
73+
74+ try :
75+ schema = json .loads (PROPOSAL_SCHEMA_PATH .read_text (encoding = "utf-8" ))
76+ except (OSError , json .JSONDecodeError ) as error :
77+ raise failures .DiracInternal (
78+ f"research proposal contract cannot be loaded: { type (error ).__name__ } "
79+ ) from None
80+
81+ schema ["properties" ]["context_digest" ] = {"const" : context ["digest" ]}
82+ singleton_ids = {
83+ "hypothesis_drafts" : ("hypothesis_id" , "h1" ),
84+ "claim_assessments" : ("claim_id" , "c1" ),
85+ "scientific_questions" : ("question_id" , "q1" ),
86+ "candidate_actions" : ("proposal_action_id" , "a1" ),
87+ }
88+ for section , (field , value ) in singleton_ids .items ():
89+ section_schema = schema ["properties" ][section ]
90+ section_schema ["minItems" ] = 1
91+ section_schema ["items" ]["properties" ][field ] = {"const" : value }
92+ schema ["properties" ]["candidate_actions" ]["items" ]["properties" ][
93+ "scientific_question_id"
94+ ] = {"const" : "q1" }
95+ schema ["properties" ]["preferred_action_id" ] = {"const" : "a1" }
96+
97+ has_eligible_evidence = any (
98+ item ["source_class" ] == "typed_evidence"
99+ and item ["claim_boundary" ]["eligible_as_scientific_evidence" ]
100+ for item in context ["facts" ]
101+ )
102+ if not has_eligible_evidence :
103+ schema ["properties" ]["claim_assessments" ]["items" ]["properties" ][
104+ "interpretation"
105+ ] = {"const" : "unresolved" }
106+ fact_ids = sorted ({str (item ["fact_id" ]) for item in context ["facts" ]})
107+ fact_id_schema = schema ["$defs" ]["factIds" ]
108+ if fact_ids :
109+ fact_id_schema ["items" ] = {"enum" : fact_ids }
110+ else :
111+ fact_id_schema ["maxItems" ] = 0
112+
113+ refs : dict [tuple [str , str ], dict [str , str ]] = {}
114+ for item in context ["objects" ]:
115+ ref = item ["ref" ]
116+ refs [(ref ["kind" ], ref ["id" ])] = dict (ref )
117+ for item in context ["facts" ]:
118+ ref = item ["subject_ref" ]
119+ refs [(ref ["kind" ], ref ["id" ])] = dict (ref )
120+ for available in context ["available_actions" ]:
121+ for ref in available ["subject_refs" ]:
122+ refs [(ref ["kind" ], ref ["id" ])] = dict (ref )
123+ subject_refs = [refs [key ] for key in sorted (refs )]
124+ if subject_refs :
125+ schema ["properties" ]["scientific_questions" ]["items" ]["properties" ][
126+ "subject_ref"
127+ ] = {"enum" : subject_refs }
128+
129+ candidate = schema ["properties" ]["candidate_actions" ]["items" ]
130+ branches : list [dict [str , Any ]] = []
131+ for available in context ["available_actions" ]:
132+ template_id = str (available ["template_id" ])
133+ template = catalog .get (template_id )
134+ if template is None :
135+ raise failures .DiracInternal (
136+ f"research context exposes unknown action template { template_id } "
137+ )
138+ for subject_ref in available ["subject_refs" ]:
139+ branch = copy .deepcopy (candidate )
140+ branch ["properties" ]["proposal_action_id" ] = {"const" : "a1" }
141+ branch ["properties" ]["scientific_question_id" ] = {"const" : "q1" }
142+ branch ["properties" ]["template_id" ] = {"const" : template_id }
143+ branch ["properties" ]["subject_ref" ] = {"const" : dict (subject_ref )}
144+ hints = copy .deepcopy (template ["model_hint_schema" ])
145+ edge = hints .get ("properties" , {}).get ("edge_id" )
146+ if (
147+ isinstance (edge , dict )
148+ and subject_ref ["kind" ] == "free_energy_transformation"
149+ ):
150+ hints ["properties" ]["edge_id" ] = {"const" : subject_ref ["id" ]}
151+ branch ["properties" ]["parameter_hints" ] = hints
152+ branches .append (branch )
153+ if not branches :
154+ schema ["properties" ]["candidate_actions" ]["maxItems" ] = 0
155+ else :
156+ schema ["properties" ]["candidate_actions" ]["items" ] = {"oneOf" : branches }
157+ return schema
158+
159+
68160def _read_context (payload : Mapping [str , Any ], ctx : InvocationContext ) -> dict [str , Any ]:
69161 if ctx .artifact_reader is None :
70162 raise failures .DiracUnsupported (
@@ -244,6 +336,7 @@ def propose_handler(payload: dict, ctx: InvocationContext) -> HandlerResult:
244336 details = {"reason" : error .reason , ** error .details },
245337 ) from None
246338 provider = OpenAICompatibleChatProvider ()
339+ output_schema = build_generation_schema (context , catalog )
247340 maximum_regenerations = int (profile .document ["bounds" ]["max_schema_regenerations" ])
248341 validation_error : dict [str , Any ] | None = None
249342 usage : dict [str , Any ] = {}
@@ -257,7 +350,8 @@ def propose_handler(payload: dict, ctx: InvocationContext) -> HandlerResult:
257350 )
258351 try :
259352 result = provider .complete_json (
260- profile , system_prompt = system , context_json = user
353+ profile , system_prompt = system , context_json = user ,
354+ output_schema = output_schema ,
261355 )
262356 except ProviderUnavailable as error :
263357 raise failures .DiracProviderUnavailable (
0 commit comments