Documentation
¶
Overview ¶
Package aix is the Go form of SEC-AF's `.ai(...)` gate calls — the single structured LLM request, as opposed to the multi-turn coding-agent session that internal/harnessx drives.
Python shape (src/sec_af/agents/prove/verdict.py:99, reasoners/phases.py:125, harness.py:420, agents/dedup.py:113, compliance/mapping.py:410):
result = await app.ai(user=prompt, schema=VerdictDecision) result = await app.ai(system=system, user=prompt, schema=Model, model=...)
The Python SDK turns `schema=Model` into an OpenAI structured-output request (agentfield/agent_ai.py:803):
"schema": _strictify_openai_schema(schema.model_json_schema())
so the Go port must (a) start from the SAME pydantic schema — the committed fixture harnessx already embeds — and (b) apply the SAME strictification, because OpenAI's strict mode rejects a schema that omits additionalProperties:false or under-populates required.
Index ¶
- func Strictify(schema map[string]any) map[string]any
- func StrictifyOrdered[T any](schema map[string]any) any
- func Structured[T any](ctx context.Context, app appx.AIer, system, user string) (T, error)
- func StructuredOpts[T any](ctx context.Context, app appx.AIer, system, user string, extra ...ai.Option) (T, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Strictify ¶
Strictify ports the Python SDK's _strictify_openai_schema (`agentfield.agent_ai._strictify_openai_schema`, sdk/python/agentfield/agent_ai.py):
def walk(node):
if isinstance(node, dict):
node = {key: walk(value) for key, value in node.items()}
props = node.get("properties")
if isinstance(props, dict) and (node.get("type") == "object" or "type" not in node):
node["additionalProperties"] = False
node["required"] = list(props.keys())
return node
if isinstance(node, list):
return [walk(item) for item in node]
return node
i.e. EVERY dict that has a "properties" object and is either explicitly type:"object" or carries no "type" at all gets additionalProperties:false and a required list naming all of its properties — recursing first, so $defs, nested properties, items and anyOf branches are all covered. Forcing every property into required is OpenAI's documented strict-mode requirement; a genuinely optional field is expressed as nullable, not as absent-from-required.
Two Go-specific notes:
- The input is never mutated; a fresh tree is returned.
- `required` is emitted in SORTED key order, because a bare `map[string]any` carries no insertion order to reproduce Python's `list(props.keys())`. The LIVE path does not go through here: it calls StrictifyOrdered[T], which recovers pydantic's declaration order from the Go struct. This entry point stays for the schema transform itself (and for the goldens, which are generated from the committed sort_keys=True fixtures, so their own property order is sorted).
func StrictifyOrdered ¶
StrictifyOrdered is Strictify plus pydantic's DECLARATION order.
Python strictifies the LIVE `Model.model_json_schema()`, whose `properties` is an insertion-ordered dict in pydantic field-declaration order; `node["required"] = list(props.keys())` (agent_ai.py:319) therefore inherits that order, and litellm serialises the dict as-is. Go decodes the committed fixture into a `map[string]any`, which has no order at all, and `json.Marshal` sorts every map key — so without this the bytes in `response_format.json_schema.schema` differ from Python's for every `.ai(schema=...)` call.
The order is recovered from the Go struct, which the port keeps in pydantic declaration order (harnessx.FieldOrders). It is applied to the ROOT object and to every `$defs` entry whose name matches a struct reachable from T — which is every object node pydantic emits, because a nested BaseModel always becomes a `$defs` entry. Property names with no matching Go field are appended in sorted order rather than dropped, so a drifted fixture degrades to a visible ordering difference instead of losing schema content.
Residual: the key order WITHIN each node (title/type/description/...) is still sorted rather than pydantic's, because go/scripts/gen_schemas.py writes the fixtures with sort_keys=True and that order is gone before Go sees it. The finding this closes is about `properties` and `required`; those are the two the pydantic document orders meaningfully.
func Structured ¶
Structured performs `await app.ai(system=system, user=user, schema=T)` and returns the parsed T.
system == "" means Python's `system=None`: no system message is added, which is what every SEC-AF call site except AIGateWrapper.invoke does.
Decoding is TOLERANT and RETRIED exactly as the Python SDK's is — see parseStructured and maxParseRetries.
func StructuredOpts ¶
func StructuredOpts[T any](ctx context.Context, app appx.AIer, system, user string, extra ...ai.Option) (T, error)
StructuredOpts is Structured with extra SDK options appended after the schema option — the seam AIGateWrapper.invoke needs, since it also passes `model=self.config.ai_model` (src/sec_af/harness.go:420-425). Keeping it separate leaves Structured's signature exactly the one the design doc specifies.
Types ¶
This section is empty.