@harny/vercel-ai-provider 0.0.0-stage → 0.1.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 +141 -2
- package/dist/client.d.ts +46 -0
- package/dist/client.js +266 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +32 -0
- package/dist/format.d.ts +4 -0
- package/dist/format.js +11 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/messages.d.ts +19 -0
- package/dist/messages.js +57 -0
- package/dist/middleware.d.ts +41 -0
- package/dist/middleware.js +136 -0
- package/dist/provider.d.ts +32 -0
- package/dist/provider.js +34 -0
- package/dist/standalone.d.ts +22 -0
- package/dist/standalone.js +47 -0
- package/dist/tools.d.ts +676 -0
- package/dist/tools.js +66 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +43 -4
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The memory API answered with an error, or the call never reached it. */
|
|
2
|
+
export declare class HarnyAPIError extends Error {
|
|
3
|
+
/** The API's error code, for example `content-required`, or `network-error` / `rate-limited` / `timeout` from this package. */
|
|
4
|
+
readonly code: string;
|
|
5
|
+
/** HTTP status, or 0 when no answer came back. */
|
|
6
|
+
readonly status: number;
|
|
7
|
+
/** The API's plain-words explanation. */
|
|
8
|
+
readonly debugMessage: string;
|
|
9
|
+
/** Extra data the API attached (for example the ids a delete could not find). */
|
|
10
|
+
readonly data: unknown;
|
|
11
|
+
/** Seconds to wait before retrying, when the API said so (a per-minute limit). */
|
|
12
|
+
readonly retryAfter: number | undefined;
|
|
13
|
+
/** The `X-Request-Id` of the call, to quote when you ask for help. */
|
|
14
|
+
readonly requestId: string | undefined;
|
|
15
|
+
constructor(init: {
|
|
16
|
+
code: string;
|
|
17
|
+
status: number;
|
|
18
|
+
debugMessage: string;
|
|
19
|
+
data?: unknown;
|
|
20
|
+
retryAfter?: number;
|
|
21
|
+
requestId?: string;
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** The package was set up in a way that cannot work (no key, no user, a plain-http address). Nothing was sent. */
|
|
26
|
+
export declare class HarnyConfigError extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** The memory API answered with an error, or the call never reached it. */
|
|
2
|
+
export class HarnyAPIError extends Error {
|
|
3
|
+
/** The API's error code, for example `content-required`, or `network-error` / `rate-limited` / `timeout` from this package. */
|
|
4
|
+
code;
|
|
5
|
+
/** HTTP status, or 0 when no answer came back. */
|
|
6
|
+
status;
|
|
7
|
+
/** The API's plain-words explanation. */
|
|
8
|
+
debugMessage;
|
|
9
|
+
/** Extra data the API attached (for example the ids a delete could not find). */
|
|
10
|
+
data;
|
|
11
|
+
/** Seconds to wait before retrying, when the API said so (a per-minute limit). */
|
|
12
|
+
retryAfter;
|
|
13
|
+
/** The `X-Request-Id` of the call, to quote when you ask for help. */
|
|
14
|
+
requestId;
|
|
15
|
+
constructor(init) {
|
|
16
|
+
super(`Harny ${init.status || 'network'} ${init.code}: ${init.debugMessage}`, { cause: init.cause });
|
|
17
|
+
this.name = 'HarnyAPIError';
|
|
18
|
+
this.code = init.code;
|
|
19
|
+
this.status = init.status;
|
|
20
|
+
this.debugMessage = init.debugMessage;
|
|
21
|
+
this.data = init.data;
|
|
22
|
+
this.retryAfter = init.retryAfter;
|
|
23
|
+
this.requestId = init.requestId;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** The package was set up in a way that cannot work (no key, no user, a plain-http address). Nothing was sent. */
|
|
27
|
+
export class HarnyConfigError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'HarnyConfigError';
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RecallResult } from './types.js';
|
|
2
|
+
export declare const DEFAULT_HEADING = "What you remember about this person from earlier conversations, most relevant first. Use it when it helps; do not mention it unless asked:";
|
|
3
|
+
/** The recalled evidence as a short block for a system prompt. Empty string when there is none. */
|
|
4
|
+
export declare function formatMemories(results: readonly RecallResult[], heading?: string): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DEFAULT_HEADING = 'What you remember about this person from earlier conversations, most relevant first. Use it when it helps; do not mention it unless asked:';
|
|
2
|
+
/** The recalled evidence as a short block for a system prompt. Empty string when there is none. */
|
|
3
|
+
export function formatMemories(results, heading = DEFAULT_HEADING) {
|
|
4
|
+
const lines = results
|
|
5
|
+
.filter((result) => result.content && result.content.trim())
|
|
6
|
+
.map((result) => {
|
|
7
|
+
const when = result.occurredAt ? ` (${result.occurredAt.slice(0, 10)})` : '';
|
|
8
|
+
return `- ${result.content.trim()}${when}`;
|
|
9
|
+
});
|
|
10
|
+
return lines.length ? `${heading}\n${lines.join('\n')}` : '';
|
|
11
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createHarny, harny, type HarnyProvider } from './provider.js';
|
|
2
|
+
export { createMemoryMiddleware, type MemorySettings } from './middleware.js';
|
|
3
|
+
export { createTools, type ToolSettings } from './tools.js';
|
|
4
|
+
export { addMemories, getMemories, retrieveMemories, type MemoryCallOptions } from './standalone.js';
|
|
5
|
+
export { HarnyClient, resolveBaseURL } from './client.js';
|
|
6
|
+
export { HarnyAPIError, HarnyConfigError } from './errors.js';
|
|
7
|
+
export { formatMemories, DEFAULT_HEADING } from './format.js';
|
|
8
|
+
export { VERSION } from './version.js';
|
|
9
|
+
export type { PromptMessage } from './messages.js';
|
|
10
|
+
export type * from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createHarny, harny } from './provider.js';
|
|
2
|
+
export { createMemoryMiddleware } from './middleware.js';
|
|
3
|
+
export { createTools } from './tools.js';
|
|
4
|
+
export { addMemories, getMemories, retrieveMemories } from './standalone.js';
|
|
5
|
+
export { HarnyClient, resolveBaseURL } from './client.js';
|
|
6
|
+
export { HarnyAPIError, HarnyConfigError } from './errors.js';
|
|
7
|
+
export { formatMemories, DEFAULT_HEADING } from './format.js';
|
|
8
|
+
export { VERSION } from './version.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A prompt message in the AI SDK's shape (`LanguageModelV4Prompt` entries and `ModelMessage` both fit). */
|
|
2
|
+
export interface PromptMessage {
|
|
3
|
+
role: string;
|
|
4
|
+
content: unknown;
|
|
5
|
+
}
|
|
6
|
+
/** The text of one message: its text parts joined by a newline. Files, tool calls and reasoning are left out. */
|
|
7
|
+
export declare function textOf(message: PromptMessage): string;
|
|
8
|
+
/** The text of the last user message, which is what the person just asked. */
|
|
9
|
+
export declare function lastUserQuery(prompt: readonly PromptMessage[]): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The user turns that are new in this call: the user messages after the last assistant or tool message.
|
|
12
|
+
* Assistant text is never returned (user turns only).
|
|
13
|
+
*/
|
|
14
|
+
export declare function newUserTurns(prompt: readonly PromptMessage[]): string[];
|
|
15
|
+
export declare function sha256Hex(text: string): Promise<string>;
|
|
16
|
+
/** The id of a stored user turn: a hash of the conversation key and the text, so a repeat replaces itself. */
|
|
17
|
+
export declare function turnId(conversationKey: string, text: string): Promise<string>;
|
|
18
|
+
/** The conversation key when none is set: a hash of the first user message, which stays fixed as the chat grows. */
|
|
19
|
+
export declare function defaultConversationKey(prompt: readonly PromptMessage[]): Promise<string>;
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** The text of one message: its text parts joined by a newline. Files, tool calls and reasoning are left out. */
|
|
2
|
+
export function textOf(message) {
|
|
3
|
+
const content = message.content;
|
|
4
|
+
if (typeof content === 'string')
|
|
5
|
+
return content.trim();
|
|
6
|
+
if (!Array.isArray(content))
|
|
7
|
+
return '';
|
|
8
|
+
return content
|
|
9
|
+
.filter((part) => !!part && part.type === 'text' && typeof part.text === 'string')
|
|
10
|
+
.map((part) => part.text)
|
|
11
|
+
.join('\n')
|
|
12
|
+
.trim();
|
|
13
|
+
}
|
|
14
|
+
/** The text of the last user message, which is what the person just asked. */
|
|
15
|
+
export function lastUserQuery(prompt) {
|
|
16
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
17
|
+
const message = prompt[i];
|
|
18
|
+
if (message?.role === 'user') {
|
|
19
|
+
const text = textOf(message);
|
|
20
|
+
if (text)
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The user turns that are new in this call: the user messages after the last assistant or tool message.
|
|
28
|
+
* Assistant text is never returned (user turns only).
|
|
29
|
+
*/
|
|
30
|
+
export function newUserTurns(prompt) {
|
|
31
|
+
let start = 0;
|
|
32
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
33
|
+
const role = prompt[i]?.role;
|
|
34
|
+
if (role === 'assistant' || role === 'tool') {
|
|
35
|
+
start = i + 1;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return prompt
|
|
40
|
+
.slice(start)
|
|
41
|
+
.filter((message) => message.role === 'user')
|
|
42
|
+
.map(textOf)
|
|
43
|
+
.filter((text) => text.length > 0);
|
|
44
|
+
}
|
|
45
|
+
export async function sha256Hex(text) {
|
|
46
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
|
|
47
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
48
|
+
}
|
|
49
|
+
/** The id of a stored user turn: a hash of the conversation key and the text, so a repeat replaces itself. */
|
|
50
|
+
export async function turnId(conversationKey, text) {
|
|
51
|
+
return `turn-${await sha256Hex(`${conversationKey}\n${text}`)}`;
|
|
52
|
+
}
|
|
53
|
+
/** The conversation key when none is set: a hash of the first user message, which stays fixed as the chat grows. */
|
|
54
|
+
export async function defaultConversationKey(prompt) {
|
|
55
|
+
const first = prompt.find((message) => message.role === 'user');
|
|
56
|
+
return sha256Hex(first ? textOf(first) : '');
|
|
57
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { LanguageModelMiddleware } from 'ai';
|
|
2
|
+
import type { HarnyClient } from './client.js';
|
|
3
|
+
import { type PromptMessage } from './messages.js';
|
|
4
|
+
import type { RecallLevel, RecallPending, RecallResult } from './types.js';
|
|
5
|
+
export interface MemorySettings {
|
|
6
|
+
/** The end user. Defaults to the `user` given to createHarny. */
|
|
7
|
+
user?: string;
|
|
8
|
+
/** Recall before each turn. Default true. */
|
|
9
|
+
recall?: boolean;
|
|
10
|
+
/** Evidence size for recall. Default 'low'. Not with `maxTokens`. */
|
|
11
|
+
level?: RecallLevel;
|
|
12
|
+
/** Evidence budget in tokens. Not with `level`. */
|
|
13
|
+
maxTokens?: number;
|
|
14
|
+
/** Most memories to inject. Default 20. */
|
|
15
|
+
limit?: number;
|
|
16
|
+
/** Store the person's messages after a successful call. User turns only: assistant text is never stored. Default false: saving conversation messages is opt-in (`saveUserMessages: true`). */
|
|
17
|
+
saveUserMessages?: boolean;
|
|
18
|
+
/** Names the conversation for the ids of stored messages. Default: a hash of the first user message. */
|
|
19
|
+
conversationId?: string | ((prompt: readonly PromptMessage[]) => string | Promise<string>);
|
|
20
|
+
/** The line above the injected memories. */
|
|
21
|
+
heading?: string;
|
|
22
|
+
/** Replace the whole injected block. Return '' to inject nothing. */
|
|
23
|
+
formatMemories?: (results: readonly RecallResult[]) => string;
|
|
24
|
+
/** Extra metadata on every stored message. */
|
|
25
|
+
metadata?: Record<string, unknown>;
|
|
26
|
+
/** Memory trouble never fails the model call; it comes here. */
|
|
27
|
+
onError?: (error: unknown, context: {
|
|
28
|
+
stage: 'recall' | 'ingest';
|
|
29
|
+
}) => void;
|
|
30
|
+
/** Called when recall reports sends still being processed, so the answer may miss the newest facts. */
|
|
31
|
+
onPending?: (pending: RecallPending) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Hand the store to the host so the response is not held back (for example Vercel's waitUntil).
|
|
34
|
+
* Without it the call waits for the store to finish.
|
|
35
|
+
*/
|
|
36
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
37
|
+
/** How long a recall answer is reused for the same person and question (tool loops ask again). Default 30000 ms. */
|
|
38
|
+
cacheTtlMs?: number;
|
|
39
|
+
}
|
|
40
|
+
/** The memory middleware: recall into the system prompt before the call, store the person's message after it. */
|
|
41
|
+
export declare function createMemoryMiddleware(client: HarnyClient, settings?: MemorySettings): LanguageModelMiddleware;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { DEFAULT_HEADING, formatMemories } from './format.js';
|
|
2
|
+
import { defaultConversationKey, lastUserQuery, newUserTurns, turnId } from './messages.js';
|
|
3
|
+
const DEFAULT_LIMIT = 20;
|
|
4
|
+
/** The memory middleware: recall into the system prompt before the call, store the person's message after it. */
|
|
5
|
+
export function createMemoryMiddleware(client, settings = {}) {
|
|
6
|
+
const cache = new Map();
|
|
7
|
+
const stored = new Set();
|
|
8
|
+
const ttl = settings.cacheTtlMs ?? 30_000;
|
|
9
|
+
const report = (error, stage) => {
|
|
10
|
+
try {
|
|
11
|
+
settings.onError?.(error, { stage });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// A failing error handler must not fail the call either.
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
async function recallBlock(prompt) {
|
|
18
|
+
if (settings.recall === false)
|
|
19
|
+
return '';
|
|
20
|
+
const query = lastUserQuery(prompt);
|
|
21
|
+
if (!query)
|
|
22
|
+
return '';
|
|
23
|
+
const user = client.actingUser(settings.user);
|
|
24
|
+
if (!user)
|
|
25
|
+
throw new Error("Name the end user: pass user to createHarny or to the call's settings.");
|
|
26
|
+
const cacheKey = `${user}\n${query}`;
|
|
27
|
+
const hit = cache.get(cacheKey);
|
|
28
|
+
if (hit && hit.expires > Date.now())
|
|
29
|
+
return hit.block;
|
|
30
|
+
const answer = await client.recall({
|
|
31
|
+
query,
|
|
32
|
+
user,
|
|
33
|
+
limit: settings.limit ?? DEFAULT_LIMIT,
|
|
34
|
+
...(settings.maxTokens !== undefined ? { maxTokens: settings.maxTokens } : { level: settings.level ?? 'low' }),
|
|
35
|
+
});
|
|
36
|
+
if (answer.pending) {
|
|
37
|
+
try {
|
|
38
|
+
settings.onPending?.(answer.pending);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// see report()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const block = settings.formatMemories
|
|
45
|
+
? settings.formatMemories(answer.results)
|
|
46
|
+
: formatMemories(answer.results, settings.heading ?? DEFAULT_HEADING);
|
|
47
|
+
cache.set(cacheKey, { block, expires: Date.now() + ttl });
|
|
48
|
+
return block;
|
|
49
|
+
}
|
|
50
|
+
async function saveTurns(prompt) {
|
|
51
|
+
if (settings.saveUserMessages !== true)
|
|
52
|
+
return;
|
|
53
|
+
const turns = newUserTurns(prompt);
|
|
54
|
+
if (!turns.length)
|
|
55
|
+
return;
|
|
56
|
+
const user = client.actingUser(settings.user);
|
|
57
|
+
if (!user)
|
|
58
|
+
throw new Error("Name the end user: pass user to createHarny or to the call's settings.");
|
|
59
|
+
const conversation = typeof settings.conversationId === 'function'
|
|
60
|
+
? await settings.conversationId(prompt)
|
|
61
|
+
: (settings.conversationId ?? (await defaultConversationKey(prompt)));
|
|
62
|
+
for (const text of turns) {
|
|
63
|
+
const id = await turnId(conversation, text);
|
|
64
|
+
const memoKey = `${user}\n${id}`;
|
|
65
|
+
if (stored.has(memoKey))
|
|
66
|
+
continue;
|
|
67
|
+
await client.ingest({
|
|
68
|
+
content: text,
|
|
69
|
+
id,
|
|
70
|
+
label: 'User message',
|
|
71
|
+
timestamp: new Date(),
|
|
72
|
+
user,
|
|
73
|
+
...(settings.metadata ? { metadata: settings.metadata } : {}),
|
|
74
|
+
});
|
|
75
|
+
stored.add(memoKey);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function store(prompt) {
|
|
79
|
+
const work = saveTurns(prompt).catch((error) => report(error, 'ingest'));
|
|
80
|
+
if (settings.waitUntil) {
|
|
81
|
+
try {
|
|
82
|
+
settings.waitUntil(work);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
report(error, 'ingest');
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return work;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
transformParams: async ({ params }) => {
|
|
93
|
+
const prompt = params.prompt;
|
|
94
|
+
let block = '';
|
|
95
|
+
try {
|
|
96
|
+
block = await recallBlock(prompt);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
report(error, 'recall');
|
|
100
|
+
}
|
|
101
|
+
if (!block)
|
|
102
|
+
return params;
|
|
103
|
+
const messages = [...params.prompt];
|
|
104
|
+
const first = messages[0];
|
|
105
|
+
if (first && first.role === 'system') {
|
|
106
|
+
messages[0] = { ...first, content: `${first.content}\n\n${block}` };
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
messages.unshift({ role: 'system', content: block });
|
|
110
|
+
}
|
|
111
|
+
return { ...params, prompt: messages };
|
|
112
|
+
},
|
|
113
|
+
wrapGenerate: async ({ doGenerate, params }) => {
|
|
114
|
+
const result = await doGenerate();
|
|
115
|
+
await store(params.prompt);
|
|
116
|
+
return result;
|
|
117
|
+
},
|
|
118
|
+
wrapStream: async ({ doStream, params }) => {
|
|
119
|
+
const result = await doStream();
|
|
120
|
+
let failed = false;
|
|
121
|
+
const prompt = params.prompt;
|
|
122
|
+
const stream = result.stream.pipeThrough(new TransformStream({
|
|
123
|
+
transform(chunk, controller) {
|
|
124
|
+
if (chunk.type === 'error')
|
|
125
|
+
failed = true;
|
|
126
|
+
controller.enqueue(chunk);
|
|
127
|
+
},
|
|
128
|
+
async flush() {
|
|
129
|
+
if (!failed)
|
|
130
|
+
await store(prompt);
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
return { ...result, stream };
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { wrapLanguageModel, type LanguageModel } from 'ai';
|
|
2
|
+
import { HarnyClient } from './client.js';
|
|
3
|
+
import { createMemoryMiddleware, type MemorySettings } from './middleware.js';
|
|
4
|
+
import { createTools, type ToolSettings } from './tools.js';
|
|
5
|
+
import type { HarnyClientOptions } from './types.js';
|
|
6
|
+
export interface HarnyProvider {
|
|
7
|
+
/** Wrap a model so it remembers the person. Accepts a model object, or a gateway model id such as 'openai/gpt-5'. */
|
|
8
|
+
<M extends LanguageModel>(model: M, settings?: MemorySettings): ReturnType<typeof wrapLanguageModel>;
|
|
9
|
+
/** The same as calling the provider. */
|
|
10
|
+
languageModel(model: LanguageModel, settings?: MemorySettings): ReturnType<typeof wrapLanguageModel>;
|
|
11
|
+
/** The memory middleware alone, for `wrapLanguageModel({ model, middleware })`. */
|
|
12
|
+
middleware(settings?: MemorySettings): ReturnType<typeof createMemoryMiddleware>;
|
|
13
|
+
/** `{ harny_remember, harny_recall }` for a tool set. */
|
|
14
|
+
tools(settings?: ToolSettings): ReturnType<typeof createTools>;
|
|
15
|
+
/** The memory API client behind it. */
|
|
16
|
+
client: HarnyClient;
|
|
17
|
+
ingest: HarnyClient['ingest'];
|
|
18
|
+
ingestBatch: HarnyClient['ingestBatch'];
|
|
19
|
+
getIngestion: HarnyClient['getIngestion'];
|
|
20
|
+
waitForIngestion: HarnyClient['waitForIngestion'];
|
|
21
|
+
deleteIngestion: HarnyClient['deleteIngestion'];
|
|
22
|
+
deleteDataPoints: HarnyClient['deleteDataPoints'];
|
|
23
|
+
recall: HarnyClient['recall'];
|
|
24
|
+
usage: HarnyClient['usage'];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create the provider. Nothing is read or sent until the first call, so it is safe to create at import time.
|
|
28
|
+
* `apiKey` defaults to HARNY_API_KEY, `baseURL` to HARNY_API_URL, else https://api.harny.ai.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createHarny(options?: HarnyClientOptions): HarnyProvider;
|
|
31
|
+
/** A ready provider that reads HARNY_API_KEY and HARNY_API_URL. Name the person per call: harny(model, { user }). */
|
|
32
|
+
export declare const harny: HarnyProvider;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { gateway, wrapLanguageModel } from 'ai';
|
|
2
|
+
import { HarnyClient } from './client.js';
|
|
3
|
+
import { createMemoryMiddleware } from './middleware.js';
|
|
4
|
+
import { createTools } from './tools.js';
|
|
5
|
+
/**
|
|
6
|
+
* Create the provider. Nothing is read or sent until the first call, so it is safe to create at import time.
|
|
7
|
+
* `apiKey` defaults to HARNY_API_KEY, `baseURL` to HARNY_API_URL, else https://api.harny.ai.
|
|
8
|
+
*/
|
|
9
|
+
export function createHarny(options = {}) {
|
|
10
|
+
const client = new HarnyClient(options);
|
|
11
|
+
const wrap = (model, settings = {}) => {
|
|
12
|
+
const resolved = typeof model === 'string' ? gateway(model) : model;
|
|
13
|
+
return wrapLanguageModel({
|
|
14
|
+
model: resolved,
|
|
15
|
+
middleware: createMemoryMiddleware(client, settings),
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
const provider = ((model, settings) => wrap(model, settings));
|
|
19
|
+
provider.languageModel = wrap;
|
|
20
|
+
provider.middleware = (settings) => createMemoryMiddleware(client, settings);
|
|
21
|
+
provider.tools = (settings) => createTools(client, { user: options.user, ...settings });
|
|
22
|
+
provider.client = client;
|
|
23
|
+
provider.ingest = client.ingest.bind(client);
|
|
24
|
+
provider.ingestBatch = client.ingestBatch.bind(client);
|
|
25
|
+
provider.getIngestion = client.getIngestion.bind(client);
|
|
26
|
+
provider.waitForIngestion = client.waitForIngestion.bind(client);
|
|
27
|
+
provider.deleteIngestion = client.deleteIngestion.bind(client);
|
|
28
|
+
provider.deleteDataPoints = client.deleteDataPoints.bind(client);
|
|
29
|
+
provider.recall = client.recall.bind(client);
|
|
30
|
+
provider.usage = client.usage.bind(client);
|
|
31
|
+
return provider;
|
|
32
|
+
}
|
|
33
|
+
/** A ready provider that reads HARNY_API_KEY and HARNY_API_URL. Name the person per call: harny(model, { user }). */
|
|
34
|
+
export const harny = createHarny();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type PromptMessage } from './messages.js';
|
|
2
|
+
import type { HarnyClientOptions, IngestReceipt, RecallLevel, RecallResult } from './types.js';
|
|
3
|
+
export interface MemoryCallOptions extends HarnyClientOptions {
|
|
4
|
+
level?: RecallLevel;
|
|
5
|
+
maxTokens?: number;
|
|
6
|
+
limit?: number;
|
|
7
|
+
/** For `addMemories`: names the conversation for the ids. Default 'default'. */
|
|
8
|
+
conversationId?: string;
|
|
9
|
+
/** For `addMemories`: extra metadata on every stored message. */
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
/** For `retrieveMemories`: the line above the memories. */
|
|
12
|
+
heading?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Store the user messages of a prompt-shaped array. Assistant and system messages are skipped: only what the
|
|
16
|
+
* person said is kept. Returns one receipt per stored message.
|
|
17
|
+
*/
|
|
18
|
+
export declare function addMemories(messages: readonly PromptMessage[], options?: MemoryCallOptions): Promise<IngestReceipt[]>;
|
|
19
|
+
/** The raw recalled results for the last user message of a prompt (or for a plain string). */
|
|
20
|
+
export declare function getMemories(prompt: string | readonly PromptMessage[], options?: MemoryCallOptions): Promise<RecallResult[]>;
|
|
21
|
+
/** A system-prompt block with the recalled memories, or '' when there are none. */
|
|
22
|
+
export declare function retrieveMemories(prompt: string | readonly PromptMessage[], options?: MemoryCallOptions): Promise<string>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { HarnyClient } from './client.js';
|
|
2
|
+
import { DEFAULT_HEADING, formatMemories } from './format.js';
|
|
3
|
+
import { lastUserQuery, textOf, turnId } from './messages.js';
|
|
4
|
+
function asPrompt(prompt) {
|
|
5
|
+
return typeof prompt === 'string' ? [{ role: 'user', content: prompt }] : [...prompt];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Store the user messages of a prompt-shaped array. Assistant and system messages are skipped: only what the
|
|
9
|
+
* person said is kept. Returns one receipt per stored message.
|
|
10
|
+
*/
|
|
11
|
+
export async function addMemories(messages, options = {}) {
|
|
12
|
+
const client = new HarnyClient(options);
|
|
13
|
+
const conversation = options.conversationId ?? 'default';
|
|
14
|
+
const receipts = [];
|
|
15
|
+
for (const message of messages) {
|
|
16
|
+
if (message.role !== 'user')
|
|
17
|
+
continue;
|
|
18
|
+
const text = textOf(message);
|
|
19
|
+
if (!text)
|
|
20
|
+
continue;
|
|
21
|
+
receipts.push(await client.ingest({
|
|
22
|
+
content: text,
|
|
23
|
+
id: await turnId(conversation, text),
|
|
24
|
+
label: 'User message',
|
|
25
|
+
timestamp: new Date(),
|
|
26
|
+
...(options.metadata ? { metadata: options.metadata } : {}),
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
return receipts;
|
|
30
|
+
}
|
|
31
|
+
/** The raw recalled results for the last user message of a prompt (or for a plain string). */
|
|
32
|
+
export async function getMemories(prompt, options = {}) {
|
|
33
|
+
const query = lastUserQuery(asPrompt(prompt));
|
|
34
|
+
if (!query)
|
|
35
|
+
return [];
|
|
36
|
+
const client = new HarnyClient(options);
|
|
37
|
+
const answer = await client.recall({
|
|
38
|
+
query,
|
|
39
|
+
limit: options.limit ?? 20,
|
|
40
|
+
...(options.maxTokens !== undefined ? { maxTokens: options.maxTokens } : { level: options.level ?? 'low' }),
|
|
41
|
+
});
|
|
42
|
+
return answer.results;
|
|
43
|
+
}
|
|
44
|
+
/** A system-prompt block with the recalled memories, or '' when there are none. */
|
|
45
|
+
export async function retrieveMemories(prompt, options = {}) {
|
|
46
|
+
return formatMemories(await getMemories(prompt, options), options.heading ?? DEFAULT_HEADING);
|
|
47
|
+
}
|