pilotswarm-horizon-store 0.2.2
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/README.md +154 -0
- package/dist/src/agent-tools.d.ts +32 -0
- package/dist/src/agent-tools.d.ts.map +1 -0
- package/dist/src/agent-tools.js +263 -0
- package/dist/src/agent-tools.js.map +1 -0
- package/dist/src/config.d.ts +93 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +120 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/db-retry.d.ts +21 -0
- package/dist/src/db-retry.d.ts.map +1 -0
- package/dist/src/db-retry.js +94 -0
- package/dist/src/db-retry.js.map +1 -0
- package/dist/src/embedding-client.d.ts +13 -0
- package/dist/src/embedding-client.d.ts.map +1 -0
- package/dist/src/embedding-client.js +79 -0
- package/dist/src/embedding-client.js.map +1 -0
- package/dist/src/graph-model.d.ts +49 -0
- package/dist/src/graph-model.d.ts.map +1 -0
- package/dist/src/graph-model.js +112 -0
- package/dist/src/graph-model.js.map +1 -0
- package/dist/src/graph-queries.d.ts +83 -0
- package/dist/src/graph-queries.d.ts.map +1 -0
- package/dist/src/graph-queries.js +727 -0
- package/dist/src/graph-queries.js.map +1 -0
- package/dist/src/graph-store.d.ts +57 -0
- package/dist/src/graph-store.d.ts.map +1 -0
- package/dist/src/graph-store.js +327 -0
- package/dist/src/graph-store.js.map +1 -0
- package/dist/src/horizon-migrator.d.ts +49 -0
- package/dist/src/horizon-migrator.d.ts.map +1 -0
- package/dist/src/horizon-migrator.js +231 -0
- package/dist/src/horizon-migrator.js.map +1 -0
- package/dist/src/horizon-store.d.ts +116 -0
- package/dist/src/horizon-store.d.ts.map +1 -0
- package/dist/src/horizon-store.js +563 -0
- package/dist/src/horizon-store.js.map +1 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +21 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/preconditions.d.ts +16 -0
- package/dist/src/preconditions.d.ts.map +1 -0
- package/dist/src/preconditions.js +99 -0
- package/dist/src/preconditions.js.map +1 -0
- package/dist/src/query-builder.d.ts +37 -0
- package/dist/src/query-builder.d.ts.map +1 -0
- package/dist/src/query-builder.js +82 -0
- package/dist/src/query-builder.js.map +1 -0
- package/dist/src/sql-util.d.ts +20 -0
- package/dist/src/sql-util.d.ts.map +1 -0
- package/dist/src/sql-util.js +40 -0
- package/dist/src/sql-util.js.map +1 -0
- package/dist/src/types.d.ts +16 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +35 -0
- package/dist/src/types.js.map +1 -0
- package/migrations/0001_facts_table.sql +77 -0
- package/migrations/0002_indexes.sql +27 -0
- package/migrations/0003_age_bootstrap.sql +13 -0
- package/migrations/0004_facts_procs.sql +253 -0
- package/migrations/0005_embedder_workflow.sql +119 -0
- package/migrations/0006_facts_read_uncrawled_embedded_gate.sql +33 -0
- package/migrations/0007_embed_key_value_text.sql +122 -0
- package/migrations/0008_embedder_failure_recovery.sql +409 -0
- package/migrations/0009_search_text_full_value.sql +52 -0
- package/migrations/0010_minimal_two_loop_embedder.sql +392 -0
- package/migrations/0011_unified_api_embedder_workflow.sql +273 -0
- package/migrations/0012_facts_soft_delete.sql +587 -0
- package/migrations/0013_graph_namespaces.sql +60 -0
- package/package.json +69 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
-- 0008_embedder_failure_recovery — isolate failed embedding rows so one bad
|
|
2
|
+
-- fact cannot block the durable embedder backlog.
|
|
3
|
+
--
|
|
4
|
+
-- Behaviour:
|
|
5
|
+
-- - facts.last_embed_error stores a numeric terminal error code for the
|
|
6
|
+
-- current row value. NULL means the row remains eligible for embedding.
|
|
7
|
+
-- - facts.embed_retry_at marks a row selected from a failed batch for one-row
|
|
8
|
+
-- retry. It is transient workflow state on the fact row itself.
|
|
9
|
+
-- - facts_touch clears last_embed_error and embed_retry_at on INSERT/content
|
|
10
|
+
-- change, and clears the embedding itself so edits reset the row to normal
|
|
11
|
+
-- pending embedding without content/embedding hash columns.
|
|
12
|
+
-- - A failed batch marks its rows with embed_retry_at. The loop then processes
|
|
13
|
+
-- those rows one at a time before selecting new batches. A single-row failure
|
|
14
|
+
-- marks that fact terminally failed and stamps last_crawled_at so an
|
|
15
|
+
-- embedded-only graph harvester is not starved forever.
|
|
16
|
+
-- - facts_embedding_failures exposes counts and samples for the Facts Manager.
|
|
17
|
+
--
|
|
18
|
+
-- Error code convention:
|
|
19
|
+
-- 1001 input_too_large
|
|
20
|
+
-- 1400 provider_bad_request
|
|
21
|
+
-- 1401 provider_authentication_failed
|
|
22
|
+
-- 1403 provider_authorization_failed
|
|
23
|
+
-- 1429 provider_rate_limited
|
|
24
|
+
-- 1500 provider_server_error
|
|
25
|
+
-- 1901 provider_malformed_response
|
|
26
|
+
-- 9999 unknown_embedding_error
|
|
27
|
+
--
|
|
28
|
+
-- Tokens: {{SCHEMA}}.
|
|
29
|
+
|
|
30
|
+
ALTER TABLE "{{SCHEMA}}".facts ADD COLUMN IF NOT EXISTS last_embed_error INT;
|
|
31
|
+
ALTER TABLE "{{SCHEMA}}".facts ADD COLUMN IF NOT EXISTS last_embed_error_at TIMESTAMPTZ;
|
|
32
|
+
ALTER TABLE "{{SCHEMA}}".facts ADD COLUMN IF NOT EXISTS embed_retry_at TIMESTAMPTZ;
|
|
33
|
+
|
|
34
|
+
ALTER TABLE "{{SCHEMA}}".facts DROP COLUMN IF EXISTS last_embedded_hash;
|
|
35
|
+
ALTER TABLE "{{SCHEMA}}".facts DROP COLUMN IF EXISTS content_hash;
|
|
36
|
+
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_facts_last_embed_error
|
|
38
|
+
ON "{{SCHEMA}}".facts (last_embed_error, id) WHERE last_embed_error IS NOT NULL;
|
|
39
|
+
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_facts_embed_retry
|
|
41
|
+
ON "{{SCHEMA}}".facts (embed_retry_at, id) WHERE embed_retry_at IS NOT NULL;
|
|
42
|
+
|
|
43
|
+
CREATE OR REPLACE FUNCTION "{{SCHEMA}}".embed_error_label(p_code INT) RETURNS TEXT
|
|
44
|
+
LANGUAGE sql IMMUTABLE AS $$
|
|
45
|
+
SELECT CASE p_code
|
|
46
|
+
WHEN 1001 THEN 'input_too_large'
|
|
47
|
+
WHEN 1400 THEN 'provider_bad_request'
|
|
48
|
+
WHEN 1401 THEN 'provider_authentication_failed'
|
|
49
|
+
WHEN 1403 THEN 'provider_authorization_failed'
|
|
50
|
+
WHEN 1429 THEN 'provider_rate_limited'
|
|
51
|
+
WHEN 1500 THEN 'provider_server_error'
|
|
52
|
+
WHEN 1901 THEN 'provider_malformed_response'
|
|
53
|
+
WHEN 9999 THEN 'unknown_embedding_error'
|
|
54
|
+
ELSE 'unknown_embedding_error'
|
|
55
|
+
END;
|
|
56
|
+
$$;
|
|
57
|
+
|
|
58
|
+
CREATE OR REPLACE FUNCTION "{{SCHEMA}}".embed_error_code(p_status INT, p_body TEXT) RETURNS INT
|
|
59
|
+
LANGUAGE sql IMMUTABLE AS $$
|
|
60
|
+
SELECT CASE
|
|
61
|
+
WHEN p_body ILIKE '%maximum input length%' OR p_body ILIKE '%too many tokens%' OR p_body ILIKE '%input%too%long%'
|
|
62
|
+
THEN 1001
|
|
63
|
+
WHEN p_status = 400 THEN 1400
|
|
64
|
+
WHEN p_status = 401 THEN 1401
|
|
65
|
+
WHEN p_status = 403 THEN 1403
|
|
66
|
+
WHEN p_status = 429 THEN 1429
|
|
67
|
+
WHEN p_status BETWEEN 500 AND 599 THEN 1500
|
|
68
|
+
ELSE 9999
|
|
69
|
+
END;
|
|
70
|
+
$$;
|
|
71
|
+
|
|
72
|
+
CREATE OR REPLACE FUNCTION "{{SCHEMA}}".embed_response_data_count(p_resp JSONB) RETURNS INT
|
|
73
|
+
LANGUAGE plpgsql IMMUTABLE AS $$
|
|
74
|
+
DECLARE
|
|
75
|
+
j JSONB;
|
|
76
|
+
BEGIN
|
|
77
|
+
IF NOT coalesce((p_resp->>'ok')::boolean, false) THEN
|
|
78
|
+
RETURN -1;
|
|
79
|
+
END IF;
|
|
80
|
+
IF coalesce(CASE WHEN (p_resp->>'status') ~ '^\d+$' THEN (p_resp->>'status')::int ELSE 0 END, 0) NOT BETWEEN 200 AND 299 THEN
|
|
81
|
+
RETURN -1;
|
|
82
|
+
END IF;
|
|
83
|
+
j := (p_resp->>'body')::jsonb;
|
|
84
|
+
IF jsonb_typeof(j->'data') <> 'array' THEN
|
|
85
|
+
RETURN -2;
|
|
86
|
+
END IF;
|
|
87
|
+
RETURN jsonb_array_length(j->'data');
|
|
88
|
+
EXCEPTION WHEN OTHERS THEN
|
|
89
|
+
RETURN -2;
|
|
90
|
+
END $$;
|
|
91
|
+
|
|
92
|
+
CREATE OR REPLACE FUNCTION "{{SCHEMA}}".embed_response_terminal_code(p_resp JSONB) RETURNS INT
|
|
93
|
+
LANGUAGE sql IMMUTABLE AS $$
|
|
94
|
+
SELECT CASE
|
|
95
|
+
WHEN "{{SCHEMA}}".embed_response_data_count(p_resp) = -2 THEN 1901
|
|
96
|
+
ELSE "{{SCHEMA}}".embed_error_code(
|
|
97
|
+
coalesce(CASE WHEN (p_resp->>'status') ~ '^\d+$' THEN (p_resp->>'status')::int ELSE 0 END, 0),
|
|
98
|
+
coalesce(p_resp->>'body', ''))
|
|
99
|
+
END;
|
|
100
|
+
$$;
|
|
101
|
+
|
|
102
|
+
CREATE OR REPLACE FUNCTION "{{SCHEMA}}".facts_touch() RETURNS trigger
|
|
103
|
+
LANGUAGE plpgsql AS $$
|
|
104
|
+
BEGIN
|
|
105
|
+
IF TG_OP = 'INSERT' OR NEW.key IS DISTINCT FROM OLD.key OR NEW.value IS DISTINCT FROM OLD.value THEN
|
|
106
|
+
NEW.updated_at := now();
|
|
107
|
+
NEW.last_crawled_at := NULL;
|
|
108
|
+
NEW.embedding := NULL;
|
|
109
|
+
NEW.embedded_at := NULL;
|
|
110
|
+
NEW.embedding_model := NULL;
|
|
111
|
+
NEW.last_embed_error := NULL;
|
|
112
|
+
NEW.last_embed_error_at := NULL;
|
|
113
|
+
NEW.embed_retry_at := NULL;
|
|
114
|
+
END IF;
|
|
115
|
+
RETURN NEW;
|
|
116
|
+
END $$;
|
|
117
|
+
|
|
118
|
+
CREATE OR REPLACE FUNCTION "{{SCHEMA}}".facts_embedding_failures(
|
|
119
|
+
p_ns_prefix TEXT DEFAULT NULL,
|
|
120
|
+
p_error_codes INT[] DEFAULT NULL,
|
|
121
|
+
p_limit INT DEFAULT 20
|
|
122
|
+
) RETURNS TABLE (
|
|
123
|
+
row_kind TEXT,
|
|
124
|
+
code INT,
|
|
125
|
+
label TEXT,
|
|
126
|
+
count BIGINT,
|
|
127
|
+
oldest_at TIMESTAMPTZ,
|
|
128
|
+
newest_at TIMESTAMPTZ,
|
|
129
|
+
max_input_chars BIGINT,
|
|
130
|
+
scope_key TEXT,
|
|
131
|
+
key TEXT,
|
|
132
|
+
value JSONB,
|
|
133
|
+
agent_id TEXT,
|
|
134
|
+
session_id TEXT,
|
|
135
|
+
shared BOOLEAN,
|
|
136
|
+
tags TEXT[],
|
|
137
|
+
created_at TIMESTAMPTZ,
|
|
138
|
+
updated_at TIMESTAMPTZ,
|
|
139
|
+
last_embed_error INT,
|
|
140
|
+
last_embed_error_at TIMESTAMPTZ,
|
|
141
|
+
input_chars BIGINT
|
|
142
|
+
)
|
|
143
|
+
LANGUAGE sql STABLE AS $$
|
|
144
|
+
WITH failed AS (
|
|
145
|
+
SELECT f.*,
|
|
146
|
+
char_length(coalesce(f.key, '') || E'\n' || coalesce(f.value::text, ''))::bigint AS input_chars
|
|
147
|
+
FROM "{{SCHEMA}}".facts f
|
|
148
|
+
WHERE f.last_embed_error IS NOT NULL
|
|
149
|
+
AND (p_ns_prefix IS NULL OR f.key LIKE p_ns_prefix)
|
|
150
|
+
AND (p_error_codes IS NULL OR f.last_embed_error = ANY(p_error_codes))
|
|
151
|
+
), stats AS (
|
|
152
|
+
SELECT f.last_embed_error AS code,
|
|
153
|
+
"{{SCHEMA}}".embed_error_label(f.last_embed_error) AS label,
|
|
154
|
+
count(*)::bigint AS count,
|
|
155
|
+
min(f.last_embed_error_at) AS oldest_at,
|
|
156
|
+
max(f.last_embed_error_at) AS newest_at,
|
|
157
|
+
coalesce(max(f.input_chars), 0)::bigint AS max_input_chars
|
|
158
|
+
FROM failed f
|
|
159
|
+
GROUP BY f.last_embed_error
|
|
160
|
+
), samples AS (
|
|
161
|
+
SELECT f.*
|
|
162
|
+
FROM failed f
|
|
163
|
+
ORDER BY f.last_embed_error_at DESC NULLS LAST, f.id DESC
|
|
164
|
+
LIMIT greatest(0, least(coalesce(p_limit, 20), 500))
|
|
165
|
+
)
|
|
166
|
+
SELECT 'stat'::text AS row_kind,
|
|
167
|
+
s.code, s.label, s.count, s.oldest_at, s.newest_at, s.max_input_chars,
|
|
168
|
+
NULL::text AS scope_key, NULL::text AS key, NULL::jsonb AS value,
|
|
169
|
+
NULL::text AS agent_id, NULL::text AS session_id, NULL::boolean AS shared,
|
|
170
|
+
NULL::text[] AS tags, NULL::timestamptz AS created_at, NULL::timestamptz AS updated_at,
|
|
171
|
+
NULL::int AS last_embed_error, NULL::timestamptz AS last_embed_error_at,
|
|
172
|
+
NULL::bigint AS input_chars
|
|
173
|
+
FROM stats s
|
|
174
|
+
UNION ALL
|
|
175
|
+
SELECT 'fact'::text AS row_kind,
|
|
176
|
+
f.last_embed_error AS code,
|
|
177
|
+
"{{SCHEMA}}".embed_error_label(f.last_embed_error) AS label,
|
|
178
|
+
NULL::bigint AS count, NULL::timestamptz AS oldest_at, NULL::timestamptz AS newest_at,
|
|
179
|
+
NULL::bigint AS max_input_chars,
|
|
180
|
+
f.scope_key, f.key, f.value, f.agent_id, f.session_id, f.shared, f.tags,
|
|
181
|
+
f.created_at, f.updated_at, f.last_embed_error,
|
|
182
|
+
f.last_embed_error_at, f.input_chars
|
|
183
|
+
FROM samples f
|
|
184
|
+
ORDER BY row_kind DESC, count DESC NULLS LAST, newest_at DESC NULLS LAST, last_embed_error_at DESC NULLS LAST;
|
|
185
|
+
$$;
|
|
186
|
+
|
|
187
|
+
DROP FUNCTION IF EXISTS "{{SCHEMA}}".facts_mark_crawled(JSONB);
|
|
188
|
+
|
|
189
|
+
CREATE OR REPLACE FUNCTION "{{SCHEMA}}".facts_mark_crawled(p_stamps JSONB)
|
|
190
|
+
RETURNS TABLE (marked INT, skipped INT)
|
|
191
|
+
LANGUAGE sql AS $$
|
|
192
|
+
WITH stamps AS (
|
|
193
|
+
SELECT e->>'scopeKey' AS scope_key
|
|
194
|
+
FROM jsonb_array_elements(p_stamps) e
|
|
195
|
+
),
|
|
196
|
+
upd AS (
|
|
197
|
+
UPDATE "{{SCHEMA}}".facts f
|
|
198
|
+
SET last_crawled_at = now()
|
|
199
|
+
FROM stamps s
|
|
200
|
+
WHERE f.scope_key = s.scope_key
|
|
201
|
+
AND f.last_crawled_at IS NULL
|
|
202
|
+
RETURNING f.scope_key
|
|
203
|
+
)
|
|
204
|
+
SELECT (SELECT count(*) FROM upd)::int AS marked,
|
|
205
|
+
((SELECT count(*) FROM stamps) - (SELECT count(*) FROM upd))::int AS skipped;
|
|
206
|
+
$$;
|
|
207
|
+
|
|
208
|
+
CREATE OR REPLACE FUNCTION "{{SCHEMA}}".embedder_workflow(p_interval int, p_batch int)
|
|
209
|
+
RETURNS text
|
|
210
|
+
LANGUAGE plpgsql AS $fn$
|
|
211
|
+
DECLARE
|
|
212
|
+
v_url text;
|
|
213
|
+
v_model text;
|
|
214
|
+
v_key text;
|
|
215
|
+
v_keyhdr text;
|
|
216
|
+
v_bearer text;
|
|
217
|
+
v_inputfield text;
|
|
218
|
+
v_timeout int;
|
|
219
|
+
v_headers jsonb;
|
|
220
|
+
v_batch_sql text;
|
|
221
|
+
v_resp_ok_sql text;
|
|
222
|
+
v_batch_fail_sql text;
|
|
223
|
+
v_zip_sql text;
|
|
224
|
+
v_retry_sql text;
|
|
225
|
+
v_retry_exists_sql text;
|
|
226
|
+
v_retry_resp_ok_sql text;
|
|
227
|
+
v_retry_zip_sql text;
|
|
228
|
+
v_retry_fail_sql text;
|
|
229
|
+
BEGIN
|
|
230
|
+
v_url := df.getvar('hz_{{SCHEMA}}_url');
|
|
231
|
+
v_model := df.getvar('hz_{{SCHEMA}}_model');
|
|
232
|
+
v_key := df.getvar('hz_{{SCHEMA}}_key');
|
|
233
|
+
v_keyhdr := coalesce(df.getvar('hz_{{SCHEMA}}_keyhdr'), 'api-key');
|
|
234
|
+
v_bearer := coalesce(df.getvar('hz_{{SCHEMA}}_bearer'), 'false');
|
|
235
|
+
v_inputfield := coalesce(df.getvar('hz_{{SCHEMA}}_inputfield'), 'input');
|
|
236
|
+
v_timeout := coalesce(df.getvar('hz_{{SCHEMA}}_timeout'), '30')::int;
|
|
237
|
+
|
|
238
|
+
IF v_url IS NULL OR v_model IS NULL THEN
|
|
239
|
+
RAISE EXCEPTION 'embedder not configured: call configureEmbedder first (durable vars hz_{{SCHEMA}}_url / hz_{{SCHEMA}}_model missing)';
|
|
240
|
+
END IF;
|
|
241
|
+
|
|
242
|
+
v_headers := jsonb_build_object('content-type', 'application/json');
|
|
243
|
+
IF v_key IS NOT NULL AND v_key <> '' THEN
|
|
244
|
+
v_headers := v_headers || jsonb_build_object(
|
|
245
|
+
v_keyhdr,
|
|
246
|
+
CASE WHEN v_bearer = 'true' THEN 'Bearer ' || v_key ELSE v_key END
|
|
247
|
+
);
|
|
248
|
+
END IF;
|
|
249
|
+
|
|
250
|
+
v_batch_sql := format(
|
|
251
|
+
$q$WITH retry AS (
|
|
252
|
+
SELECT id
|
|
253
|
+
FROM "{{SCHEMA}}".facts
|
|
254
|
+
WHERE embed_retry_at IS NOT NULL
|
|
255
|
+
AND last_embed_error IS NULL
|
|
256
|
+
ORDER BY embed_retry_at, id
|
|
257
|
+
LIMIT 1
|
|
258
|
+
)
|
|
259
|
+
SELECT jsonb_build_object(%L, jsonb_agg(t.txt ORDER BY t.id), 'model', %L)::text AS body,
|
|
260
|
+
array_agg(t.id ORDER BY t.id)::text AS ids,
|
|
261
|
+
array_agg(t.updated_at ORDER BY t.id)::text AS updated_ats
|
|
262
|
+
FROM (SELECT id, updated_at,
|
|
263
|
+
coalesce(key, '') || E'\n' || coalesce(value::text, '') AS txt
|
|
264
|
+
FROM "{{SCHEMA}}".facts
|
|
265
|
+
WHERE last_embed_error IS NULL
|
|
266
|
+
AND embed_retry_at IS NULL
|
|
267
|
+
AND NOT EXISTS (SELECT 1 FROM retry)
|
|
268
|
+
AND (embedding IS NULL
|
|
269
|
+
OR embedding_model IS DISTINCT FROM %L)
|
|
270
|
+
ORDER BY id
|
|
271
|
+
LIMIT %s) t
|
|
272
|
+
HAVING count(*) > 0$q$,
|
|
273
|
+
v_inputfield, v_model, v_model, greatest(p_batch, 1));
|
|
274
|
+
|
|
275
|
+
v_resp_ok_sql := $q$SELECT 1
|
|
276
|
+
WHERE "{{SCHEMA}}".embed_response_data_count($resp::jsonb) = cardinality(($batch.ids)::bigint[])$q$;
|
|
277
|
+
|
|
278
|
+
v_batch_fail_sql := $q$WITH selected AS (
|
|
279
|
+
SELECT u.id, u.seen_updated_at
|
|
280
|
+
FROM unnest(($batch.ids)::bigint[], ($batch.updated_ats)::timestamptz[]) AS u(id, seen_updated_at)
|
|
281
|
+
)
|
|
282
|
+
UPDATE "{{SCHEMA}}".facts f
|
|
283
|
+
SET embed_retry_at = now()
|
|
284
|
+
FROM selected s
|
|
285
|
+
WHERE f.id = s.id
|
|
286
|
+
AND f.updated_at IS NOT DISTINCT FROM s.seen_updated_at
|
|
287
|
+
AND f.last_embed_error IS NULL$q$;
|
|
288
|
+
|
|
289
|
+
v_zip_sql := format(
|
|
290
|
+
$q$WITH resp AS (SELECT ($resp::jsonb->>'body')::jsonb AS j),
|
|
291
|
+
emb AS (SELECT v.ord, v.e->'embedding' AS vec
|
|
292
|
+
FROM resp, jsonb_array_elements(resp.j->'data') WITH ORDINALITY AS v(e, ord)),
|
|
293
|
+
tgt AS (SELECT u.id, u.seen_updated_at, u.ord
|
|
294
|
+
FROM unnest(($batch.ids)::bigint[], ($batch.updated_ats)::timestamptz[]) WITH ORDINALITY AS u(id, seen_updated_at, ord)),
|
|
295
|
+
upd AS (
|
|
296
|
+
UPDATE "{{SCHEMA}}".facts f
|
|
297
|
+
SET embedding = (emb.vec)::text::vector,
|
|
298
|
+
embedded_at = now(),
|
|
299
|
+
embedding_model = %L,
|
|
300
|
+
last_embed_error = NULL,
|
|
301
|
+
last_embed_error_at = NULL,
|
|
302
|
+
embed_retry_at = NULL
|
|
303
|
+
FROM emb JOIN tgt ON tgt.ord = emb.ord
|
|
304
|
+
WHERE f.id = tgt.id
|
|
305
|
+
AND f.updated_at IS NOT DISTINCT FROM tgt.seen_updated_at
|
|
306
|
+
RETURNING f.id)
|
|
307
|
+
SELECT count(*) FROM upd$q$,
|
|
308
|
+
v_model);
|
|
309
|
+
|
|
310
|
+
v_retry_sql := format(
|
|
311
|
+
$q$WITH picked AS (
|
|
312
|
+
SELECT id, updated_at
|
|
313
|
+
FROM "{{SCHEMA}}".facts
|
|
314
|
+
WHERE embed_retry_at IS NOT NULL
|
|
315
|
+
AND last_embed_error IS NULL
|
|
316
|
+
ORDER BY embed_retry_at, id
|
|
317
|
+
LIMIT 1
|
|
318
|
+
)
|
|
319
|
+
SELECT jsonb_build_object(%L, jsonb_agg(t.txt ORDER BY t.id), 'model', %L)::text AS body,
|
|
320
|
+
array_agg(t.id ORDER BY t.id)::text AS ids,
|
|
321
|
+
array_agg(t.updated_at ORDER BY t.id)::text AS updated_ats
|
|
322
|
+
FROM (SELECT f.id, f.updated_at,
|
|
323
|
+
coalesce(f.key, '') || E'\n' || coalesce(f.value::text, '') AS txt
|
|
324
|
+
FROM "{{SCHEMA}}".facts f
|
|
325
|
+
JOIN picked p ON p.id = f.id AND p.updated_at IS NOT DISTINCT FROM f.updated_at) t
|
|
326
|
+
HAVING count(*) > 0$q$,
|
|
327
|
+
v_inputfield, v_model);
|
|
328
|
+
|
|
329
|
+
v_retry_exists_sql := $q$SELECT 1
|
|
330
|
+
FROM "{{SCHEMA}}".facts
|
|
331
|
+
WHERE embed_retry_at IS NOT NULL
|
|
332
|
+
AND last_embed_error IS NULL
|
|
333
|
+
LIMIT 1$q$;
|
|
334
|
+
|
|
335
|
+
v_retry_resp_ok_sql := $q$SELECT 1
|
|
336
|
+
WHERE "{{SCHEMA}}".embed_response_data_count($retry_resp::jsonb) = cardinality(($retry.ids)::bigint[])$q$;
|
|
337
|
+
|
|
338
|
+
v_retry_zip_sql := format(
|
|
339
|
+
$q$WITH resp AS (SELECT ($retry_resp::jsonb->>'body')::jsonb AS j),
|
|
340
|
+
emb AS (SELECT v.ord, v.e->'embedding' AS vec
|
|
341
|
+
FROM resp, jsonb_array_elements(resp.j->'data') WITH ORDINALITY AS v(e, ord)),
|
|
342
|
+
tgt AS (SELECT u.id, u.seen_updated_at, u.ord
|
|
343
|
+
FROM unnest(($retry.ids)::bigint[], ($retry.updated_ats)::timestamptz[]) WITH ORDINALITY AS u(id, seen_updated_at, ord)),
|
|
344
|
+
upd AS (
|
|
345
|
+
UPDATE "{{SCHEMA}}".facts f
|
|
346
|
+
SET embedding = (emb.vec)::text::vector,
|
|
347
|
+
embedded_at = now(),
|
|
348
|
+
embedding_model = %L,
|
|
349
|
+
last_embed_error = NULL,
|
|
350
|
+
last_embed_error_at = NULL,
|
|
351
|
+
embed_retry_at = NULL
|
|
352
|
+
FROM emb JOIN tgt ON tgt.ord = emb.ord
|
|
353
|
+
WHERE f.id = tgt.id
|
|
354
|
+
AND f.updated_at IS NOT DISTINCT FROM tgt.seen_updated_at
|
|
355
|
+
RETURNING f.id)
|
|
356
|
+
SELECT count(*) FROM upd$q$,
|
|
357
|
+
v_model);
|
|
358
|
+
|
|
359
|
+
v_retry_fail_sql := $q$WITH resp AS (
|
|
360
|
+
SELECT $retry_resp::jsonb AS r
|
|
361
|
+
), tgt AS (
|
|
362
|
+
SELECT u.id, u.seen_updated_at
|
|
363
|
+
FROM unnest(($retry.ids)::bigint[], ($retry.updated_ats)::timestamptz[]) AS u(id, seen_updated_at)
|
|
364
|
+
), classified AS (
|
|
365
|
+
SELECT tgt.id, tgt.seen_updated_at,
|
|
366
|
+
"{{SCHEMA}}".embed_response_terminal_code(resp.r) AS code
|
|
367
|
+
FROM tgt, resp
|
|
368
|
+
), upd AS (
|
|
369
|
+
UPDATE "{{SCHEMA}}".facts f
|
|
370
|
+
SET last_embed_error = classified.code,
|
|
371
|
+
last_embed_error_at = now(),
|
|
372
|
+
last_crawled_at = now(),
|
|
373
|
+
embed_retry_at = NULL
|
|
374
|
+
FROM classified
|
|
375
|
+
WHERE f.id = classified.id
|
|
376
|
+
AND f.updated_at IS NOT DISTINCT FROM classified.seen_updated_at
|
|
377
|
+
RETURNING f.id)
|
|
378
|
+
SELECT count(*) FROM upd$q$;
|
|
379
|
+
|
|
380
|
+
RETURN df.loop(
|
|
381
|
+
df.seq(
|
|
382
|
+
df.sleep(greatest(p_interval, 1)::bigint),
|
|
383
|
+
df.seq(
|
|
384
|
+
df.as(df.sql(v_batch_sql), 'batch'),
|
|
385
|
+
df.seq(
|
|
386
|
+
df.if_rows('batch',
|
|
387
|
+
df.seq(
|
|
388
|
+
df.as(df.http(v_url, 'POST', '$batch.body', v_headers, v_timeout), 'resp'),
|
|
389
|
+
df.seq(
|
|
390
|
+
df.as(df.sql(v_resp_ok_sql), 'resp_ok'),
|
|
391
|
+
df.if_rows('resp_ok',
|
|
392
|
+
df.sql(v_zip_sql),
|
|
393
|
+
df.sql(v_batch_fail_sql)))),
|
|
394
|
+
df.sql('SELECT 1')),
|
|
395
|
+
df.loop(
|
|
396
|
+
df.seq(
|
|
397
|
+
df.as(df.sql(v_retry_sql), 'retry'),
|
|
398
|
+
df.if_rows('retry',
|
|
399
|
+
df.seq(
|
|
400
|
+
df.as(df.http(v_url, 'POST', '$retry.body', v_headers, v_timeout), 'retry_resp'),
|
|
401
|
+
df.seq(
|
|
402
|
+
df.as(df.sql(v_retry_resp_ok_sql), 'retry_resp_ok'),
|
|
403
|
+
df.if_rows('retry_resp_ok',
|
|
404
|
+
df.sql(v_retry_zip_sql),
|
|
405
|
+
df.sql(v_retry_fail_sql)))),
|
|
406
|
+
df.sql('SELECT 1'))),
|
|
407
|
+
df.sql(v_retry_exists_sql))))),
|
|
408
|
+
NULL);
|
|
409
|
+
END $fn$;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
-- 0009_search_text_full_value — make lexical/BM25 search follow the public
|
|
2
|
+
-- store_fact contract.
|
|
3
|
+
--
|
|
4
|
+
-- The original generated search_text column indexed only a handful of
|
|
5
|
+
-- conventional JSON fields (name, description, text, body, subject). The
|
|
6
|
+
-- store_fact tool accepts any JSON-serializable value, and migration 0007
|
|
7
|
+
-- already moved embedding input to key + value::text for that reason. This
|
|
8
|
+
-- migration brings lexical search into the same shape so facts with fields such
|
|
9
|
+
-- as content, summary, detail, problem, or arbitrary nested JSON are searchable.
|
|
10
|
+
--
|
|
11
|
+
-- Tokens: {{SCHEMA}}.
|
|
12
|
+
|
|
13
|
+
DROP FUNCTION IF EXISTS "{{SCHEMA}}".facts_search_lexical(TEXT, TEXT, TEXT[], BOOLEAN, TEXT, TEXT, TEXT[], INT);
|
|
14
|
+
DROP INDEX IF EXISTS "{{SCHEMA}}".idx_facts_lexical;
|
|
15
|
+
|
|
16
|
+
ALTER TABLE "{{SCHEMA}}".facts DROP COLUMN IF EXISTS search_text;
|
|
17
|
+
ALTER TABLE "{{SCHEMA}}".facts ADD COLUMN search_text TEXT GENERATED ALWAYS AS (
|
|
18
|
+
coalesce(key, '') || E'\n' || coalesce(value::text, '')
|
|
19
|
+
) STORED;
|
|
20
|
+
|
|
21
|
+
CREATE EXTENSION IF NOT EXISTS pg_textsearch;
|
|
22
|
+
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_facts_lexical
|
|
24
|
+
ON "{{SCHEMA}}".facts USING bm25 (search_text) WITH (text_config = 'english');
|
|
25
|
+
|
|
26
|
+
CREATE OR REPLACE FUNCTION "{{SCHEMA}}".facts_search_lexical(
|
|
27
|
+
p_query TEXT,
|
|
28
|
+
p_reader TEXT,
|
|
29
|
+
p_granted TEXT[],
|
|
30
|
+
p_unrestricted BOOLEAN,
|
|
31
|
+
p_scope TEXT,
|
|
32
|
+
p_ns_prefix TEXT,
|
|
33
|
+
p_tags TEXT[],
|
|
34
|
+
p_pool INT
|
|
35
|
+
) RETURNS TABLE (
|
|
36
|
+
scope_key TEXT, key TEXT, value JSONB, agent_id TEXT, session_id TEXT,
|
|
37
|
+
shared BOOLEAN, tags TEXT[], created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ,
|
|
38
|
+
rank DOUBLE PRECISION
|
|
39
|
+
)
|
|
40
|
+
LANGUAGE sql STABLE AS $$
|
|
41
|
+
SELECT f.scope_key, f.key, f.value, f.agent_id, f.session_id,
|
|
42
|
+
f.shared, f.tags, f.created_at, f.updated_at,
|
|
43
|
+
(-(f.search_text <@> q.bq))::double precision AS rank
|
|
44
|
+
FROM "{{SCHEMA}}".facts f,
|
|
45
|
+
(SELECT to_bm25query(p_query, '{{SCHEMA}}.idx_facts_lexical') AS bq) q
|
|
46
|
+
WHERE (f.search_text <@> q.bq) < 0
|
|
47
|
+
AND "{{SCHEMA}}".facts_acl(f.shared, f.session_id, p_reader, p_granted, p_unrestricted, p_scope)
|
|
48
|
+
AND (p_ns_prefix IS NULL OR f.key LIKE p_ns_prefix)
|
|
49
|
+
AND (p_tags IS NULL OR f.tags @> p_tags)
|
|
50
|
+
ORDER BY f.search_text <@> q.bq ASC
|
|
51
|
+
LIMIT p_pool;
|
|
52
|
+
$$;
|