@relvehq/agent-ready 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +338 -3
- package/SETUP.md +337 -0
- package/dist/bin/agent-ready.d.ts +23 -0
- package/dist/bin/agent-ready.js +350 -0
- package/dist/cli/init.d.ts +70 -0
- package/dist/cli/init.js +366 -0
- package/dist/core/accept.d.ts +8 -0
- package/dist/core/accept.js +76 -0
- package/dist/core/config.d.ts +145 -0
- package/dist/core/config.js +103 -0
- package/dist/core/convert.d.ts +22 -0
- package/dist/core/convert.js +334 -0
- package/dist/core/detect.d.ts +30 -0
- package/dist/core/detect.js +82 -0
- package/dist/core/easy.d.ts +19 -0
- package/dist/core/easy.js +117 -0
- package/dist/core/enrich.d.ts +30 -0
- package/dist/core/enrich.js +155 -0
- package/dist/core/llms.d.ts +11 -0
- package/dist/core/llms.js +71 -0
- package/dist/core/report.d.ts +15 -0
- package/dist/core/report.js +46 -0
- package/dist/core/resolve.d.ts +43 -0
- package/dist/core/resolve.js +204 -0
- package/dist/core/respond.d.ts +18 -0
- package/dist/core/respond.js +59 -0
- package/dist/core/sitemap.d.ts +31 -0
- package/dist/core/sitemap.js +140 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +76 -0
- package/dist/next/dashboard.d.ts +14 -0
- package/dist/next/dashboard.js +56 -0
- package/dist/next/index.d.ts +42 -0
- package/dist/next/index.js +100 -0
- package/dist/next/route.d.ts +26 -0
- package/dist/next/route.js +167 -0
- package/dist/node/index.d.ts +8 -0
- package/dist/node/index.js +191 -0
- package/dist/proof/audit.d.ts +47 -0
- package/dist/proof/audit.js +253 -0
- package/dist/proof/bots.d.ts +24 -0
- package/dist/proof/bots.js +60 -0
- package/dist/proof/dashboard.d.ts +8 -0
- package/dist/proof/dashboard.js +77 -0
- package/dist/proof/hits.d.ts +18 -0
- package/dist/proof/hits.js +51 -0
- package/dist/proof/jsonl.d.ts +5 -0
- package/dist/proof/jsonl.js +80 -0
- package/dist/proof/stats.d.ts +39 -0
- package/dist/proof/stats.js +127 -0
- package/dist/refine/check.d.ts +17 -0
- package/dist/refine/check.js +134 -0
- package/dist/refine/extract.d.ts +7 -0
- package/dist/refine/extract.js +120 -0
- package/dist/refine/file-store.d.ts +6 -0
- package/dist/refine/file-store.js +87 -0
- package/dist/refine/model.d.ts +30 -0
- package/dist/refine/model.js +105 -0
- package/dist/refine/prompts.d.ts +49 -0
- package/dist/refine/prompts.js +178 -0
- package/dist/refine/refine.d.ts +38 -0
- package/dist/refine/refine.js +75 -0
- package/dist/refine/run.d.ts +52 -0
- package/dist/refine/run.js +66 -0
- package/dist/refine/types.d.ts +17 -0
- package/dist/refine/types.js +8 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-admin.php +363 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-converter.php +762 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-llms.php +192 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-markdown-builder.php +447 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-negotiation.php +334 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-response.php +226 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-router.php +421 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-settings.php +115 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-store.php +543 -0
- package/dist/wp/relve-agent-ready/includes/index.php +2 -0
- package/dist/wp/relve-agent-ready/index.php +2 -0
- package/dist/wp/relve-agent-ready/readme.txt +75 -0
- package/dist/wp/relve-agent-ready/relve-agent-ready.php +70 -0
- package/dist/wp/relve-agent-ready/uninstall.php +24 -0
- package/package.json +64 -6
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type PageInput } from "./types";
|
|
2
|
+
export interface PromptSettings {
|
|
3
|
+
pageType: string;
|
|
4
|
+
version: number;
|
|
5
|
+
requiredSections: string[];
|
|
6
|
+
tokenBudget: number;
|
|
7
|
+
/** Kept from the POC table; not enforced yet. */
|
|
8
|
+
recallThreshold: number | null;
|
|
9
|
+
}
|
|
10
|
+
export interface RefinePrompt {
|
|
11
|
+
/** "<page_type>-v<version>", e.g. "tool-v1": the doc's prompt_version. */
|
|
12
|
+
id: string;
|
|
13
|
+
system: string;
|
|
14
|
+
user: string;
|
|
15
|
+
settings: PromptSettings;
|
|
16
|
+
}
|
|
17
|
+
export declare function parsePrompt(source: string): RefinePrompt;
|
|
18
|
+
export interface FilledPrompt {
|
|
19
|
+
system: string;
|
|
20
|
+
user: string;
|
|
21
|
+
}
|
|
22
|
+
export interface FillOptions {
|
|
23
|
+
generatedAt: string;
|
|
24
|
+
/** {{SITE_NAME}}. Default: the page's host without "www.". */
|
|
25
|
+
siteName?: string;
|
|
26
|
+
/** {{PAGE_KIND_HINT}}. Default: guessPageKind(input). */
|
|
27
|
+
kindHint?: PageKind;
|
|
28
|
+
}
|
|
29
|
+
/** Put the page into the prompt. Every {{PLACEHOLDER}} the recipe defines is filled. */
|
|
30
|
+
export declare function fillPrompt(prompt: RefinePrompt, input: PageInput, opts: FillOptions): FilledPrompt;
|
|
31
|
+
/** The page kinds guessPageKind can name; passed to a `page` prompt as {{PAGE_KIND_HINT}}. */
|
|
32
|
+
export declare const PAGE_KINDS: readonly ["home", "product", "pricing", "article", "docs", "listing", "event", "about", "contact", "legal", "other"];
|
|
33
|
+
export type PageKind = (typeof PAGE_KINDS)[number];
|
|
34
|
+
/**
|
|
35
|
+
* A first guess at the page's kind, from the path and JSON-LD. Only a hint:
|
|
36
|
+
* the prompt tells the model to confirm it from the page or pick better.
|
|
37
|
+
* Path rules win over generic JSON-LD (every page has an Organization block).
|
|
38
|
+
*/
|
|
39
|
+
export declare function guessPageKind(input: Pick<PageInput, "url" | "jsonLd">): PageKind;
|
|
40
|
+
/** The prompt key that takes any path no other prompt claims. */
|
|
41
|
+
export declare const FALLBACK_PROMPT = "page";
|
|
42
|
+
/** The prompt for a path: a matching page-type prompt, else the any-page prompt, else null. */
|
|
43
|
+
export declare function promptFor(path: string, prompts: Record<string, RefinePrompt>, rules?: PageTypeRules): RefinePrompt | null;
|
|
44
|
+
/** Which prompt a path uses: the first matching prefix. Relve's types by default. */
|
|
45
|
+
export type PageTypeRules = ReadonlyArray<readonly [prefix: string, pageType: string]>;
|
|
46
|
+
export declare const DEFAULT_PAGE_TYPES: PageTypeRules;
|
|
47
|
+
export declare function pageTypeFor(path: string, rules?: PageTypeRules): string | null;
|
|
48
|
+
/** Many prompt files -> the newest version per page type. */
|
|
49
|
+
export declare function promptsByType(prompts: RefinePrompt[]): Record<string, RefinePrompt>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Refine, step 2: a prompt file -> the model's messages.
|
|
3
|
+
//
|
|
4
|
+
// A prompt file is the POC's format (changes/markdown-poc/prompts/*-v1.md):
|
|
5
|
+
//
|
|
6
|
+
// ## SYSTEM the rules (may hold {{SOURCE_HASH}}, {{PROMPT_VERSION}},
|
|
7
|
+
// {{GENERATED_AT}} in its front matter template)
|
|
8
|
+
// ## USER the template the page is filled into ({{PAGE_URL}},
|
|
9
|
+
// {{PAGE_TEXT}}, {{PAGE_LINKS}}, {{JSON_LD}})
|
|
10
|
+
// ---
|
|
11
|
+
// ## Settings a table: page_type, version, required_sections,
|
|
12
|
+
// token_budget, recall_threshold
|
|
13
|
+
//
|
|
14
|
+
// The settings are checked by code (refine/check.ts), never by the model.
|
|
15
|
+
// EDGE-SAFE (pure).
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.DEFAULT_PAGE_TYPES = exports.FALLBACK_PROMPT = exports.PAGE_KINDS = void 0;
|
|
18
|
+
exports.parsePrompt = parsePrompt;
|
|
19
|
+
exports.fillPrompt = fillPrompt;
|
|
20
|
+
exports.guessPageKind = guessPageKind;
|
|
21
|
+
exports.promptFor = promptFor;
|
|
22
|
+
exports.pageTypeFor = pageTypeFor;
|
|
23
|
+
exports.promptsByType = promptsByType;
|
|
24
|
+
const types_1 = require("./types");
|
|
25
|
+
function parsePrompt(source) {
|
|
26
|
+
const md = source.replace(/^/, "").replace(/\r\n?/g, "\n");
|
|
27
|
+
const sys = /^## SYSTEM[ \t]*$/m.exec(md);
|
|
28
|
+
const usr = /^## USER[ \t]*$/m.exec(md);
|
|
29
|
+
const set = /^## Settings\b.*$/m.exec(md);
|
|
30
|
+
if (!sys || !usr || !set || !(sys.index < usr.index && usr.index < set.index)) {
|
|
31
|
+
throw new Error("agent-ready refine: a prompt needs '## SYSTEM', '## USER' and '## Settings', in that order.");
|
|
32
|
+
}
|
|
33
|
+
const system = md.slice(sys.index + sys[0].length, usr.index).trim();
|
|
34
|
+
const user = md
|
|
35
|
+
.slice(usr.index + usr[0].length, set.index)
|
|
36
|
+
.trim()
|
|
37
|
+
.replace(/\n-{3,}\s*$/, "")
|
|
38
|
+
.trim();
|
|
39
|
+
const rows = {};
|
|
40
|
+
for (const m of md.slice(set.index).matchAll(/^\|\s*`(\w+)`\s*\|(.+)\|\s*$/gm)) {
|
|
41
|
+
rows[m[1]] = Array.from(m[2].matchAll(/`([^`]+)`/g), (v) => v[1]);
|
|
42
|
+
}
|
|
43
|
+
const one = (k) => rows[k]?.[0];
|
|
44
|
+
const pageType = one("page_type");
|
|
45
|
+
const version = Number(one("version"));
|
|
46
|
+
const tokenBudget = Number(one("token_budget"));
|
|
47
|
+
if (!pageType || !Number.isFinite(version) || !Number.isFinite(tokenBudget)) {
|
|
48
|
+
throw new Error("agent-ready refine: the Settings table needs page_type, version and token_budget.");
|
|
49
|
+
}
|
|
50
|
+
const recall = Number(one("recall_threshold"));
|
|
51
|
+
return {
|
|
52
|
+
id: `${pageType}-v${version}`,
|
|
53
|
+
system,
|
|
54
|
+
user,
|
|
55
|
+
settings: {
|
|
56
|
+
pageType,
|
|
57
|
+
version,
|
|
58
|
+
requiredSections: rows.required_sections ?? [],
|
|
59
|
+
tokenBudget,
|
|
60
|
+
recallThreshold: Number.isFinite(recall) ? recall : null,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Put the page into the prompt. Every {{PLACEHOLDER}} the recipe defines is filled. */
|
|
65
|
+
function fillPrompt(prompt, input, opts) {
|
|
66
|
+
const u = new URL(input.url);
|
|
67
|
+
const vars = {
|
|
68
|
+
PAGE_URL: input.url,
|
|
69
|
+
PAGE_TEXT: input.text,
|
|
70
|
+
PAGE_LINKS: (0, types_1.formatLinks)(input.links),
|
|
71
|
+
JSON_LD: JSON.stringify(input.jsonLd, null, 2),
|
|
72
|
+
SOURCE_HASH: input.sourceHash,
|
|
73
|
+
PROMPT_VERSION: prompt.id,
|
|
74
|
+
GENERATED_AT: opts.generatedAt,
|
|
75
|
+
SITE_NAME: opts.siteName ?? u.hostname.replace(/^www\./, ""),
|
|
76
|
+
SITE_URL: u.origin,
|
|
77
|
+
PAGE_KIND_HINT: opts.kindHint ?? guessPageKind(input),
|
|
78
|
+
};
|
|
79
|
+
// A function replacer, so "$" in page text is never read as a pattern.
|
|
80
|
+
const fill = (s) => s.replace(/\{\{([A-Z_]+)\}\}/g, (m, k) => (k in vars ? vars[k] : m));
|
|
81
|
+
return { system: fill(prompt.system), user: fill(prompt.user) };
|
|
82
|
+
}
|
|
83
|
+
/** The page kinds guessPageKind can name; passed to a `page` prompt as {{PAGE_KIND_HINT}}. */
|
|
84
|
+
exports.PAGE_KINDS = ["home", "product", "pricing", "article", "docs", "listing", "event", "about", "contact", "legal", "other"];
|
|
85
|
+
const KIND_BY_LD = [
|
|
86
|
+
[/^(Article|BlogPosting|NewsArticle|TechArticle|Report|ScholarlyArticle)$/, "article"],
|
|
87
|
+
[/^(Event|BusinessEvent|EducationEvent|Hackathon|SocialEvent)$/, "event"],
|
|
88
|
+
[/^(Product|SoftwareApplication|WebApplication|MobileApplication|Service|Course)$/, "product"],
|
|
89
|
+
[/^(CollectionPage|ItemList|SearchResultsPage)$/, "listing"],
|
|
90
|
+
[/^(AboutPage|ProfilePage)$/, "about"],
|
|
91
|
+
[/^(ContactPage)$/, "contact"],
|
|
92
|
+
[/^(HowTo)$/, "docs"],
|
|
93
|
+
];
|
|
94
|
+
const KIND_BY_PATH = [
|
|
95
|
+
[/^\/(pricing|plans|prices)(\/|$)/, "pricing"],
|
|
96
|
+
[/^\/(blog|news|articles?|posts?|stories|insights|press|case-stud(y|ies)|resources\/blog)(\/|$)/, "article"],
|
|
97
|
+
[/^\/(docs?|documentation|help|support|guides?|kb|knowledge-base|faq|learn|api)(\/|$)/, "docs"],
|
|
98
|
+
[/^\/(events?|webinars?|conferences?)(\/|$)/, "event"],
|
|
99
|
+
[/^\/(about|about-us|company|team|careers|jobs|mission)(\/|$)/, "about"],
|
|
100
|
+
[/^\/(contact|contact-us|locations?|stores?)(\/|$)/, "contact"],
|
|
101
|
+
[/^\/(privacy|terms|legal|cookies?|policies|policy|refunds?|security|gdpr|tos)(-[\w-]+)?(\/|$)/, "legal"],
|
|
102
|
+
[/^\/(products?|services?|solutions?|features?|tools?|apps?|platform|integrations?)\/[^/]+/, "product"],
|
|
103
|
+
[/^\/(products?|services?|solutions?|tools?|category|categories|collections?|shop|catalog|directory|tags?)(\/|$)/, "listing"],
|
|
104
|
+
];
|
|
105
|
+
/**
|
|
106
|
+
* A first guess at the page's kind, from the path and JSON-LD. Only a hint:
|
|
107
|
+
* the prompt tells the model to confirm it from the page or pick better.
|
|
108
|
+
* Path rules win over generic JSON-LD (every page has an Organization block).
|
|
109
|
+
*/
|
|
110
|
+
function guessPageKind(input) {
|
|
111
|
+
let path = "/";
|
|
112
|
+
try {
|
|
113
|
+
path = new URL(input.url).pathname.replace(/\/+$/, "") || "/";
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// keep "/"
|
|
117
|
+
}
|
|
118
|
+
if (path === "/")
|
|
119
|
+
return "home";
|
|
120
|
+
for (const [re, kind] of KIND_BY_PATH)
|
|
121
|
+
if (re.test(path.toLowerCase()))
|
|
122
|
+
return kind;
|
|
123
|
+
for (const t of ldTypes(input.jsonLd))
|
|
124
|
+
for (const [re, kind] of KIND_BY_LD)
|
|
125
|
+
if (re.test(t))
|
|
126
|
+
return kind;
|
|
127
|
+
return "other";
|
|
128
|
+
}
|
|
129
|
+
function ldTypes(blocks) {
|
|
130
|
+
const out = [];
|
|
131
|
+
const visit = (v, depth) => {
|
|
132
|
+
if (!v || typeof v !== "object" || depth > 3)
|
|
133
|
+
return;
|
|
134
|
+
if (Array.isArray(v))
|
|
135
|
+
return v.forEach((x) => visit(x, depth + 1));
|
|
136
|
+
const o = v;
|
|
137
|
+
const t = o["@type"];
|
|
138
|
+
if (typeof t === "string")
|
|
139
|
+
out.push(t);
|
|
140
|
+
else if (Array.isArray(t))
|
|
141
|
+
out.push(...t.filter((x) => typeof x === "string"));
|
|
142
|
+
if (o["@graph"])
|
|
143
|
+
visit(o["@graph"], depth + 1);
|
|
144
|
+
if (o.mainEntity)
|
|
145
|
+
visit(o.mainEntity, depth + 1);
|
|
146
|
+
};
|
|
147
|
+
visit(blocks, 0);
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
/** The prompt key that takes any path no other prompt claims. */
|
|
151
|
+
exports.FALLBACK_PROMPT = "page";
|
|
152
|
+
/** The prompt for a path: a matching page-type prompt, else the any-page prompt, else null. */
|
|
153
|
+
function promptFor(path, prompts, rules = exports.DEFAULT_PAGE_TYPES) {
|
|
154
|
+
const type = pageTypeFor(path, rules);
|
|
155
|
+
return (type && prompts[type]) || prompts[exports.FALLBACK_PROMPT] || null;
|
|
156
|
+
}
|
|
157
|
+
exports.DEFAULT_PAGE_TYPES = [
|
|
158
|
+
["/tool/", "tool"],
|
|
159
|
+
["/agent/", "agent"],
|
|
160
|
+
["/events/", "event"],
|
|
161
|
+
["/blog/", "blog"],
|
|
162
|
+
];
|
|
163
|
+
function pageTypeFor(path, rules = exports.DEFAULT_PAGE_TYPES) {
|
|
164
|
+
for (const [prefix, type] of rules)
|
|
165
|
+
if (path.startsWith(prefix) && path.length > prefix.length)
|
|
166
|
+
return type;
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
/** Many prompt files -> the newest version per page type. */
|
|
170
|
+
function promptsByType(prompts) {
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const p of prompts) {
|
|
173
|
+
const cur = out[p.settings.pageType];
|
|
174
|
+
if (!cur || p.settings.version > cur.settings.version)
|
|
175
|
+
out[p.settings.pageType] = p;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type CheckIssue, type CheckOptions } from "./check";
|
|
2
|
+
import { type ModelClient } from "./model";
|
|
3
|
+
import { type RefinePrompt } from "./prompts";
|
|
4
|
+
import type { PageInput } from "./types";
|
|
5
|
+
export type RefineStatus = "ok" | "unchanged" | "rejected" | "error";
|
|
6
|
+
export interface RefineOptions extends CheckOptions {
|
|
7
|
+
/** Absolute page URL. */
|
|
8
|
+
url: string;
|
|
9
|
+
prompt: RefinePrompt;
|
|
10
|
+
model: ModelClient;
|
|
11
|
+
/** The page's HTML, when the caller already has it (else it is fetched). */
|
|
12
|
+
html?: string;
|
|
13
|
+
fetch?: typeof fetch;
|
|
14
|
+
/** The stored doc's fingerprint: equal to the page's -> "unchanged", no model call. */
|
|
15
|
+
previousHash?: string | null;
|
|
16
|
+
/** Extra attempts after a failed check, each told what failed. Default 1. */
|
|
17
|
+
retries?: number;
|
|
18
|
+
now?: () => Date;
|
|
19
|
+
/** {{SITE_NAME}} for the any-page prompt. Default: the host. */
|
|
20
|
+
siteName?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RefineResult {
|
|
23
|
+
status: RefineStatus;
|
|
24
|
+
url: string;
|
|
25
|
+
input: PageInput | null;
|
|
26
|
+
/** The last document the model wrote (kept on "rejected", for review). */
|
|
27
|
+
doc: string | null;
|
|
28
|
+
issues: CheckIssue[];
|
|
29
|
+
attempts: number;
|
|
30
|
+
tokens: number | null;
|
|
31
|
+
generatedAt: string | null;
|
|
32
|
+
usage: {
|
|
33
|
+
inputTokens: number;
|
|
34
|
+
outputTokens: number;
|
|
35
|
+
};
|
|
36
|
+
error: string | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function refinePage(opts: RefineOptions): Promise<RefineResult>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Refine one page: fetch -> extract -> (unchanged? stop) -> fill the prompt ->
|
|
3
|
+
// model -> check -> retry once with the issues -> result.
|
|
4
|
+
// Nothing is stored here; the runner decides (refine/run.ts).
|
|
5
|
+
//
|
|
6
|
+
// NODE ONLY (extract uses linkedom).
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.refinePage = refinePage;
|
|
9
|
+
const extract_1 = require("./extract");
|
|
10
|
+
const check_1 = require("./check");
|
|
11
|
+
const model_1 = require("./model");
|
|
12
|
+
const prompts_1 = require("./prompts");
|
|
13
|
+
async function refinePage(opts) {
|
|
14
|
+
const result = {
|
|
15
|
+
status: "error",
|
|
16
|
+
url: opts.url,
|
|
17
|
+
input: null,
|
|
18
|
+
doc: null,
|
|
19
|
+
issues: [],
|
|
20
|
+
attempts: 0,
|
|
21
|
+
tokens: null,
|
|
22
|
+
generatedAt: null,
|
|
23
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
24
|
+
error: null,
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
const html = opts.html ?? (await fetchPage(opts.url, opts.fetch ?? fetch));
|
|
28
|
+
const input = await (0, extract_1.extractInput)(html, { url: opts.url });
|
|
29
|
+
result.input = input;
|
|
30
|
+
if (opts.previousHash && opts.previousHash === input.sourceHash) {
|
|
31
|
+
result.status = "unchanged";
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
const generatedAt = (opts.now ?? (() => new Date()))().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
35
|
+
result.generatedAt = generatedAt;
|
|
36
|
+
const filled = (0, prompts_1.fillPrompt)(opts.prompt, input, { generatedAt, siteName: opts.siteName });
|
|
37
|
+
const maxAttempts = 1 + Math.max(0, opts.retries ?? 1);
|
|
38
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
39
|
+
result.attempts = attempt;
|
|
40
|
+
const user = attempt === 1 ? filled.user : `${filled.user}\n\n${retryNote(result.issues)}`;
|
|
41
|
+
const reply = await opts.model({ system: filled.system, user });
|
|
42
|
+
result.usage.inputTokens += reply.inputTokens ?? 0;
|
|
43
|
+
result.usage.outputTokens += reply.outputTokens ?? 0;
|
|
44
|
+
const doc = (0, model_1.cleanReply)(reply.text);
|
|
45
|
+
const check = (0, check_1.checkDoc)(doc, input, opts.prompt.settings, opts);
|
|
46
|
+
result.doc = doc;
|
|
47
|
+
result.issues = check.issues;
|
|
48
|
+
result.tokens = check.tokens;
|
|
49
|
+
if (check.ok) {
|
|
50
|
+
result.status = "ok";
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
result.status = "rejected";
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
result.status = "error";
|
|
59
|
+
result.error = err?.message ?? String(err);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function retryNote(issues) {
|
|
64
|
+
const list = issues.map((i) => `- ${i.kind}: ${i.detail}`).join("\n");
|
|
65
|
+
return `<check_failed>\nYour previous document failed the automatic check:\n${list}\nRemove anything that is not in the input and fix the rest. Output the corrected document only.\n</check_failed>`;
|
|
66
|
+
}
|
|
67
|
+
async function fetchPage(url, f) {
|
|
68
|
+
const res = await f(url, {
|
|
69
|
+
headers: { accept: "text/html", "user-agent": "agent-ready-refine/0.1" },
|
|
70
|
+
redirect: "follow",
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok)
|
|
73
|
+
throw new Error(`page fetch failed: HTTP ${res.status}`);
|
|
74
|
+
return res.text();
|
|
75
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type RefineResult } from "./refine";
|
|
2
|
+
import { type PageTypeRules, type RefinePrompt } from "./prompts";
|
|
3
|
+
import type { CheckOptions } from "./check";
|
|
4
|
+
import type { ModelClient } from "./model";
|
|
5
|
+
export interface StoredDoc {
|
|
6
|
+
path: string;
|
|
7
|
+
url: string;
|
|
8
|
+
markdown: string;
|
|
9
|
+
sourceHash: string;
|
|
10
|
+
promptId: string;
|
|
11
|
+
generatedAt: string;
|
|
12
|
+
tokens: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RefineStore {
|
|
15
|
+
/** The fingerprint the stored doc was written from, or null. */
|
|
16
|
+
lastHash(path: string): Promise<string | null>;
|
|
17
|
+
save(doc: StoredDoc): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface RunOptions extends CheckOptions {
|
|
20
|
+
/** Public origin, e.g. "https://relvehq.com". */
|
|
21
|
+
site: string;
|
|
22
|
+
paths: string[];
|
|
23
|
+
/** Prompts by page type (promptsByType). */
|
|
24
|
+
prompts: Record<string, RefinePrompt>;
|
|
25
|
+
pageTypes?: PageTypeRules;
|
|
26
|
+
model: ModelClient;
|
|
27
|
+
store: RefineStore;
|
|
28
|
+
/** Refine even when the fingerprint is unchanged. */
|
|
29
|
+
force?: boolean;
|
|
30
|
+
/** Pages at a time. Default 2. */
|
|
31
|
+
concurrency?: number;
|
|
32
|
+
retries?: number;
|
|
33
|
+
fetch?: typeof fetch;
|
|
34
|
+
now?: () => Date;
|
|
35
|
+
siteName?: string;
|
|
36
|
+
onResult?: (r: RunItem) => void;
|
|
37
|
+
}
|
|
38
|
+
export interface RunItem extends Partial<Omit<RefineResult, "status">> {
|
|
39
|
+
path: string;
|
|
40
|
+
status: RefineResult["status"] | "no-prompt";
|
|
41
|
+
promptId: string | null;
|
|
42
|
+
saved: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface RunReport {
|
|
45
|
+
items: RunItem[];
|
|
46
|
+
counts: Record<RunItem["status"], number>;
|
|
47
|
+
usage: {
|
|
48
|
+
inputTokens: number;
|
|
49
|
+
outputTokens: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export declare function runRefine(opts: RunOptions): Promise<RunReport>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Refine, step 5: the runner. Refines a list of pages, skips pages whose
|
|
3
|
+
// fingerprint hasn't changed ("stay fresh"), saves what passes the check.
|
|
4
|
+
//
|
|
5
|
+
// await runRefine({ site, paths, prompts, model, store })
|
|
6
|
+
//
|
|
7
|
+
// The store is anything with lastHash() and save(): the file store
|
|
8
|
+
// (refine/file-store.ts), a database, a test double.
|
|
9
|
+
// NODE ONLY (through refinePage).
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.runRefine = runRefine;
|
|
12
|
+
const refine_1 = require("./refine");
|
|
13
|
+
const prompts_1 = require("./prompts");
|
|
14
|
+
async function runRefine(opts) {
|
|
15
|
+
const site = opts.site.replace(/\/+$/, "");
|
|
16
|
+
const items = new Array(opts.paths.length);
|
|
17
|
+
let next = 0;
|
|
18
|
+
const one = async (path) => {
|
|
19
|
+
const prompt = (0, prompts_1.promptFor)(path, opts.prompts, opts.pageTypes);
|
|
20
|
+
if (!prompt)
|
|
21
|
+
return { path, status: "no-prompt", promptId: null, saved: false };
|
|
22
|
+
const previousHash = opts.force ? null : await opts.store.lastHash(path);
|
|
23
|
+
const r = await (0, refine_1.refinePage)({
|
|
24
|
+
url: site + path,
|
|
25
|
+
prompt,
|
|
26
|
+
model: opts.model,
|
|
27
|
+
previousHash,
|
|
28
|
+
retries: opts.retries,
|
|
29
|
+
fetch: opts.fetch,
|
|
30
|
+
now: opts.now,
|
|
31
|
+
siteName: opts.siteName,
|
|
32
|
+
countTokens: opts.countTokens,
|
|
33
|
+
});
|
|
34
|
+
let saved = false;
|
|
35
|
+
if (r.status === "ok" && r.doc && r.input && r.generatedAt) {
|
|
36
|
+
await opts.store.save({
|
|
37
|
+
path,
|
|
38
|
+
url: r.url,
|
|
39
|
+
markdown: r.doc,
|
|
40
|
+
sourceHash: r.input.sourceHash,
|
|
41
|
+
promptId: prompt.id,
|
|
42
|
+
generatedAt: r.generatedAt,
|
|
43
|
+
tokens: r.tokens ?? 0,
|
|
44
|
+
});
|
|
45
|
+
saved = true;
|
|
46
|
+
}
|
|
47
|
+
return { ...r, path, promptId: prompt.id, saved };
|
|
48
|
+
};
|
|
49
|
+
const worker = async () => {
|
|
50
|
+
while (next < opts.paths.length) {
|
|
51
|
+
const i = next++;
|
|
52
|
+
const item = await one(opts.paths[i]);
|
|
53
|
+
items[i] = item;
|
|
54
|
+
opts.onResult?.(item);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(opts.concurrency ?? 2, opts.paths.length)) }, worker));
|
|
58
|
+
const counts = { ok: 0, unchanged: 0, rejected: 0, error: 0, "no-prompt": 0 };
|
|
59
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
60
|
+
for (const it of items) {
|
|
61
|
+
counts[it.status]++;
|
|
62
|
+
usage.inputTokens += it.usage?.inputTokens ?? 0;
|
|
63
|
+
usage.outputTokens += it.usage?.outputTokens ?? 0;
|
|
64
|
+
}
|
|
65
|
+
return { items, counts, usage };
|
|
66
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface PageLink {
|
|
2
|
+
url: string;
|
|
3
|
+
text: string;
|
|
4
|
+
}
|
|
5
|
+
/** The model's input for one page (see refine/extract.ts). */
|
|
6
|
+
export interface PageInput {
|
|
7
|
+
/** Absolute page URL. */
|
|
8
|
+
url: string;
|
|
9
|
+
text: string;
|
|
10
|
+
links: PageLink[];
|
|
11
|
+
jsonLd: unknown[];
|
|
12
|
+
/** SHA-256 hex of `text + "\n" + JSON.stringify(jsonLd)`. */
|
|
13
|
+
sourceHash: string;
|
|
14
|
+
mainSource: "main" | "article" | "body";
|
|
15
|
+
}
|
|
16
|
+
/** Links as the prompt's {{PAGE_LINKS}} block: "url | text" per line. */
|
|
17
|
+
export declare function formatLinks(links: PageLink[]): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared shapes for refine. EDGE-SAFE (no imports).
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.formatLinks = formatLinks;
|
|
5
|
+
/** Links as the prompt's {{PAGE_LINKS}} block: "url | text" per line. */
|
|
6
|
+
function formatLinks(links) {
|
|
7
|
+
return links.map((l) => (l.text ? `${l.url} | ${l.text}` : l.url)).join("\n");
|
|
8
|
+
}
|