Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ The reference implementation accepts any `ModelAdapter`. Two adapters ship:
- `src/adapters/mock-adapter.ts` -- a deterministic adapter for development and testing. No API key required.
- `src/adapters/anthropic-adapter.ts` -- the Anthropic API adapter. Activated when `ANTHROPIC_API_KEY` is set. To use it: `export ANTHROPIC_API_KEY=sk-...` and replace the mock adapter with the Anthropic adapter in your code.

### Dogfooding (real LLM call)

`npm run example:legal-summarization-anthropic` runs the legal-summarization decision-type against a **real** Anthropic call and writes both the COTS (`examples/output/legal-summarization-anthropic.cots.json`) and a run record (`...run-record.json`: adapter, model, timestamps, SHA-256 of the COTS and decision text) so the run is recorded and reproducible. Requires `ANTHROPIC_API_KEY`; without it the script exits with a clear message and the mock example remains the offline path. Validate the resulting COTS with the conformance suite and render it with `se-trace explore`.

## Decision-types

Two decision-types are implemented:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build": "tsc",
"test": "vitest run",
"example:legal-summarization": "node dist/examples/legal-summarization.js",
"example:legal-summarization-anthropic": "node dist/examples/legal-summarization-anthropic.js",
"example:transaction-routing": "node dist/examples/transaction-routing.js"
},
"keywords": [
Expand Down
97 changes: 97 additions & 0 deletions src/examples/legal-summarization-anthropic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Dogfooding example: produce a COTS from a REAL LLM call via the Anthropic
* adapter (Advancement Plan Phase 5, publisher-side dogfooding).
*
* Run:
* export ANTHROPIC_API_KEY=sk-...
* npm run build
* npm run example:legal-summarization-anthropic
*
* The COTS is written to examples/output/legal-summarization-anthropic.cots.json
* together with a run-record JSON (prompt, model, parameters, timestamps,
* content hashes) so the example is recorded and reproducible. Validate the
* COTS with the conformance suite:
*
* (in source-engineering-conformance) node dist/cli.js validate <path>
* (in source-engineering-tools) node dist/cli/se-trace.js explore <path> -o ./out
*
* Without ANTHROPIC_API_KEY the script exits with a clear message; the mock
* example (example:legal-summarization) remains the offline path.
*/

import { createHash } from "node:crypto";
import { writeFileSync, mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { Layer1 } from "../layer1/layer1.js";
import { AnthropicModelAdapter } from "../adapters/anthropic-adapter.js";
import { LegalSummarizationDecisionType } from "../decision-types/legal-summarization.js";
import type { SourceArtifact } from "../cots.js";

const __dirname = dirname(fileURLToPath(import.meta.url));

const sha256 = (s: string): string => createHash("sha256").update(s).digest("hex");

const main = async () => {
if (!process.env.ANTHROPIC_API_KEY) {
console.error(
"ANTHROPIC_API_KEY is not set. This example performs a real LLM call and needs a key.\n" +
"For the offline path, use: npm run example:legal-summarization"
);
process.exit(1);
}

const sourceArtifact: SourceArtifact = {
id: "urn:source-engineering:source:example-contract-2026-001",
origin: {
system_name: "Acme Contracts",
system_version: "1.0.0",
configuration_hash: "0000000000000000000000000000000000000000000000000000000000000001",
},
version: {
content_hash: "1111111111111111111111111111111111111111111111111111111111111111",
issued_at: "2026-10-10T08:00:00Z",
},
authority: { asserted_by: "acme-contracts", trust_score: 0.95 },
temporal_validity: { valid_from: "2026-10-10T08:00:00Z", valid_until: null },
content_reference: { uri: "https://example.com/contracts/2026-001.pdf", media_type: "application/pdf" },
};

const question = {
document: { uri: "https://example.com/contracts/2026-001.pdf", media_type: "application/pdf" },
};

const adapter = new AnthropicModelAdapter();
const startedAt = new Date().toISOString();
const layer1 = new Layer1(LegalSummarizationDecisionType, adapter);
const cots = await layer1.invoke(question, [sourceArtifact]);
const finishedAt = new Date().toISOString();

const outDir = resolve(__dirname, "..", "..", "examples", "output");
mkdirSync(outDir, { recursive: true });
const cotsPath = resolve(outDir, "legal-summarization-anthropic.cots.json");
writeFileSync(cotsPath, JSON.stringify(cots, null, 2));

const runRecord = {
kind: "se-dogfood-run-record",
example: "legal-summarization-anthropic",
adapter: { name: adapter.name, version: adapter.version },
started_at: startedAt,
finished_at: finishedAt,
cots_id: cots.cots_id,
cots_sha256: sha256(JSON.stringify(cots)),
decision_text_sha256: sha256(cots.decisions[0].decision_text),
note: "Recorded, reproducible run of the Anthropic adapter. Re-running with the same prompt and model version should produce the same COTS shape; the decision text may vary with the model.",
};
const recordPath = resolve(outDir, "legal-summarization-anthropic.run-record.json");
writeFileSync(recordPath, JSON.stringify(runRecord, null, 2));

console.log(`COTS written to ${cotsPath}`);
console.log(`Run record written to ${recordPath}`);
console.log(`Decision text (first 200 chars): ${cots.decisions[0].decision_text.slice(0, 200)}`);
};

main().catch((err) => {
console.error(err);
process.exit(1);
});
66 changes: 66 additions & 0 deletions tests/anthropic-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Tests for the Anthropic adapter (dogfooding path). The real API is never
* called in tests: fetch is stubbed. Verifies request shape, response
* extraction, error handling, and the missing-key guard.
*/

import { describe, it, expect, vi, afterEach } from "vitest";
import { AnthropicModelAdapter } from "../src/adapters/anthropic-adapter.js";

afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

describe("AnthropicModelAdapter", () => {
it("throws a clear error when no API key is available", () => {
const saved = process.env.ANTHROPIC_API_KEY;
delete process.env.ANTHROPIC_API_KEY;
try {
expect(() => new AnthropicModelAdapter()).toThrow(/API key not found/);
} finally {
if (saved !== undefined) process.env.ANTHROPIC_API_KEY = saved;
}
});

it("sends the documented request shape and extracts text blocks", async () => {
const calls: Array<{ url: string; init: RequestInit }> = [];
vi.stubGlobal(
"fetch",
vi.fn(async (url: string, init: RequestInit) => {
calls.push({ url, init });
return new Response(
JSON.stringify({
content: [
{ type: "text", text: "Summary: " },
{ type: "tool_use", name: "x" },
{ type: "text", text: "the contract." },
],
}),
{ status: 200 }
);
})
);
const adapter = new AnthropicModelAdapter({ apiKey: "test-key", model: "claude-sonnet-4-5" });
const out = await adapter.complete("Summarise this contract.");
expect(out).toBe("Summary: the contract.");
expect(calls).toHaveLength(1);
expect(calls[0].url).toBe("https://api.anthropic.com/v1/messages");
const headers = calls[0].init.headers as Record<string, string>;
expect(headers["x-api-key"]).toBe("test-key");
expect(headers["anthropic-version"]).toBe("2023-06-01");
const body = JSON.parse(calls[0].init.body as string);
expect(body.model).toBe("claude-sonnet-4-5");
expect(body.messages[0].role).toBe("user");
expect(body.messages[0].content).toContain("Summarise this contract.");
});

it("surfaces API errors with status and body", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response("rate limited", { status: 429 }))
);
const adapter = new AnthropicModelAdapter({ apiKey: "test-key" });
await expect(adapter.complete("hi")).rejects.toThrow(/Anthropic API error 429/);
});
});
Loading