@relvehq/agent-ready 0.0.0-stage → 0.3.0
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 +364 -3
- package/SETUP.md +354 -0
- package/dist/bin/agent-ready.d.ts +23 -0
- package/dist/bin/agent-ready.js +444 -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/remote.d.ts +57 -0
- package/dist/refine/remote.js +238 -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 +550 -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 +455 -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-refiner.php +894 -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 +201 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-store.php +583 -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 +108 -0
- package/dist/wp/relve-agent-ready/relve-agent-ready.php +81 -0
- package/dist/wp/relve-agent-ready/uninstall.php +35 -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,57 @@
|
|
|
1
|
+
export declare const PACKAGE_VERSION = "0.3.0";
|
|
2
|
+
export declare const DEFAULT_REFINE_ENDPOINT = "https://relvehq.com/api/agent-ready/v1/refine";
|
|
3
|
+
export declare const REFINE_MODELS: readonly ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini"];
|
|
4
|
+
export declare const USER_AGENT = "agent-ready/0.3.0 (+https://relvehq.com/agent-ready)";
|
|
5
|
+
export type RemoteStatus = "ok" | "unchanged" | "rejected" | "error" | "skipped";
|
|
6
|
+
export interface RemoteItem {
|
|
7
|
+
path: string;
|
|
8
|
+
status: RemoteStatus;
|
|
9
|
+
/** Error code from the contract (e.g. "openai_auth", "too_little_text"), or a local one ("page_fetch", "network", "timeout", "bad_response"). */
|
|
10
|
+
code?: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
/** Check messages on "rejected". */
|
|
13
|
+
issues?: string[];
|
|
14
|
+
tokens?: number;
|
|
15
|
+
kind?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
saved: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface RemoteReport {
|
|
20
|
+
items: RemoteItem[];
|
|
21
|
+
counts: Record<RemoteStatus, number>;
|
|
22
|
+
tokens: number;
|
|
23
|
+
/** The error code that stopped the run early, or null. */
|
|
24
|
+
stopped: string | null;
|
|
25
|
+
dir: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RemoteOptions {
|
|
28
|
+
/** Public origin, e.g. "https://your-site.com". */
|
|
29
|
+
site: string;
|
|
30
|
+
paths: string[];
|
|
31
|
+
/** The caller's OpenAI key. Sent in X-OpenAI-Key, nothing else. */
|
|
32
|
+
apiKey: string;
|
|
33
|
+
/** Output folder. Default ".agent-ready/md". */
|
|
34
|
+
outDir?: string;
|
|
35
|
+
endpoint?: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
siteName?: string;
|
|
38
|
+
/** Ignore saved fingerprints. */
|
|
39
|
+
force?: boolean;
|
|
40
|
+
/** Pages at a time. Default 2. */
|
|
41
|
+
concurrency?: number;
|
|
42
|
+
fetch?: typeof fetch;
|
|
43
|
+
/** Relve API timeout. Default 90 s. */
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
/** Page fetch timeout. Default 30 s. */
|
|
46
|
+
pageTimeoutMs?: number;
|
|
47
|
+
/** Wait before retrying a 502/504. Default 2 s. */
|
|
48
|
+
retryDelayMs?: number;
|
|
49
|
+
now?: () => Date;
|
|
50
|
+
sleep?: (ms: number) => Promise<void>;
|
|
51
|
+
onResult?: (item: RemoteItem) => void;
|
|
52
|
+
}
|
|
53
|
+
/** The saved JSON's fingerprint, else one built from a local run's sourceHash, else null. */
|
|
54
|
+
export declare function savedFingerprint(outDir: string, path: string): Promise<string | null>;
|
|
55
|
+
export declare function refineRemote(opts: RemoteOptions): Promise<RemoteReport>;
|
|
56
|
+
/** What a stop code means, for the CLI. */
|
|
57
|
+
export declare function stopMessage(code: string): string;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Refine through the Relve API, with the caller's own OpenAI key (BYOK).
|
|
3
|
+
// changes/agent-ready-byok-refine/contract.md is the wire format.
|
|
4
|
+
//
|
|
5
|
+
// await refineRemote({ site, paths, apiKey, outDir })
|
|
6
|
+
//
|
|
7
|
+
// For each page: fetch its HTML here, read the saved fingerprint from
|
|
8
|
+
// `<out>/<path>.json`, POST { url, html, siteName?, model?, previousFingerprint? }
|
|
9
|
+
// to the endpoint with the key in `X-OpenAI-Key`, and save `ok` results as
|
|
10
|
+
// `<out>/<path>.md` + `<out>/<path>.json` through fileStore, so the files are
|
|
11
|
+
// served exactly like the ones local refine writes. Relve's prompt runs on
|
|
12
|
+
// Relve's server; it never reaches this machine. The key is sent with each
|
|
13
|
+
// call and is never written, logged or put in an error message here.
|
|
14
|
+
//
|
|
15
|
+
// NODE ONLY (fileStore, node:fs).
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.USER_AGENT = exports.REFINE_MODELS = exports.DEFAULT_REFINE_ENDPOINT = exports.PACKAGE_VERSION = void 0;
|
|
51
|
+
exports.savedFingerprint = savedFingerprint;
|
|
52
|
+
exports.refineRemote = refineRemote;
|
|
53
|
+
exports.stopMessage = stopMessage;
|
|
54
|
+
const file_store_1 = require("./file-store");
|
|
55
|
+
exports.PACKAGE_VERSION = "0.3.0";
|
|
56
|
+
exports.DEFAULT_REFINE_ENDPOINT = "https://relvehq.com/api/agent-ready/v1/refine";
|
|
57
|
+
exports.REFINE_MODELS = ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini"];
|
|
58
|
+
exports.USER_AGENT = `agent-ready/${exports.PACKAGE_VERSION} (+https://relvehq.com/agent-ready)`;
|
|
59
|
+
/** Errors that mean every later call would fail the same way: the run stops. */
|
|
60
|
+
const FATAL = new Set(["openai_auth", "openai_quota", "missing_key"]);
|
|
61
|
+
/** The saved JSON's fingerprint, else one built from a local run's sourceHash, else null. */
|
|
62
|
+
async function savedFingerprint(outDir, path) {
|
|
63
|
+
try {
|
|
64
|
+
const { readFile } = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
|
|
65
|
+
const meta = JSON.parse(await readFile(metaFile(outDir, path), "utf8"));
|
|
66
|
+
if (typeof meta.fingerprint === "string" && meta.fingerprint)
|
|
67
|
+
return meta.fingerprint;
|
|
68
|
+
if (typeof meta.sourceHash === "string" && meta.sourceHash)
|
|
69
|
+
return meta.sourceHash.startsWith("sha256:") ? meta.sourceHash : `sha256:${meta.sourceHash}`;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** `<out>/<path>.json`, the same name fileStore uses. */
|
|
77
|
+
function metaFile(outDir, path) {
|
|
78
|
+
const clean = path.replace(/^\/+|\/+$/g, "");
|
|
79
|
+
return `${outDir.replace(/[\\/]+$/, "")}/${clean || "index"}.json`;
|
|
80
|
+
}
|
|
81
|
+
async function refineRemote(opts) {
|
|
82
|
+
const site = opts.site.replace(/\/+$/, "");
|
|
83
|
+
const dir = opts.outDir ?? ".agent-ready/md";
|
|
84
|
+
const store = (0, file_store_1.fileStore)(dir);
|
|
85
|
+
const f = opts.fetch ?? fetch;
|
|
86
|
+
const endpoint = opts.endpoint ?? exports.DEFAULT_REFINE_ENDPOINT;
|
|
87
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
88
|
+
const now = opts.now ?? (() => new Date());
|
|
89
|
+
const items = opts.paths.map((path) => ({ path, status: "skipped", saved: false }));
|
|
90
|
+
let next = 0;
|
|
91
|
+
let stopped = null;
|
|
92
|
+
const one = async (path) => {
|
|
93
|
+
const url = site + path;
|
|
94
|
+
let html;
|
|
95
|
+
try {
|
|
96
|
+
html = await fetchPage(f, url, opts.pageTimeoutMs ?? 30000);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return { path, status: "error", code: "page_fetch", message: err.message, saved: false };
|
|
100
|
+
}
|
|
101
|
+
const previousFingerprint = opts.force ? null : await savedFingerprint(dir, path);
|
|
102
|
+
const body = JSON.stringify({
|
|
103
|
+
url,
|
|
104
|
+
html,
|
|
105
|
+
...(opts.siteName ? { siteName: opts.siteName } : {}),
|
|
106
|
+
...(opts.model ? { model: opts.model } : {}),
|
|
107
|
+
...(previousFingerprint ? { previousFingerprint } : {}),
|
|
108
|
+
});
|
|
109
|
+
let reply = null;
|
|
110
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
111
|
+
reply = await post(f, endpoint, opts.apiKey, body, opts.timeoutMs ?? 90000);
|
|
112
|
+
if (attempt > 0)
|
|
113
|
+
break;
|
|
114
|
+
if (reply.httpStatus === 429 && reply.json?.error === "rate_limited") {
|
|
115
|
+
const after = Number(reply.json.retryAfter ?? reply.retryAfterHeader ?? 10);
|
|
116
|
+
await sleep(Math.min(Number.isFinite(after) && after >= 0 ? after : 10, 60) * 1000);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (reply.httpStatus === 502 || reply.httpStatus === 504) {
|
|
120
|
+
await sleep(opts.retryDelayMs ?? 2000);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
const r = reply;
|
|
126
|
+
if (r.local)
|
|
127
|
+
return { path, status: "error", code: r.local.code, message: r.local.message, saved: false };
|
|
128
|
+
const j = r.json ?? {};
|
|
129
|
+
if (r.httpStatus === 200 && j.status === "ok" && typeof j.markdown === "string" && typeof j.fingerprint === "string") {
|
|
130
|
+
const stamp = now().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
131
|
+
const tokens = Number(j.tokens) || 0;
|
|
132
|
+
const doc = {
|
|
133
|
+
path,
|
|
134
|
+
url,
|
|
135
|
+
markdown: j.markdown,
|
|
136
|
+
// sourceHash keeps local refine's freshness check working on these files.
|
|
137
|
+
sourceHash: j.fingerprint.replace(/^sha256:/, ""),
|
|
138
|
+
promptId: String(j.prompt ?? "page-v1"),
|
|
139
|
+
generatedAt: stamp,
|
|
140
|
+
tokens,
|
|
141
|
+
fingerprint: j.fingerprint,
|
|
142
|
+
model: j.model ?? opts.model ?? null,
|
|
143
|
+
prompt: j.prompt ?? null,
|
|
144
|
+
kind: j.kind ?? null,
|
|
145
|
+
refinedAt: stamp,
|
|
146
|
+
source: "relve-api",
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
await store.save(doc);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
return { path, status: "error", code: "write", message: err.message, saved: false };
|
|
153
|
+
}
|
|
154
|
+
return { path, status: "ok", tokens, kind: j.kind ?? undefined, model: j.model ?? undefined, saved: true };
|
|
155
|
+
}
|
|
156
|
+
if (r.httpStatus === 200 && j.status === "unchanged")
|
|
157
|
+
return { path, status: "unchanged", saved: false };
|
|
158
|
+
if (j.status === "rejected") {
|
|
159
|
+
return { path, status: "rejected", code: "rejected", issues: Array.isArray(j.issues) ? j.issues.map(String) : [], saved: false };
|
|
160
|
+
}
|
|
161
|
+
const code = typeof j.error === "string" ? j.error : r.httpStatus === 200 ? "bad_response" : `http_${r.httpStatus}`;
|
|
162
|
+
return { path, status: "error", code, message: typeof j.message === "string" ? j.message : undefined, saved: false };
|
|
163
|
+
};
|
|
164
|
+
const worker = async () => {
|
|
165
|
+
while (!stopped && next < opts.paths.length) {
|
|
166
|
+
const i = next++;
|
|
167
|
+
const item = await one(opts.paths[i]);
|
|
168
|
+
items[i] = item;
|
|
169
|
+
if (item.status === "error" && item.code && FATAL.has(item.code))
|
|
170
|
+
stopped = item.code;
|
|
171
|
+
opts.onResult?.(item);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(opts.concurrency ?? 2, opts.paths.length || 1)) }, worker));
|
|
175
|
+
const counts = { ok: 0, unchanged: 0, rejected: 0, error: 0, skipped: 0 };
|
|
176
|
+
let tokens = 0;
|
|
177
|
+
for (const it of items) {
|
|
178
|
+
counts[it.status]++;
|
|
179
|
+
tokens += it.tokens ?? 0;
|
|
180
|
+
}
|
|
181
|
+
return { items, counts, tokens, stopped, dir: store.dir };
|
|
182
|
+
}
|
|
183
|
+
/** What a stop code means, for the CLI. */
|
|
184
|
+
function stopMessage(code) {
|
|
185
|
+
if (code === "openai_auth")
|
|
186
|
+
return "OpenAI rejected the key in OPENAI_API_KEY. Check it at https://platform.openai.com/api-keys and run again.";
|
|
187
|
+
if (code === "openai_quota")
|
|
188
|
+
return "Your OpenAI account is out of quota or has no billing set up. Check https://platform.openai.com/settings/organization/billing and run again.";
|
|
189
|
+
if (code === "missing_key")
|
|
190
|
+
return "The Relve API got no key. Set OPENAI_API_KEY and run again.";
|
|
191
|
+
return code;
|
|
192
|
+
}
|
|
193
|
+
async function post(f, endpoint, apiKey, body, timeoutMs) {
|
|
194
|
+
const ctl = new AbortController();
|
|
195
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
196
|
+
try {
|
|
197
|
+
const res = await f(endpoint, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: { "content-type": "application/json", accept: "application/json", "x-openai-key": apiKey, "user-agent": exports.USER_AGENT },
|
|
200
|
+
body,
|
|
201
|
+
signal: ctl.signal,
|
|
202
|
+
});
|
|
203
|
+
const text = await res.text();
|
|
204
|
+
let json = null;
|
|
205
|
+
try {
|
|
206
|
+
json = JSON.parse(text);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return { httpStatus: res.status, json: null, retryAfterHeader: null, local: { code: res.ok ? "bad_response" : `http_${res.status}`, message: "the Relve API did not answer with JSON" } };
|
|
210
|
+
}
|
|
211
|
+
return { httpStatus: res.status, json, retryAfterHeader: res.headers.get("retry-after") };
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
const aborted = err?.name === "AbortError" || ctl.signal.aborted;
|
|
215
|
+
return {
|
|
216
|
+
httpStatus: 0,
|
|
217
|
+
json: null,
|
|
218
|
+
retryAfterHeader: null,
|
|
219
|
+
local: aborted ? { code: "timeout", message: `no answer from the Relve API in ${Math.round(timeoutMs / 1000)} s` } : { code: "network", message: err?.message ?? "request failed" },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function fetchPage(f, url, timeoutMs) {
|
|
227
|
+
const ctl = new AbortController();
|
|
228
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
229
|
+
try {
|
|
230
|
+
const res = await f(url, { headers: { accept: "text/html", "user-agent": exports.USER_AGENT }, redirect: "follow", signal: ctl.signal });
|
|
231
|
+
if (!res.ok)
|
|
232
|
+
throw new Error(`HTTP ${res.status}`);
|
|
233
|
+
return await res.text();
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
}
|
|
238
|
+
}
|