@spendgraph/prompt 0.5.0 → 0.7.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 +202 -0
- package/README.md +27 -221
- package/dist/budget/budget.d.ts +10 -0
- package/dist/budget/budget.js +1 -31
- package/dist/budget/errors.js +1 -11
- package/dist/budget/index.js +1 -2
- package/dist/build/build.d.ts +11 -2
- package/dist/build/build.js +1 -57
- package/dist/build/index.js +1 -1
- package/dist/cache/cache.d.ts +16 -0
- package/dist/cache/cache.js +1 -128
- package/dist/cache/index.js +1 -1
- package/dist/client.d.ts +27 -1
- package/dist/client.js +1 -145
- package/dist/fingerprint/fingerprint.d.ts +23 -0
- package/dist/fingerprint/fingerprint.js +1 -0
- package/dist/fingerprint/index.d.ts +2 -0
- package/dist/fingerprint/index.js +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -5
- package/dist/internals.d.ts +4 -2
- package/dist/internals.js +1 -6
- package/dist/pack/index.d.ts +3 -0
- package/dist/pack/index.js +1 -0
- package/dist/pack/pack.d.ts +58 -0
- package/dist/pack/pack.js +1 -0
- package/dist/pack/tokens.d.ts +11 -0
- package/dist/pack/tokens.js +1 -0
- package/dist/prompt/bind.d.ts +13 -18
- package/dist/prompt/bind.js +1 -15
- package/dist/prompt/factory.d.ts +1 -1
- package/dist/prompt/factory.js +1 -100
- package/dist/prompt/index.js +1 -2
- package/dist/pull/index.js +1 -1
- package/dist/pull/pull.js +1 -19
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -3
- package/dist/render/messages.js +1 -14
- package/dist/render/record.d.ts +12 -0
- package/dist/render/record.js +1 -0
- package/dist/render/system.js +4 -13
- package/dist/render/types.d.ts +21 -5
- package/dist/render/types.js +0 -1
- package/dist/render/variables.d.ts +7 -0
- package/dist/render/variables.js +1 -20
- package/dist/run/across-models.d.ts +11 -1
- package/dist/run/across-models.js +1 -5
- package/dist/run/concurrency.d.ts +1 -7
- package/dist/run/concurrency.js +1 -12
- package/dist/run/id.js +1 -3
- package/dist/run/index.js +1 -5
- package/dist/run/once.js +1 -21
- package/dist/run/sample.js +1 -28
- package/dist/types/dataset.js +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/payload.js +0 -1
- package/dist/types/prompt.d.ts +6 -1
- package/dist/types/prompt.js +0 -1
- package/dist/types/result.js +0 -1
- package/dist/types/rollout.js +0 -1
- package/dist/types/run.js +0 -1
- package/dist/types/trace.js +0 -1
- package/dist/types/version.js +0 -1
- package/docs/caching.mdx +46 -0
- package/docs/calling.mdx +58 -0
- package/docs/datasets.mdx +61 -0
- package/docs/fields.mdx +63 -0
- package/docs/overview.mdx +78 -0
- package/docs/running.mdx +55 -0
- package/package.json +12 -11
package/dist/prompt/factory.js
CHANGED
|
@@ -1,100 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { renderMessages } from "../render/index.js";
|
|
3
|
-
import { newRolloutId } from "../run/id.js";
|
|
4
|
-
import { bind } from "./bind.js";
|
|
5
|
-
export const noRecorder = {
|
|
6
|
-
enabled: false,
|
|
7
|
-
write: () => { },
|
|
8
|
-
};
|
|
9
|
-
export const MAX_RECORDED_MESSAGES = 50;
|
|
10
|
-
let warnedAboutLength = false;
|
|
11
|
-
function warnIfUnrecordable(messages) {
|
|
12
|
-
if (warnedAboutLength || messages.length <= MAX_RECORDED_MESSAGES)
|
|
13
|
-
return;
|
|
14
|
-
warnedAboutLength = true;
|
|
15
|
-
console.warn(`[prompt] ${messages.length} messages is over the ${MAX_RECORDED_MESSAGES} a rollout ` +
|
|
16
|
-
"can record. The call still runs; the rollout will be rejected. Trim the history you pass.");
|
|
17
|
-
}
|
|
18
|
-
function lastUserMessage(messages) {
|
|
19
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
20
|
-
if (messages[i].role === "user")
|
|
21
|
-
return messages[i].content;
|
|
22
|
-
}
|
|
23
|
-
return "";
|
|
24
|
-
}
|
|
25
|
-
export function makePrompt(shape, source, recorder = noRecorder) {
|
|
26
|
-
const serialize = (values = {}) => serializeFields(values, shape.fields);
|
|
27
|
-
const format = (values = {}, opts = {}) => renderMessages(shape.blocks, shape.question, serialize(values), opts.history);
|
|
28
|
-
const built = {
|
|
29
|
-
id: shape.id,
|
|
30
|
-
name: shape.name,
|
|
31
|
-
slug: shape.slug,
|
|
32
|
-
fields: shape.fields,
|
|
33
|
-
versionId: shape.versionId,
|
|
34
|
-
models: shape.models,
|
|
35
|
-
source,
|
|
36
|
-
serialize,
|
|
37
|
-
format,
|
|
38
|
-
async call(values, run, opts = {}) {
|
|
39
|
-
const messages = format(values, { history: opts.history });
|
|
40
|
-
warnIfUnrecordable(messages);
|
|
41
|
-
const turn = opts.tools?.trace(opts.query ?? lastUserMessage(messages));
|
|
42
|
-
const rolloutId = opts.rolloutId ?? newRolloutId();
|
|
43
|
-
const recording = recorder.enabled && (opts.report ?? true);
|
|
44
|
-
const startedAt = Date.now();
|
|
45
|
-
const sent = {
|
|
46
|
-
rolloutId,
|
|
47
|
-
versionId: shape.versionId,
|
|
48
|
-
fields: serialize(values),
|
|
49
|
-
rendered: messages,
|
|
50
|
-
};
|
|
51
|
-
let outcome;
|
|
52
|
-
try {
|
|
53
|
-
outcome = await run({ messages, turn });
|
|
54
|
-
}
|
|
55
|
-
catch (err) {
|
|
56
|
-
if (recording) {
|
|
57
|
-
const recorded = turn?.record();
|
|
58
|
-
recorder.write(shape.id, {
|
|
59
|
-
...sent,
|
|
60
|
-
model: "unknown",
|
|
61
|
-
output: "",
|
|
62
|
-
status: "failed",
|
|
63
|
-
error: err instanceof Error ? err.message : String(err),
|
|
64
|
-
latencyMs: Date.now() - startedAt,
|
|
65
|
-
offeredTools: recorded?.offeredTools,
|
|
66
|
-
steps: recorded?.steps,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
throw err;
|
|
70
|
-
}
|
|
71
|
-
const latencyMs = Date.now() - startedAt;
|
|
72
|
-
let pricing;
|
|
73
|
-
if (recording) {
|
|
74
|
-
const recorded = turn?.record();
|
|
75
|
-
const written = recorder.write(shape.id, {
|
|
76
|
-
...sent,
|
|
77
|
-
model: outcome.model,
|
|
78
|
-
output: outcome.output,
|
|
79
|
-
status: outcome.status ?? "completed",
|
|
80
|
-
error: outcome.error,
|
|
81
|
-
inputTokens: outcome.inputTokens,
|
|
82
|
-
outputTokens: outcome.outputTokens,
|
|
83
|
-
cacheReadTokens: outcome.cacheReadTokens,
|
|
84
|
-
cacheWriteTokens: outcome.cacheWriteTokens,
|
|
85
|
-
citationTokens: outcome.citationTokens,
|
|
86
|
-
reasoningTokens: outcome.reasoningTokens,
|
|
87
|
-
latencyMs,
|
|
88
|
-
caseId: outcome.caseId,
|
|
89
|
-
seed: outcome.seed,
|
|
90
|
-
offeredTools: outcome.offeredTools ?? recorded?.offeredTools,
|
|
91
|
-
steps: outcome.steps ?? recorded?.steps,
|
|
92
|
-
});
|
|
93
|
-
pricing = written instanceof Promise ? written : undefined;
|
|
94
|
-
}
|
|
95
|
-
return { ...outcome, rolloutId, ...(pricing ? { pricing } : {}) };
|
|
96
|
-
},
|
|
97
|
-
bind: (model) => bind(built, model),
|
|
98
|
-
};
|
|
99
|
-
return built;
|
|
100
|
-
}
|
|
1
|
+
import{serializeFields as R}from"@spendgraph/sdk";import{findVariables as S,renderMessages as h}from"../render/index.js";import{recordable as v}from"../render/record.js";import{newRolloutId as E}from"../run/id.js";import{fingerprint as $}from"../fingerprint/index.js";import{bind as j}from"./bind.js";const x={enabled:!1,write:()=>{}},A=50;let D=!1;function z(e){D||e.length<=A||(D=!0,console.warn(`[prompt] ${e.length} messages is over the ${A} a rollout can record. The call still runs; the rollout will be rejected. Trim the history you pass.`))}function O(e,r,l){const s=Object.keys(e).filter(i=>l.has(i)&&!r.has(i));return s.length===0||console.warn(`[prompt] ${s.map(i=>`{${i}}`).join(", ")} appears in this prompt and you passed a value for it, but no field declares it, so it was not substituted and the placeholder goes to the model as written. Add it to the prompt's fields.`),s}function P(e){for(let r=e.length-1;r>=0;r--)if(e[r].role==="user")return e[r].content;return""}function L(e,r,l=x){const s=new Set(e.fields.map(t=>t.name)),i=new Set(S(e.blocks,e.question));let w=!1;const u=(t={})=>(w||(w=O(t,s,i).length>0),R(t,e.fields)),q=(t={},f={})=>h(e.blocks,e.question,u(t),f.history),b={id:e.id,name:e.name,slug:e.slug,fields:e.fields,versionId:e.versionId,fingerprint:$({name:e.name,blocks:e.blocks,question:e.question}),models:e.models,source:r,serialize:u,format:q,async call(t,f,d={}){const k=u(t),a=h(e.blocks,e.question,k,d.history);z(a);const m=d.tools?.trace(d.query??P(a)),T=d.rolloutId??E(),g=l.enabled&&(d.report??!0),y=Date.now(),I=()=>({rolloutId:T,versionId:e.versionId,fields:k,rendered:v(a)});let o;try{o=await f({messages:a,turn:m})}catch(n){if(g){const c=m?.record();l.write(e.id,{...I(),model:"unknown",output:"",status:"failed",error:n instanceof Error?n.message:String(n),latencyMs:Date.now()-y,offeredTools:c?.offeredTools,steps:c?.steps})}throw n}const M=Date.now()-y;let p;if(g){const n=m?.record(),c=l.write(e.id,{...I(),model:o.model,output:o.output,status:o.status??"completed",error:o.error,inputTokens:o.inputTokens,outputTokens:o.outputTokens,cacheReadTokens:o.cacheReadTokens,cacheWriteTokens:o.cacheWriteTokens,citationTokens:o.citationTokens,reasoningTokens:o.reasoningTokens,latencyMs:M,caseId:o.caseId,seed:o.seed,offeredTools:o.offeredTools??n?.offeredTools,steps:o.steps??n?.steps});p=c instanceof Promise?c:void 0}return{...o,rolloutId:T,...p?{pricing:p}:{}}},bind:t=>j(b,t)};return b}export{A as MAX_RECORDED_MESSAGES,L as makePrompt,x as noRecorder};
|
package/dist/prompt/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { MAX_RECORDED_MESSAGES, makePrompt, noRecorder } from "./factory.js";
|
|
1
|
+
import{bind as e}from"./bind.js";import{MAX_RECORDED_MESSAGES as E,makePrompt as p,noRecorder as t}from"./factory.js";export{E as MAX_RECORDED_MESSAGES,e as bind,p as makePrompt,t as noRecorder};
|
package/dist/pull/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{pullWithCache as l}from"./pull.js";export{l as pullWithCache};
|
package/dist/pull/pull.js
CHANGED
|
@@ -1,19 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const hit = cache.get(key);
|
|
3
|
-
if (hit && !hit.stale)
|
|
4
|
-
return hit.value;
|
|
5
|
-
if (hit) {
|
|
6
|
-
cache.refreshInBackground(key, fetchOne);
|
|
7
|
-
return hit.value;
|
|
8
|
-
}
|
|
9
|
-
const startedAt = cache.beginFetch();
|
|
10
|
-
try {
|
|
11
|
-
const value = await fetchOne();
|
|
12
|
-
cache.settle(key, value, startedAt);
|
|
13
|
-
return value;
|
|
14
|
-
}
|
|
15
|
-
catch (err) {
|
|
16
|
-
cache.abandon(key);
|
|
17
|
-
throw err;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
1
|
+
async function o(t,n,e){const r=t.get(n);if(r&&!r.stale)return r.value;if(r)return t.refreshInBackground(n,e),r.value;const u=t.beginFetch();try{const a=await e();return t.settle(n,a,u),a}catch(a){throw t.abandon(n),a}}export{o as pullWithCache};
|
package/dist/render/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { renderMessages } from "./messages.js";
|
|
2
2
|
export { compileSystemPrompt } from "./system.js";
|
|
3
|
-
export type { Block, History, Message } from "./types.js";
|
|
3
|
+
export type { Block, History, Message, MessageCall } from "./types.js";
|
|
4
4
|
export { applyVariables, findVariables } from "./variables.js";
|
package/dist/render/index.js
CHANGED
|
@@ -1,3 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { compileSystemPrompt } from "./system.js";
|
|
3
|
-
export { applyVariables, findVariables } from "./variables.js";
|
|
1
|
+
import{renderMessages as o}from"./messages.js";import{compileSystemPrompt as a}from"./system.js";import{applyVariables as s,findVariables as t}from"./variables.js";export{s as applyVariables,a as compileSystemPrompt,t as findVariables,o as renderMessages};
|
package/dist/render/messages.js
CHANGED
|
@@ -1,14 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { applyVariables } from "./variables.js";
|
|
3
|
-
export function renderMessages(blocks, question, values, history = []) {
|
|
4
|
-
const system = compileSystemPrompt(blocks, values);
|
|
5
|
-
const user = applyVariables(question, values);
|
|
6
|
-
const turns = history.filter((m) => m.role !== "system");
|
|
7
|
-
return system
|
|
8
|
-
? [
|
|
9
|
-
{ role: "system", content: system },
|
|
10
|
-
...turns,
|
|
11
|
-
{ role: "user", content: user },
|
|
12
|
-
]
|
|
13
|
-
: [...turns, { role: "user", content: user }];
|
|
14
|
-
}
|
|
1
|
+
import{compileSystemPrompt as l}from"./system.js";import{applyVariables as p}from"./variables.js";function y(s,n,e,m=[]){const t=l(s,e),r=p(n,e),o=m.filter(c=>c.role!=="system");return t?[{role:"system",content:t},...o,{role:"user",content:r}]:[...o,{role:"user",content:r}]}export{y as renderMessages};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RenderedMessage } from "@spendgraph/sdk";
|
|
2
|
+
import type { Message } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The messages as a rollout can carry them.
|
|
5
|
+
*
|
|
6
|
+
* A `tool` turn is recorded as itself, carrying the name of whatever answered.
|
|
7
|
+
* What was called and with which arguments is still in `steps`, where it is
|
|
8
|
+
* queryable rather than embedded in a transcript — but the result the model
|
|
9
|
+
* read is part of the conversation, and rewriting it into a `user` message made
|
|
10
|
+
* the transcript claim a person said it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function recordable(messages: Message[]): RenderedMessage[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function n(t){return t.map(o=>o.role==="tool"?{role:"tool",content:o.content,name:o.name??"a tool"}:{role:o.role,content:o.content})}export{n as recordable};
|
package/dist/render/system.js
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const body = b.body?.trim();
|
|
6
|
-
if (!body)
|
|
7
|
-
continue;
|
|
8
|
-
const title = b.title?.trim();
|
|
9
|
-
const text = applyVariables(body, values);
|
|
10
|
-
out.push(title ? `## ${applyVariables(title, values)}\n${text}` : text);
|
|
11
|
-
}
|
|
12
|
-
return out.join("\n\n");
|
|
13
|
-
}
|
|
1
|
+
import{applyVariables as e}from"./variables.js";function s(m,t){const o=[];for(const n of m??[]){const i=n.body?.trim();if(!i)continue;const r=n.title?.trim(),c=e(i,t);o.push(r?`## ${e(r,t)}
|
|
2
|
+
${c}`:c)}return o.join(`
|
|
3
|
+
|
|
4
|
+
`)}export{s as compileSystemPrompt};
|
package/dist/render/types.d.ts
CHANGED
|
@@ -3,13 +3,29 @@ export type Block = {
|
|
|
3
3
|
title: string;
|
|
4
4
|
body: string;
|
|
5
5
|
};
|
|
6
|
+
/** A tool call an assistant turn made, kept so its result can be paired back to it. */
|
|
7
|
+
export interface MessageCall {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
args: Record<string, unknown>;
|
|
11
|
+
}
|
|
6
12
|
/**
|
|
7
|
-
* One turn. `assistant`
|
|
8
|
-
* wording still renders a system turn and a user turn, never an
|
|
13
|
+
* One turn. `assistant` and `tool` appear only in conversation history — a
|
|
14
|
+
* prompt's own wording still renders a system turn and a user turn, never an
|
|
15
|
+
* answer.
|
|
16
|
+
*
|
|
17
|
+
* Structurally the `PromptMessage` that `@spendgraph/llms` takes, so a
|
|
18
|
+
* conversation that used tools can be handed straight back as history rather
|
|
19
|
+
* than flattened into prose first. Nothing here templates it: history goes to
|
|
20
|
+
* the model as it arrived.
|
|
9
21
|
*/
|
|
10
|
-
export
|
|
11
|
-
role: "system" | "user" | "assistant";
|
|
22
|
+
export interface Message {
|
|
23
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
12
24
|
content: string;
|
|
13
|
-
|
|
25
|
+
toolCalls?: MessageCall[];
|
|
26
|
+
toolCallId?: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
isError?: boolean;
|
|
29
|
+
}
|
|
14
30
|
/** Prior turns, oldest first, placed between the system turn and the question. */
|
|
15
31
|
export type History = Message[];
|
package/dist/render/types.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -3,6 +3,13 @@ import type { Block } from "./types.js";
|
|
|
3
3
|
* Substitutes `{name}` placeholders, leaving unknown ones verbatim — emptying
|
|
4
4
|
* one would send a prompt with a hole the caller cannot see.
|
|
5
5
|
*
|
|
6
|
+
* `Object.hasOwn` rather than `in`, which walks the prototype chain: every
|
|
7
|
+
* object has `constructor`, `toString` and `valueOf`, so `in` counted those as
|
|
8
|
+
* supplied and a prompt reading "explain what {constructor} does" went to the
|
|
9
|
+
* model as "explain what function Object() { [native code] } does". A template
|
|
10
|
+
* about code is all it takes, and noise that looks like content is worse than
|
|
11
|
+
* the visible hole this function exists to avoid.
|
|
12
|
+
*
|
|
6
13
|
* Mirrors `lib/prompt.ts` in the app. Duplicated because this package depends on
|
|
7
14
|
* nothing; `rendered` on every rollout makes a divergence visible.
|
|
8
15
|
*/
|
package/dist/render/variables.js
CHANGED
|
@@ -1,20 +1 @@
|
|
|
1
|
-
|
|
2
|
-
if (!text || !values)
|
|
3
|
-
return text;
|
|
4
|
-
return text.replace(/\{(\w+)\}/g, (whole, name) => name in values ? values[name] : whole);
|
|
5
|
-
}
|
|
6
|
-
export function findVariables(blocks, question) {
|
|
7
|
-
const seen = [];
|
|
8
|
-
const scan = (text) => {
|
|
9
|
-
for (const m of text.matchAll(/\{(\w+)\}/g)) {
|
|
10
|
-
if (!seen.includes(m[1]))
|
|
11
|
-
seen.push(m[1]);
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
for (const b of blocks ?? []) {
|
|
15
|
-
scan(b.title ?? "");
|
|
16
|
-
scan(b.body ?? "");
|
|
17
|
-
}
|
|
18
|
-
scan(question);
|
|
19
|
-
return seen;
|
|
20
|
-
}
|
|
1
|
+
function f(o,r){return!o||!r?o:o.replace(/\{(\w+)\}/g,(c,n)=>Object.hasOwn(r,n)?r[n]:c)}function i(o,r){const c=[],n=t=>{for(const e of t.matchAll(/\{(\w+)\}/g))c.includes(e[1])||c.push(e[1])};for(const t of o??[])n(t.title??""),n(t.body??"");return n(r),c}export{f as applyVariables,i as findVariables};
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { Prompts } from "@spendgraph/sdk";
|
|
2
2
|
import type { Budget } from "../budget/index.js";
|
|
3
3
|
import type { RunOptions, RunResult } from "../types/index.js";
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* One rollout per model — the playground's comparison, headless.
|
|
6
|
+
*
|
|
7
|
+
* Always one result per model, in the order given. A model that failed is
|
|
8
|
+
* present with `status: "failed"` rather than missing, for the same reason
|
|
9
|
+
* `sampleRuns` keeps a failed seed: this is a comparison, and throwing on the
|
|
10
|
+
* first bad model discards every model that answered — after paying for them,
|
|
11
|
+
* because the calls already in flight are not cancelled by the throw.
|
|
12
|
+
*
|
|
13
|
+
* A budget stop throws instead. Money running out says nothing about a model.
|
|
14
|
+
*/
|
|
5
15
|
export declare function runAcrossModels(prompts: Prompts, promptId: string, values: Record<string, unknown>, models: string[], opts?: RunOptions & {
|
|
6
16
|
concurrency?: number;
|
|
7
17
|
}, budget?: Budget): Promise<RunResult[]>;
|
|
@@ -1,5 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { runOnce } from "./once.js";
|
|
3
|
-
export async function runAcrossModels(prompts, promptId, values, models, opts = {}, budget) {
|
|
4
|
-
return mapLimit(models, opts.concurrency ?? 4, (model) => runOnce(prompts, promptId, values, { ...opts, model, rolloutId: undefined }, budget));
|
|
5
|
-
}
|
|
1
|
+
import{BudgetExceededError as i}from"../budget/index.js";import{mapLimit as a}from"./concurrency.js";import{runOnce as d}from"./once.js";async function p(n,o,u,c,r={},s){return a(c,r.concurrency??4,async t=>{try{return await d(n,o,u,{...r,model:t,rolloutId:void 0},s)}catch(e){if(e instanceof i)throw e;return{id:null,model:t,status:"failed",output:"",error:e.message,costMicros:0,latencyMs:0,inputTokens:0,outputTokens:0,seed:r.seed??0}}})}export{p as runAcrossModels};
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Runs `fn` over `items`, at most `limit` in flight.
|
|
3
|
-
*
|
|
4
|
-
* Bounded on purpose: a whole dataset at once becomes 429s you still wait out,
|
|
5
|
-
* so the unbounded version finishes later and flakier.
|
|
6
|
-
*/
|
|
7
|
-
export declare function mapLimit<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
1
|
+
export { mapLimit } from "@spendgraph/sdk";
|
package/dist/run/concurrency.js
CHANGED
|
@@ -1,12 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
const out = new Array(items.length);
|
|
3
|
-
let next = 0;
|
|
4
|
-
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
5
|
-
while (next < items.length) {
|
|
6
|
-
const i = next++;
|
|
7
|
-
out[i] = await fn(items[i], i);
|
|
8
|
-
}
|
|
9
|
-
});
|
|
10
|
-
await Promise.all(workers);
|
|
11
|
-
return out;
|
|
12
|
-
}
|
|
1
|
+
import{mapLimit as o}from"@spendgraph/sdk";export{o as mapLimit};
|
package/dist/run/id.js
CHANGED
package/dist/run/index.js
CHANGED
|
@@ -1,5 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { mapLimit } from "./concurrency.js";
|
|
3
|
-
export { newRolloutId } from "./id.js";
|
|
4
|
-
export { runOnce } from "./once.js";
|
|
5
|
-
export { sampleRuns } from "./sample.js";
|
|
1
|
+
import{runAcrossModels as e}from"./across-models.js";import{mapLimit as p}from"./concurrency.js";import{newRolloutId as f}from"./id.js";import{runOnce as s}from"./once.js";import{sampleRuns as l}from"./sample.js";export{p as mapLimit,f as newRolloutId,e as runAcrossModels,s as runOnce,l as sampleRuns};
|
package/dist/run/once.js
CHANGED
|
@@ -1,21 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
export async function runOnce(prompts, promptId, values, opts = {}, budget) {
|
|
3
|
-
budget?.assertAffordable();
|
|
4
|
-
const rolloutId = opts.rolloutId ?? newRolloutId();
|
|
5
|
-
const res = await prompts.run(promptId, {
|
|
6
|
-
rolloutId,
|
|
7
|
-
values,
|
|
8
|
-
model: opts.model,
|
|
9
|
-
temperature: opts.temperature,
|
|
10
|
-
maxTokens: opts.maxTokens,
|
|
11
|
-
seed: opts.seed ?? 0,
|
|
12
|
-
caseId: opts.caseId,
|
|
13
|
-
candidateId: opts.candidateId,
|
|
14
|
-
parentId: opts.parentId,
|
|
15
|
-
generation: opts.generation,
|
|
16
|
-
record: opts.record ?? true,
|
|
17
|
-
});
|
|
18
|
-
if (!res.deduped)
|
|
19
|
-
budget?.add(res.rollout.costMicros ?? 0);
|
|
20
|
-
return res.rollout;
|
|
21
|
-
}
|
|
1
|
+
import{newRolloutId as c}from"./id.js";async function u(a,n,o,e={},d){d?.assertAffordable();const t=e.rolloutId??c(),r=await a.run(n,{rolloutId:t,values:o,model:e.model,temperature:e.temperature,maxTokens:e.maxTokens,seed:e.seed??0,caseId:e.caseId,candidateId:e.candidateId,parentId:e.parentId,generation:e.generation,record:e.record??!0});return r.deduped||d?.add(r.rollout.costMicros??0),r.rollout}export{u as runOnce};
|
package/dist/run/sample.js
CHANGED
|
@@ -1,28 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { mapLimit } from "./concurrency.js";
|
|
3
|
-
import { runOnce } from "./once.js";
|
|
4
|
-
export async function sampleRuns(prompts, promptId, values, opts = {}, budget) {
|
|
5
|
-
const k = Math.max(1, opts.k ?? 5);
|
|
6
|
-
const seeds = Array.from({ length: k }, (_, i) => i);
|
|
7
|
-
return mapLimit(seeds, opts.concurrency ?? 4, async (seed) => {
|
|
8
|
-
try {
|
|
9
|
-
return await runOnce(prompts, promptId, values, { ...opts, seed, rolloutId: undefined }, budget);
|
|
10
|
-
}
|
|
11
|
-
catch (err) {
|
|
12
|
-
if (err instanceof BudgetExceededError)
|
|
13
|
-
throw err;
|
|
14
|
-
return {
|
|
15
|
-
id: null,
|
|
16
|
-
model: opts.model ?? "",
|
|
17
|
-
status: "failed",
|
|
18
|
-
output: "",
|
|
19
|
-
error: err.message,
|
|
20
|
-
costMicros: 0,
|
|
21
|
-
latencyMs: 0,
|
|
22
|
-
inputTokens: 0,
|
|
23
|
-
outputTokens: 0,
|
|
24
|
-
seed,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
}
|
|
1
|
+
import{BudgetExceededError as m}from"../budget/index.js";import{mapLimit as s}from"./concurrency.js";import{runOnce as d}from"./once.js";async function y(n,o,u,t={},c){const a=Math.max(1,t.k??5),i=Array.from({length:a},(e,r)=>r);return s(i,t.concurrency??4,async e=>{try{return await d(n,o,u,{...t,seed:e,rolloutId:void 0},c)}catch(r){if(r instanceof m)throw r;return{id:null,model:t.model??"",status:"failed",output:"",error:r.message,costMicros:0,latencyMs:0,inputTokens:0,outputTokens:0,seed:e}}})}export{y as sampleRuns};
|
package/dist/types/dataset.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/types/index.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/types/payload.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/types/prompt.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Recordable, TraceHandle, TraceOptions, TraceOutcome } from "./trac
|
|
|
6
6
|
* Where a prompt came from, and so what a rollout may reference.
|
|
7
7
|
*
|
|
8
8
|
* `server` has a row and a version behind it. `custom` was written in code and
|
|
9
|
-
* has neither, which is why its `
|
|
9
|
+
* has neither, which is why its `call` records usage and not a rollout.
|
|
10
10
|
*/
|
|
11
11
|
export type PromptSource = "server" | "custom";
|
|
12
12
|
/** A prompt: its fields, its version, and how to run it. */
|
|
@@ -18,6 +18,11 @@ export interface Prompt {
|
|
|
18
18
|
fields: FieldSpec[];
|
|
19
19
|
/** What a rollout recorded from this prompt should reference. Null when custom. */
|
|
20
20
|
versionId: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* A stable id for what this prompt says, the same whether it was pulled or
|
|
23
|
+
* built, so cost history follows the wording across being registered.
|
|
24
|
+
*/
|
|
25
|
+
fingerprint: string;
|
|
21
26
|
models: string[];
|
|
22
27
|
source: PromptSource;
|
|
23
28
|
/**
|
package/dist/types/prompt.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/types/result.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/types/rollout.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/types/run.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/types/trace.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/types/version.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/docs/caching.mdx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
title: "Caching — spendgraph docs",
|
|
3
|
+
description:
|
|
4
|
+
"`pull` serves a stale copy immediately and refreshes behind the caller. Invalidating has to drop both handles, because the same prompt sits under its uuid and its slug at once.",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
# Caching, and invalidating it
|
|
8
|
+
|
|
9
|
+
`pull` serves a stale copy immediately and refreshes behind the caller, so an edit takes effect within a TTL without any request paying for the fetch.
|
|
10
|
+
|
|
11
|
+
## Invalidating drops both handles
|
|
12
|
+
|
|
13
|
+
`promote` invalidates for you. Doing it by hand needs to drop **both** handles, not just the one passed: `pull` keys on whatever string it was given, so the same prompt sits under its uuid and its slug at once — and `promote` is only reachable with a uuid while the dashboard teaches pulling by slug. Dropping one left the other warm for a full TTL, still rendering the wording the promotion replaced.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
await prompts.promote(promptId, versionId); // invalidates
|
|
17
|
+
prompts.invalidate(promptId); // both handles
|
|
18
|
+
prompts.invalidate(); // everything
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The delete runs even when nothing is stored under the handle: a cold isolate has a fetch in flight and no entry yet, and the delete is what stops that in-flight result being stored **after** the promotion.
|
|
22
|
+
|
|
23
|
+
## A handle that stopped resolving
|
|
24
|
+
|
|
25
|
+
A rename re-mints the slug, so a handle that 404s is routine rather than exotic. The cache backs one off after a failure rather than firing a doomed request on every pull, and keeps serving the entry it has.
|
|
26
|
+
|
|
27
|
+
`onPullError` is worth setting. Without it a background refresh that has stopped resolving is silent, and a rename on the dashboard freezes a running service with nothing anywhere saying so.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
new PromptClient({
|
|
31
|
+
apiKey,
|
|
32
|
+
baseUrl,
|
|
33
|
+
onPullError: (err, key) => log.warn({ key }, String(err)),
|
|
34
|
+
cache: { ttlSeconds: 300, retryAfterSeconds: 60 },
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`cache.identify` is not yours to set — the client fixes it to the prompt's id and slug, because an invalidation arriving under one handle has to reach a fetch started under the other.
|
|
39
|
+
|
|
40
|
+
<Callout tone="trap" title="The dataset is not cached with the prompt">
|
|
41
|
+
Cases change independently of the wording, so they are fetched fresh. A cached prompt and a stale case list would score today's answers against yesterday's questions and report it as an improvement.
|
|
42
|
+
</Callout>
|
|
43
|
+
|
|
44
|
+
## A reply that is not a prompt
|
|
45
|
+
|
|
46
|
+
`pull` refuses a response carrying no prompt, naming the handle and the two things worth checking — that the base URL points at a spendgraph app, and that the key can read this project. A prompt that is thin but real is still served, with the absent parts as their empty values.
|
package/docs/calling.mdx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
title: "Calling a prompt — spendgraph docs",
|
|
3
|
+
description:
|
|
4
|
+
"`call` renders, runs the model call you give it, and records what happened around it. What was sent is built once and shared by the success and failure paths.",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
# Calling a prompt
|
|
8
|
+
|
|
9
|
+
`call` renders, runs the model call **you** give it, and records what happened around it. This package never sees the provider response, which is why the call is handed to it rather than made by it.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const reply = await stored.call({ audience: "students", max_words: 50 }, async ({ messages }) => {
|
|
13
|
+
const res = await anthropic.messages.create({ model, max_tokens: 400, messages });
|
|
14
|
+
return { output: res.content[0].text, model, inputTokens: 400, outputTokens: 12 };
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
It fills in the version, fields, messages, latency and status; you return the text, model and tokens.
|
|
19
|
+
|
|
20
|
+
**Return the tokens** or every cost on the prompt reads as zero — the first call that forgets warns once.
|
|
21
|
+
|
|
22
|
+
## One record, two paths
|
|
23
|
+
|
|
24
|
+
What was sent is built **once** and shared by the success path and the failure path, so the two cannot disagree about it. That is the thing hand-written reporting gets wrong.
|
|
25
|
+
|
|
26
|
+
A callback that throws is recorded as failed and then rethrown: a provider call that throws is otherwise recorded nowhere at all, which leaves the run most in need of explaining with no evidence.
|
|
27
|
+
|
|
28
|
+
## Recording
|
|
29
|
+
|
|
30
|
+
Recording is on for every prompt and is fire-and-forget. There is no client-level switch: pass `{ report: false }` on the call that should not be recorded, which is where you know whether it should.
|
|
31
|
+
|
|
32
|
+
Fire-and-forget means nothing catches a send that fails, and what is lost is spend nothing downstream can reconstruct. **`onReportError` is where that arrives** — on the client for a rollout, and on `buildCustomPrompt` for a custom prompt's usage. A client passes its own down to what it builds.
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
`call` and `invoke` accept anything that hands out a turn — [`toolbus()`](/docs/tools/overview), or your own.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
await prompt.call(values, ({ messages, turn }) => call(messages, turn), { tools: bus });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The callback gets the turn **fully typed**; the rollout gets the tools offered and the steps taken. Which tools were offered is recorded as well as which were called: without it a rollout cannot tell "chose not to" from "was never offered", which are opposite bugs.
|
|
43
|
+
|
|
44
|
+
<Callout tone="trap" title="The recording never blocks the call, and never breaks it">
|
|
45
|
+
Reporting happens beside your call, not in front of it: a report that fails reaches `onReportError` rather than your `catch`. That is deliberate — a metering outage must not take an app down — but it does mean a missing rollout is silent unless you wired that callback.
|
|
46
|
+
</Callout>
|
|
47
|
+
|
|
48
|
+
## Binding a model
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const chain = prompt.bind(model);
|
|
52
|
+
|
|
53
|
+
await chain.invoke(values);
|
|
54
|
+
await chain.stream(values, { onText: (delta) => res.write(delta) });
|
|
55
|
+
await chain.batch(cases, { concurrency: 4 });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`stream` falls back to `call` for a model that cannot stream, so binding one does not require it.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
title: "Versions and datasets — spendgraph docs",
|
|
3
|
+
description:
|
|
4
|
+
"Cases sit on one side of a wall the server decides, so a case never moves sides. setCases replaces the whole set, because a dataset has one answer at a time.",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
# Versions and datasets
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
await prompts.versions(promptId, { origin: "user", limit: 20 });
|
|
11
|
+
await prompts.cases(promptId);
|
|
12
|
+
await prompts.setCases(promptId, cases);
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## A case sits on one side of a wall
|
|
16
|
+
|
|
17
|
+
Every case is either `feedback` or `held_out`, and **the server decides which** — it is not a field you set.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
feedback ──▶ what an optimiser is allowed to read and learn from
|
|
21
|
+
held_out ──▶ what it never sees, so a score means something
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That is the whole point of the split, and the reason the client cannot set it. A case that could move sides is a case that can be quietly moved to the side where the prompt already does well, and every number downstream stops meaning anything. `DatasetCase.split` exists on the type so a read can report it; omit it on a write.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const summary = await prompts.cases(promptId);
|
|
28
|
+
|
|
29
|
+
summary.counts; // { total, feedback, held_out }
|
|
30
|
+
summary.problem; // why this cannot be optimised against yet, or null
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`problem` is the useful one. A dataset too small, too lopsided, or missing expected outputs cannot support a comparison, and the server says which rather than returning a confident score built on four cases.
|
|
34
|
+
|
|
35
|
+
## `setCases` replaces everything
|
|
36
|
+
|
|
37
|
+
Not a merge, and deliberately.
|
|
38
|
+
|
|
39
|
+
A dataset is a **set** — "which cases am I evaluating against" has one answer at a time. A merge would leave no way to remove a case, and a stale case nobody can delete quietly biases every run after it.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
await prompts.setCases(promptId, [
|
|
43
|
+
{ caseId: "refund-partial", fieldValues: { audience: "students" }, expected: "…" },
|
|
44
|
+
]);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Send the set you want to have. Read it back with `cases()` if you need to confirm what landed.
|
|
48
|
+
|
|
49
|
+
<Callout tone="trap" title="A case id is not a label — it decides which side of the wall the case lands on">
|
|
50
|
+
The split is derived from the id, so renaming a case re-rolls it: a case that was held out can come back as feedback, and whatever proposes changes may now read what it is scored against. Nothing in the data would say so. Change the text, keep the id.
|
|
51
|
+
</Callout>
|
|
52
|
+
|
|
53
|
+
## Versions
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
await prompts.versions(promptId, { origin: "user", limit: 20 });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`origin` separates a version somebody wrote from one a run produced, which matters once an optimiser is writing candidates — otherwise the history reads as though a person made forty edits in a minute.
|
|
60
|
+
|
|
61
|
+
Promoting a version is on [Caching](/docs/prompt/caching), because the part worth knowing is what it invalidates.
|