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: 2 additions & 2 deletions dist/index.d.cts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { S as SignedHeaders, P as Post, b as PostPublishedEvent } from './types-Bky-agAM.cjs';
export { A as Author, C as CloudEvent, E as EVENT_TYPES, a as EventType, F as FeaturedImage, c as PostStatus } from './types-Bky-agAM.cjs';
import { S as SignedHeaders, P as Post, a as PostPublishedEvent } from './types-T7gU2CrZ.cjs';
export { A as Author, C as CloudEvent, E as EVENT_TYPES, b as EventType, F as FeaturedImage, c as PostStatus } from './types-T7gU2CrZ.cjs';

type SignInput = {
id: string;
Expand Down
4 changes: 2 additions & 2 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { S as SignedHeaders, P as Post, b as PostPublishedEvent } from './types-Bky-agAM.js';
export { A as Author, C as CloudEvent, E as EVENT_TYPES, a as EventType, F as FeaturedImage, c as PostStatus } from './types-Bky-agAM.js';
import { S as SignedHeaders, P as Post, a as PostPublishedEvent } from './types-T7gU2CrZ.js';
export { A as Author, C as CloudEvent, E as EVENT_TYPES, b as EventType, F as FeaturedImage, c as PostStatus } from './types-T7gU2CrZ.js';

type SignInput = {
id: string;
Expand Down
77 changes: 34 additions & 43 deletions dist/quality.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ __export(quality_exports, {
scoreQuality: () => scoreQuality
});
module.exports = __toCommonJS(quality_exports);
var import_ai_providers = require("@profullstack/ai-providers");
var WORD_RE = /\b[\p{L}\p{N}'’-]+\b/gu;
function countWords(s) {
if (!s) return 0;
Expand Down Expand Up @@ -105,7 +106,15 @@ function nicheMatches(post, allowedNiches) {
}
return { pass: matched.length > 0, matched };
}
var DEFAULT_MODEL = "claude-haiku-4-5-20251001";
var DEFAULT_TIMEOUT_MS = 12e4;
var SCORE_SCHEMA = {
type: "object",
properties: {
score: { type: "integer", minimum: 0, maximum: 10 },
reasons: { type: "array", items: { type: "string" }, maxItems: 3 }
},
required: ["score"]
};
function buildScorePrompt(post, niche) {
const nicheStr = Array.isArray(niche) ? niche.join(", ") : niche;
return [
Expand Down Expand Up @@ -134,60 +143,41 @@ function clamp(n, lo, hi) {
return Math.max(lo, Math.min(hi, Math.round(n)));
}
async function scoreQuality(input) {
const fetchImpl = input.fetchImpl ?? fetch;
const model = input.model ?? DEFAULT_MODEL;
const timeoutMs = input.timeoutMs ?? 15e3;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
try {
const res = await fetchImpl("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": input.anthropicApiKey,
"anthropic-version": "2023-06-01",
"content-type": "application/json"
},
signal: controller.signal,
body: JSON.stringify({
model,
max_tokens: 200,
messages: [
{ role: "user", content: buildScorePrompt(input.post, input.niche) }
]
})
const ai = input.ai ?? (0, import_ai_providers.createAiProviders)({
...input.fetchImpl ? {
c0mpute: { fetch: input.fetchImpl },
openai: { fetch: input.fetchImpl },
anthropic: { fetch: input.fetchImpl }
} : {},
...input.openaiApiKey ? { openai: { apiKey: input.openaiApiKey } } : {},
...input.anthropicApiKey ? { anthropic: { apiKey: input.anthropicApiKey, ...input.model ? { model: input.model } : {} } } : {}
});
if (!res.ok) {
const text2 = await res.text().catch(() => "");
return { ok: false, score: null, reasons: [], error: `HTTP ${res.status}: ${text2.slice(0, 200)}` };
}
const data = await res.json();
const text = data.content?.find((c) => c.type === "text")?.text ?? "";
const match = text.match(/\{[\s\S]*\}/);
if (!match) {
return { ok: false, score: null, reasons: [], error: "no JSON in response" };
}
let parsed;
try {
parsed = JSON.parse(match[0]);
} catch (err) {
return { ok: false, score: null, reasons: [], error: `bad JSON: ${err.message}` };
}
const obj = parsed;
const res = await ai.generateObject({
system: "You rate blog posts. You answer with one JSON object and nothing else.",
user: buildScorePrompt(input.post, input.niche),
name: "QualityScore",
jsonSchema: SCORE_SCHEMA,
maxTokens: 300,
temperature: 0,
feature: "autoblog.scoreQuality",
timeoutMs
});
const obj = res.object ?? {};
const score = clamp(obj.score, 0, 10);
const reasons = Array.isArray(obj.reasons) ? obj.reasons.filter((r) => typeof r === "string").slice(0, 5) : [];
if (score === null) {
return { ok: false, score: null, reasons, error: "score field missing or not a number" };
}
return { ok: true, score, reasons };
return { ok: true, score, reasons, provider: res.provider, model: res.model };
} catch (err) {
return {
ok: false,
score: null,
reasons: [],
error: err instanceof Error ? err.message : String(err)
};
} finally {
clearTimeout(timer);
}
}
async function gatePost(post, config) {
Expand All @@ -206,11 +196,12 @@ async function gatePost(post, config) {
metrics: heur.metrics
};
}
if (config.minQualityScore !== void 0 && config.anthropicApiKey) {
if (config.minQualityScore !== void 0) {
const q = await scoreQuality({
post,
niche: config.allowedNiches,
anthropicApiKey: config.anthropicApiKey
...config.anthropicApiKey ? { anthropicApiKey: config.anthropicApiKey } : {},
...config.openaiApiKey ? { openaiApiKey: config.openaiApiKey } : {}
});
if (!q.ok) {
if (config.failOpenOnLlmError !== false) {
Expand Down
2 changes: 1 addition & 1 deletion dist/quality.cjs.map

Large diffs are not rendered by default.

40 changes: 32 additions & 8 deletions dist/quality.d.cts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { P as Post } from './types-Bky-agAM.cjs';
import { P as Post } from './types-T7gU2CrZ.cjs';

type HeuristicConfig = {
/** Reject if word count is below this. Default 500. */
Expand Down Expand Up @@ -33,19 +33,38 @@ type QualityScore = {
score: number | null;
reasons: string[];
error?: string;
/** Which provider answered: "c0mpute", "openai" or "anthropic". */
provider?: string;
/** The model that answered. */
model?: string;
};
type ScoreQualityInput = {
post: Post;
/** Optional niche context to bias the model's "relevance" axis. */
niche?: string | string[];
/** Anthropic API key. */
anthropicApiKey: string;
/** Override model — defaults to claude-haiku-4-5-20251001. */
/**
* Anthropic API key, used only for the paid fallback. Optional: c0mpute
* needs no key, so leaving this out still scores.
*/
anthropicApiKey?: string;
/** OpenAI API key for the other paid fallback. Optional. */
openaiApiKey?: string;
/** Override the model on the paid fallback. */
model?: string;
/** Override fetch (testing). */
/** Override fetch (testing). Applies to every provider. */
fetchImpl?: typeof fetch;
/** Override timeout in ms (default 15s). */
/** Override timeout in ms (default 120s - a peer node is slower than Haiku). */
timeoutMs?: number;
/** Pre-built router, for tests or an app that already has one. */
ai?: AiRouterLike;
};
/** The slice of @profullstack/ai-providers' AiRouter this module uses. */
type AiRouterLike = {
generateObject(req: Record<string, unknown>): Promise<{
object: unknown;
provider: string;
model: string;
}>;
};
declare function scoreQuality(input: ScoreQualityInput): Promise<QualityScore>;
type GateConfig = {
Expand All @@ -55,8 +74,13 @@ type GateConfig = {
allowedNiches?: string[];
/** Minimum quality score 0–10. Skip LLM call if undefined. */
minQualityScore?: number;
/** Anthropic API key for the LLM gate. Required iff minQualityScore is set. */
/**
* Anthropic API key for the paid fallback. Optional - the gate runs on
* c0mpute, which needs no key.
*/
anthropicApiKey?: string;
/** OpenAI API key for the other paid fallback. Optional. */
openaiApiKey?: string;
/** Fail-open behavior when the LLM call errors. Default true. */
failOpenOnLlmError?: boolean;
};
Expand All @@ -73,4 +97,4 @@ type GateOutcome = {
};
declare function gatePost(post: Post, config: GateConfig): Promise<GateOutcome>;

export { type GateConfig, type GateOutcome, type HeuristicConfig, type HeuristicResult, type QualityScore, type ScoreQualityInput, gatePost, nicheMatches, scoreHeuristics, scoreQuality };
export { type AiRouterLike, type GateConfig, type GateOutcome, type HeuristicConfig, type HeuristicResult, type QualityScore, type ScoreQualityInput, gatePost, nicheMatches, scoreHeuristics, scoreQuality };
40 changes: 32 additions & 8 deletions dist/quality.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { P as Post } from './types-Bky-agAM.js';
import { P as Post } from './types-T7gU2CrZ.js';

type HeuristicConfig = {
/** Reject if word count is below this. Default 500. */
Expand Down Expand Up @@ -33,19 +33,38 @@ type QualityScore = {
score: number | null;
reasons: string[];
error?: string;
/** Which provider answered: "c0mpute", "openai" or "anthropic". */
provider?: string;
/** The model that answered. */
model?: string;
};
type ScoreQualityInput = {
post: Post;
/** Optional niche context to bias the model's "relevance" axis. */
niche?: string | string[];
/** Anthropic API key. */
anthropicApiKey: string;
/** Override model — defaults to claude-haiku-4-5-20251001. */
/**
* Anthropic API key, used only for the paid fallback. Optional: c0mpute
* needs no key, so leaving this out still scores.
*/
anthropicApiKey?: string;
/** OpenAI API key for the other paid fallback. Optional. */
openaiApiKey?: string;
/** Override the model on the paid fallback. */
model?: string;
/** Override fetch (testing). */
/** Override fetch (testing). Applies to every provider. */
fetchImpl?: typeof fetch;
/** Override timeout in ms (default 15s). */
/** Override timeout in ms (default 120s - a peer node is slower than Haiku). */
timeoutMs?: number;
/** Pre-built router, for tests or an app that already has one. */
ai?: AiRouterLike;
};
/** The slice of @profullstack/ai-providers' AiRouter this module uses. */
type AiRouterLike = {
generateObject(req: Record<string, unknown>): Promise<{
object: unknown;
provider: string;
model: string;
}>;
};
declare function scoreQuality(input: ScoreQualityInput): Promise<QualityScore>;
type GateConfig = {
Expand All @@ -55,8 +74,13 @@ type GateConfig = {
allowedNiches?: string[];
/** Minimum quality score 0–10. Skip LLM call if undefined. */
minQualityScore?: number;
/** Anthropic API key for the LLM gate. Required iff minQualityScore is set. */
/**
* Anthropic API key for the paid fallback. Optional - the gate runs on
* c0mpute, which needs no key.
*/
anthropicApiKey?: string;
/** OpenAI API key for the other paid fallback. Optional. */
openaiApiKey?: string;
/** Fail-open behavior when the LLM call errors. Default true. */
failOpenOnLlmError?: boolean;
};
Expand All @@ -73,4 +97,4 @@ type GateOutcome = {
};
declare function gatePost(post: Post, config: GateConfig): Promise<GateOutcome>;

export { type GateConfig, type GateOutcome, type HeuristicConfig, type HeuristicResult, type QualityScore, type ScoreQualityInput, gatePost, nicheMatches, scoreHeuristics, scoreQuality };
export { type AiRouterLike, type GateConfig, type GateOutcome, type HeuristicConfig, type HeuristicResult, type QualityScore, type ScoreQualityInput, gatePost, nicheMatches, scoreHeuristics, scoreQuality };
77 changes: 34 additions & 43 deletions dist/quality.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading