import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
import { a as neutralizeUntrusted, i as guardrailsEnabled, n as llmProvider, o as maskPii, r as localLlmConfig, s as piiMaskingEnabled } from "./compliance-kq0xHRw3.js";
import Anthropic from "@anthropic-ai/sdk";
//#region src/core/resilience.ts
var CircuitBreaker = class {
	opts;
	failures = 0;
	openedAt = null;
	_state = "closed";
	constructor(opts) {
		this.opts = opts;
	}
	get state() {
		return this._state;
	}
	async call(fn) {
		if (this._state === "open") if (Date.now() - (this.openedAt ?? 0) >= this.opts.halfOpenAfter) this._state = "half-open";
		else throw new Error("Circuit open");
		try {
			const result = await fn();
			this.failures = 0;
			this._state = "closed";
			this.openedAt = null;
			return result;
		} catch (err) {
			this.failures++;
			if (this._state === "half-open" || this.failures >= this.opts.threshold) {
				this._state = "open";
				this.openedAt = Date.now();
				this.failures = 0;
			}
			throw err;
		}
	}
};
//#endregion
//#region src/core/input-guard.ts
function guardIsoDate(val, field) {
	if (typeof val !== "string" || !val) throw new Error(`${field}: invalid date`);
	const d = new Date(val);
	if (isNaN(d.getTime())) throw new Error(`${field}: invalid date`);
	if (/^\d{4}-\d{2}-\d{2}/.test(val)) {
		const [year, month, day] = val.slice(0, 10).split("-").map(Number);
		if (month < 1 || month > 12 || day < 1 || day > 31) throw new Error(`${field}: invalid date`);
		const reparse = /* @__PURE__ */ new Date(`${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`);
		if (isNaN(reparse.getTime()) || reparse.getMonth() + 1 !== month) throw new Error(`${field}: invalid date`);
	}
	return val;
}
const DEFAULT_LLM_MAX_BYTES = 512 * 1024;
function guardLlmResponse(response, maxBytes = DEFAULT_LLM_MAX_BYTES) {
	if (typeof response !== "string") throw new Error("LLM response: expected string");
	const byteLen = Buffer.byteLength(response, "utf-8");
	if (byteLen > maxBytes) throw new Error(`LLM response exceeds ${maxBytes} bytes (got ${byteLen})`);
	return response;
}
//#endregion
//#region src/core/llm.ts
var llm_exports = /* @__PURE__ */ __exportAll({
	callLlm: () => callLlm,
	summarizeEmail: () => summarizeEmail
});
const MODEL = "claude-haiku-4-5-20251001";
let _client = null;
let llmCircuit = new CircuitBreaker({
	threshold: 3,
	timeoutMs: 3e4,
	halfOpenAfter: 3e4
});
function getClient() {
	if (!process.env["ANTHROPIC_API_KEY"]) return null;
	if (!_client) _client = new Anthropic();
	return _client;
}
function emailFallback(snippet) {
	return {
		summary: snippet.slice(0, 300),
		sentiment: "neutral",
		nextSteps: []
	};
}
async function summarizeEmail(subject, snippet, from) {
	const client = getClient();
	if (!client) return emailFallback(snippet);
	try {
		const textBlock = (await client.messages.create({
			model: MODEL,
			max_tokens: 200,
			system: [{
				type: "text",
				text: "You are a CRM assistant. Extract structured information from email metadata.\nReturn ONLY valid JSON matching: { \"summary\": string (2 sentences, German), \"sentiment\": \"positive\"|\"neutral\"|\"negative\"|\"urgent\", \"nextSteps\": string[] }",
				cache_control: { type: "ephemeral" }
			}],
			messages: [{
				role: "user",
				content: `Subject: ${subject}\nFrom: ${from}\nContent: ${snippet}`
			}]
		})).content.find((b) => b.type === "text");
		if (!textBlock || textBlock.type !== "text") return emailFallback(snippet);
		try {
			return JSON.parse(textBlock.text);
		} catch {
			return emailFallback(snippet);
		}
	} catch {
		return emailFallback(snippet);
	}
}
function recordCall(model, inputTokens, outputTokens, ctx) {
	const dataDir = process.env["DXCRM_DATA_DIR"] ?? process.cwd();
	import("./usage-D0u9a-lV.js").then(({ recordUsage }) => recordUsage(dataDir, {
		...ctx?.slug ? { slug: ctx.slug } : {},
		...ctx?.tool ? { tool: ctx.tool } : {},
		model,
		inputTokens,
		outputTokens
	}));
}
/**
* Local-LLM path (D17): call an OpenAI-compatible endpoint (Ollama/local) via
* fetch — no extra dependency — so customer data can stay on-machine. Usage is
* still recorded for cost/observability parity with the Anthropic path.
*/
async function callLocalLlm(masked, ctx) {
	const { baseUrl, model } = localLlmConfig();
	const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({
			model,
			max_tokens: 500,
			messages: [{
				role: "user",
				content: masked
			}]
		})
	});
	if (!res.ok) throw new Error(`Local LLM error ${res.status}`);
	const data = await res.json();
	const text = data.choices?.[0]?.message?.content;
	if (!text) throw new Error("No text response from local LLM");
	if (data.usage) recordCall(model, data.usage.prompt_tokens ?? 0, data.usage.completion_tokens ?? 0, ctx);
	return text;
}
async function callLlm(prompt, ctx) {
	const provider = llmProvider();
	const client = provider === "anthropic" ? getClient() : null;
	if (provider === "anthropic" && !client) throw new Error("ANTHROPIC_API_KEY not set");
	const guarded = guardrailsEnabled() ? neutralizeUntrusted(prompt) : prompt;
	const { masked, unmask } = piiMaskingEnabled() ? maskPii(guarded) : {
		masked: guarded,
		unmask: (t) => t
	};
	if (provider !== "anthropic") return llmCircuit.call(async () => unmask(guardLlmResponse(await callLocalLlm(masked, ctx))));
	return llmCircuit.call(async () => {
		const response = await client.messages.create({
			model: MODEL,
			max_tokens: 500,
			messages: [{
				role: "user",
				content: masked
			}]
		});
		const usage = response.usage;
		if (usage) recordCall(MODEL, usage.input_tokens, usage.output_tokens, ctx);
		const textBlock = response.content.find((b) => b.type === "text");
		if (!textBlock || textBlock.type !== "text") throw new Error("No text response from LLM");
		return unmask(guardLlmResponse(textBlock.text));
	});
}
//#endregion
export { guardIsoDate as i, llm_exports as n, summarizeEmail as r, callLlm as t };

//# sourceMappingURL=llm-BnSUBisu.js.map