aix

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func Strictify

func Strictify(schema map[string]any) map[string]any

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

func StrictifyOrdered[T any](schema map[string]any) any

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

func Structured[T any](ctx context.Context, app appx.AIer, system, user string) (T, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL