managarten/packages/shared-ai/src/tools/function-schema.ts
Till JS a4bc7d2ee3 feat(invoices): M8 AI tools — create/mark_paid/list/stats
The last open item from the plan. Missions can now draft invoices from
chat context, mark customer payments, and read status for autonomous
follow-up cadences.

Tool catalog (packages/shared-ai/src/tools/schemas.ts)
- create_invoice (propose) — clientName + lines[] + currency + due
- mark_invoice_paid (propose) — by id, optional back-dated paidAt
- list_invoices (auto) — with status + limit filter
- get_invoice_stats (auto) — open/overdue/YTD per currency

Had to widen the tool-parameter type vocabulary so create_invoice can
declare lines as a typed array. Touched three places:
- ToolSchema-side: the catalog's `type` string is already free-form so
  'array' / 'object' just pass through
- ModuleTool-side (apps/mana/apps/web/src/lib/data/tools/types.ts): added
  'array' | 'object' to the union so TS doesn't narrow the executor's
  param signatures
- function-schema translator (packages/shared-ai): mapParamType +
  JsonSchemaProperty both gained the two new types; the catalog-typo
  guard test now uses 'fruit' as its sentinel (array no longer unknown)

Executor (apps/mana/apps/web/src/lib/modules/invoices/tools.ts)
- coerceLines accepts either a real array or a JSON-stringified array
  (planners vary), skips malformed entries, converts major→minor units
- create_invoice pulls the generated number back from Dexie so the
  success message shows "Entwurf 2026-0042 …" — the user recognises it
- mark_invoice_paid normalises YYYY-MM-DD → ISO so the store's timestamp
  invariant (ISO throughout) stays intact
- list_invoices derives overdue on read (consistent with useAllInvoices),
  returns major-unit amounts so the LLM reasons in user-facing numbers
- get_invoice_stats returns counts + open/overdue/YTD per currency

Registration: invoicesTools added to tools/init.ts. mana-ai drift guard
is happy (41/41 green); webapp + shared-ai type-check 0 errors; full
invoice test suite 59/59 green.

Closes: docs/plans/invoices-module.md §M8. All plan milestones now DONE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:22:20 +02:00

108 lines
3.4 KiB
TypeScript

/**
* Converts AI_TOOL_CATALOG entries to OpenAI-spec function schemas.
*
* Every provider we support (Google Gemini, OpenAI, OpenRouter, Groq,
* Together, Ollama 0.3+) speaks the same `{ type: "function", function:
* { name, description, parameters } }` shape for tool declarations.
* Parameters are JSON Schema. This converter is the single bridge
* between our catalog format and the wire format — changing the catalog
* shape only needs this file (plus tests) to be updated.
*
* The converter keeps tools lean: no `_rationale` meta-parameter, no
* wrapper objects, no provider-specific tweaks. Identifying calls to
* a specific policy decision (propose/auto/deny) is the executor's job,
* not the model's.
*/
import type { ToolSchema } from './schemas';
/** OpenAI-compatible JSON-Schema property type. `array` / `object` are
* emitted without a nested `items` / `properties` schema — tools that
* take structured payloads describe the expected shape inside their
* human-readable description and parse in the executor. Tightening to
* a full JSON-Schema tree would be strictly better but isn't required
* by the OpenAI / Anthropic function-calling specs. */
export interface JsonSchemaProperty {
type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
description?: string;
enum?: readonly string[];
}
/** OpenAI-compatible JSON-Schema object wrapper. */
export interface JsonSchemaObject {
type: 'object';
properties: Record<string, JsonSchemaProperty>;
required: string[];
}
/** OpenAI-compatible function declaration. */
export interface FunctionSpec {
name: string;
description: string;
parameters: JsonSchemaObject;
}
/** OpenAI-compatible tool entry (only `function` tools are supported). */
export interface ToolSpec {
type: 'function';
function: FunctionSpec;
}
/** Map a ToolSchema parameter type ("string" | "number" | "boolean") to
* a JSON Schema type. Catalog values are already narrow enough to pass
* through, but we centralise the translation here for future-proofing
* (e.g. if we ever introduce `"integer"` as a distinct type). */
function mapParamType(t: string): JsonSchemaProperty['type'] {
switch (t) {
case 'string':
case 'number':
case 'integer':
case 'boolean':
case 'array':
case 'object':
return t;
default:
// Unknown types in the catalog are a bug, but don't silently
// coerce to string — that would mask the mistake.
throw new Error(`Unsupported parameter type in tool catalog: "${t}"`);
}
}
/** Convert a single ToolSchema into an OpenAI-spec ToolSpec. */
export function toolToFunctionSchema(tool: ToolSchema): ToolSpec {
const properties: Record<string, JsonSchemaProperty> = {};
const required: string[] = [];
for (const param of tool.parameters) {
const prop: JsonSchemaProperty = {
type: mapParamType(param.type),
description: param.description,
};
if (param.enum && param.enum.length > 0) {
prop.enum = param.enum;
}
properties[param.name] = prop;
if (param.required) {
required.push(param.name);
}
}
return {
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: {
type: 'object',
properties,
required,
},
},
};
}
/** Convert a full tool list to OpenAI-spec entries in one call.
* Handy for building the `tools` field on a chat-completion request. */
export function toolsToFunctionSchemas(tools: readonly ToolSchema[]): ToolSpec[] {
return tools.map(toolToFunctionSchema);
}