From 296c75e42d75e49da61060995051fd0ce331e82f Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 19 May 2026 14:23:16 +0800 Subject: [PATCH] feat: add generic structured output example schema --- README.md | 2 +- docs/structured-output.md | 35 +++++++++------- .../schemas/binary-classifier.schema.json | 41 +++++++++++++++++++ package.json | 1 + tests/structured-output.test.ts | 28 +++++++++++++ 5 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 examples/schemas/binary-classifier.schema.json diff --git a/README.md b/README.md index 1c9b85d..c5ba81c 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ See [docs/structured-output.md](docs/structured-output.md). Example: ```bash -localagent --final-schema ./schema.json -p "inspect https://github.com/openclaw/openclaw/pull/80568 and classify it" +localagent --final-schema ./examples/schemas/binary-classifier.schema.json -p "classify whether this issue is release-blocking: " ``` ## Options diff --git a/docs/structured-output.md b/docs/structured-output.md index add08ba..3994e05 100644 --- a/docs/structured-output.md +++ b/docs/structured-output.md @@ -2,7 +2,7 @@ Localagent supports structured final answers for workflows that need machine-readable output from a local model. -Example: inspect a GitHub PR with Pi tools, then classify whether the PR is related to local models. +Example: inspect a document, issue, or pull request with Pi tools, then classify whether it matches a criterion. ## How It Works @@ -22,19 +22,19 @@ This keeps the agent loop freeform while making the final answer structured. ## CLI ```bash -localagent --final-schema ./schema.json -p "inspect https://github.com/openclaw/openclaw/pull/80568 and classify it" +localagent --final-schema ./examples/schemas/binary-classifier.schema.json -p "classify whether this issue is release-blocking: " ``` Alias: ```bash -localagent --schema ./schema.json -p "inspect the PR and classify it" +localagent --schema ./examples/schemas/binary-classifier.schema.json -p "classify whether this issue is release-blocking: " ``` You can also set a default schema with: ```bash -export LOCALAGENT_FINAL_SCHEMA=./schema.json +export LOCALAGENT_FINAL_SCHEMA=./examples/schemas/binary-classifier.schema.json ``` ## Tool Allow Lists @@ -44,7 +44,7 @@ If you pass Pi a tool allow list with `--tools` or `-t`, localagent automaticall For example, this: ```bash -localagent --final-schema ./schema.json --tools bash -p "inspect the PR" +localagent --final-schema ./examples/schemas/binary-classifier.schema.json --tools bash -p "inspect the repository and classify the issue" ``` is passed to Pi as if the tool list were: @@ -63,15 +63,16 @@ bash,final_json { "type": "object", "additionalProperties": false, - "required": ["is_local_model_related", "interest", "confidence", "description", "caveats"], + "required": ["is_match", "label", "confidence", "summary", "reasons", "caveats"], "properties": { - "is_local_model_related": { "type": "boolean" }, - "interest": { + "is_match": { "type": "boolean" }, + "label": { "type": "string", - "enum": ["i0", "i1", "i2", "i3", "i4"] + "enum": ["match", "partial_match", "no_match", "unclear"] }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 }, - "description": { "type": "string" }, + "summary": { "type": "string" }, + "reasons": { "type": "array", "items": { "type": "string" } }, "caveats": { "type": "array", "items": { "type": "string" } } } } @@ -81,11 +82,15 @@ bash,final_json ```json { - "is_local_model_related": true, - "interest": "i0", - "confidence": 0.9, - "description": "This PR fixes LM Studio auth resolution, which affects using local model servers from OpenClaw.", - "caveats": ["This is provider integration work, not model inference behavior."] + "is_match": true, + "label": "match", + "confidence": 0.86, + "summary": "The issue describes a startup crash that blocks the release candidate path.", + "reasons": [ + "The issue affects the release branch.", + "The failure happens before the main UI can load." + ], + "caveats": ["The impact on patch releases was not checked."] } ``` diff --git a/examples/schemas/binary-classifier.schema.json b/examples/schemas/binary-classifier.schema.json new file mode 100644 index 0000000..ca46677 --- /dev/null +++ b/examples/schemas/binary-classifier.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Binary Classifier Result", + "type": "object", + "additionalProperties": false, + "required": ["is_match", "label", "confidence", "summary", "reasons", "caveats"], + "properties": { + "is_match": { + "type": "boolean", + "description": "Whether the item matches the criterion from the prompt." + }, + "label": { + "type": "string", + "enum": ["match", "partial_match", "no_match", "unclear"], + "description": "A compact classification label." + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "summary": { + "type": "string", + "description": "One or two sentences explaining the classification." + }, + "reasons": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Short evidence-backed reasons for the classification." + }, + "caveats": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Important uncertainty, missing context, or limitations." + } + } +} diff --git a/package.json b/package.json index b5ef4c5..04ae28f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "files": [ "dist/src/**", "docs/**", + "examples/**", "README.md", "LICENSE" ], diff --git a/tests/structured-output.test.ts b/tests/structured-output.test.ts index c1d926a..906df3d 100644 --- a/tests/structured-output.test.ts +++ b/tests/structured-output.test.ts @@ -10,6 +10,34 @@ import { createLaunchPlan } from "../src/pi/launch.js"; import { createFinalSchemaRuntime, readFinalSchemaOutput } from "../src/structured/final-schema.js"; describe("structured output", () => { + it("ships a context-agnostic example schema", async () => { + const raw = await readFile( + path.join(process.cwd(), "examples", "schemas", "binary-classifier.schema.json"), + "utf8" + ); + const parsed = JSON.parse(raw) as { + type?: string; + required?: string[]; + properties?: { label?: { enum?: string[] } }; + }; + + expect(parsed.type).toBe("object"); + expect(parsed.required).toEqual([ + "is_match", + "label", + "confidence", + "summary", + "reasons", + "caveats" + ]); + expect(parsed.properties?.label?.enum).toEqual([ + "match", + "partial_match", + "no_match", + "unclear" + ]); + }); + it("creates a final_json extension from a schema", async () => { const stateDir = await mkdtemp(path.join(os.tmpdir(), "localagent-structured-")); try {