ahok-skill 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +8 -0
- package/Dockerfile +59 -0
- package/RAW_SKILL.md +219 -0
- package/README.md +277 -0
- package/SKILL.md +58 -0
- package/bin/opm.js +268 -0
- package/data/openmemory.sqlite +0 -0
- package/data/openmemory.sqlite-shm +0 -0
- package/data/openmemory.sqlite-wal +0 -0
- package/dist/ai/graph.js +293 -0
- package/dist/ai/mcp.js +397 -0
- package/dist/cli.js +78 -0
- package/dist/core/cfg.js +87 -0
- package/dist/core/db.js +636 -0
- package/dist/core/memory.js +116 -0
- package/dist/core/migrate.js +227 -0
- package/dist/core/models.js +105 -0
- package/dist/core/telemetry.js +57 -0
- package/dist/core/types.js +2 -0
- package/dist/core/vector/postgres.js +52 -0
- package/dist/core/vector/valkey.js +246 -0
- package/dist/core/vector_store.js +2 -0
- package/dist/index.js +44 -0
- package/dist/memory/decay.js +301 -0
- package/dist/memory/embed.js +675 -0
- package/dist/memory/hsg.js +959 -0
- package/dist/memory/reflect.js +131 -0
- package/dist/memory/user_summary.js +99 -0
- package/dist/migrate.js +9 -0
- package/dist/ops/compress.js +255 -0
- package/dist/ops/dynamics.js +189 -0
- package/dist/ops/extract.js +333 -0
- package/dist/ops/ingest.js +214 -0
- package/dist/server/index.js +109 -0
- package/dist/server/middleware/auth.js +137 -0
- package/dist/server/routes/auth.js +186 -0
- package/dist/server/routes/compression.js +108 -0
- package/dist/server/routes/dashboard.js +399 -0
- package/dist/server/routes/docs.js +241 -0
- package/dist/server/routes/dynamics.js +312 -0
- package/dist/server/routes/ide.js +280 -0
- package/dist/server/routes/index.js +33 -0
- package/dist/server/routes/keys.js +132 -0
- package/dist/server/routes/langgraph.js +61 -0
- package/dist/server/routes/memory.js +213 -0
- package/dist/server/routes/sources.js +140 -0
- package/dist/server/routes/system.js +63 -0
- package/dist/server/routes/temporal.js +293 -0
- package/dist/server/routes/users.js +101 -0
- package/dist/server/routes/vercel.js +57 -0
- package/dist/server/server.js +211 -0
- package/dist/server.js +3 -0
- package/dist/sources/base.js +223 -0
- package/dist/sources/github.js +171 -0
- package/dist/sources/google_drive.js +166 -0
- package/dist/sources/google_sheets.js +112 -0
- package/dist/sources/google_slides.js +139 -0
- package/dist/sources/index.js +34 -0
- package/dist/sources/notion.js +165 -0
- package/dist/sources/onedrive.js +143 -0
- package/dist/sources/web_crawler.js +166 -0
- package/dist/temporal_graph/index.js +20 -0
- package/dist/temporal_graph/query.js +240 -0
- package/dist/temporal_graph/store.js +116 -0
- package/dist/temporal_graph/timeline.js +241 -0
- package/dist/temporal_graph/types.js +2 -0
- package/dist/utils/chunking.js +60 -0
- package/dist/utils/index.js +31 -0
- package/dist/utils/keyword.js +94 -0
- package/dist/utils/text.js +120 -0
- package/nodemon.json +7 -0
- package/package.json +50 -0
- package/references/api_reference.md +66 -0
- package/references/examples.md +45 -0
- package/src/ai/graph.ts +363 -0
- package/src/ai/mcp.ts +494 -0
- package/src/cli.ts +94 -0
- package/src/core/cfg.ts +110 -0
- package/src/core/db.ts +1052 -0
- package/src/core/memory.ts +99 -0
- package/src/core/migrate.ts +302 -0
- package/src/core/models.ts +107 -0
- package/src/core/telemetry.ts +47 -0
- package/src/core/types.ts +130 -0
- package/src/core/vector/postgres.ts +61 -0
- package/src/core/vector/valkey.ts +261 -0
- package/src/core/vector_store.ts +9 -0
- package/src/index.ts +5 -0
- package/src/memory/decay.ts +427 -0
- package/src/memory/embed.ts +707 -0
- package/src/memory/hsg.ts +1245 -0
- package/src/memory/reflect.ts +158 -0
- package/src/memory/user_summary.ts +110 -0
- package/src/migrate.ts +8 -0
- package/src/ops/compress.ts +296 -0
- package/src/ops/dynamics.ts +272 -0
- package/src/ops/extract.ts +360 -0
- package/src/ops/ingest.ts +286 -0
- package/src/server/index.ts +159 -0
- package/src/server/middleware/auth.ts +156 -0
- package/src/server/routes/auth.ts +223 -0
- package/src/server/routes/compression.ts +106 -0
- package/src/server/routes/dashboard.ts +420 -0
- package/src/server/routes/docs.ts +380 -0
- package/src/server/routes/dynamics.ts +516 -0
- package/src/server/routes/ide.ts +283 -0
- package/src/server/routes/index.ts +32 -0
- package/src/server/routes/keys.ts +131 -0
- package/src/server/routes/langgraph.ts +71 -0
- package/src/server/routes/memory.ts +440 -0
- package/src/server/routes/sources.ts +111 -0
- package/src/server/routes/system.ts +68 -0
- package/src/server/routes/temporal.ts +335 -0
- package/src/server/routes/users.ts +111 -0
- package/src/server/routes/vercel.ts +55 -0
- package/src/server/server.js +215 -0
- package/src/server.ts +1 -0
- package/src/sources/base.ts +257 -0
- package/src/sources/github.ts +156 -0
- package/src/sources/google_drive.ts +144 -0
- package/src/sources/google_sheets.ts +85 -0
- package/src/sources/google_slides.ts +115 -0
- package/src/sources/index.ts +19 -0
- package/src/sources/notion.ts +148 -0
- package/src/sources/onedrive.ts +131 -0
- package/src/sources/web_crawler.ts +161 -0
- package/src/temporal_graph/index.ts +4 -0
- package/src/temporal_graph/query.ts +299 -0
- package/src/temporal_graph/store.ts +156 -0
- package/src/temporal_graph/timeline.ts +319 -0
- package/src/temporal_graph/types.ts +41 -0
- package/src/utils/chunking.ts +66 -0
- package/src/utils/index.ts +25 -0
- package/src/utils/keyword.ts +137 -0
- package/src/utils/text.ts +115 -0
- package/tests/test_api_workspace_management.ts +413 -0
- package/tests/test_bulk_delete.ts +267 -0
- package/tests/test_omnibus.ts +166 -0
- package/tests/test_workspace_management.ts +278 -0
- package/tests/verify.ts +104 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { all_async, run_async, get_async, q, memories_table } from "../core/db";
|
|
2
|
+
import { now } from "../utils";
|
|
3
|
+
import { cosineSimilarity } from "../memory/embed";
|
|
4
|
+
|
|
5
|
+
export const ALPHA_LEARNING_RATE_FOR_RECALL_REINFORCEMENT = 0.15;
|
|
6
|
+
export const BETA_LEARNING_RATE_FOR_EMOTIONAL_FREQUENCY = 0.2;
|
|
7
|
+
export const GAMMA_ATTENUATION_CONSTANT_FOR_GRAPH_DISTANCE = 0.35;
|
|
8
|
+
export const THETA_CONSOLIDATION_COEFFICIENT_FOR_LONG_TERM = 0.4;
|
|
9
|
+
export const ETA_REINFORCEMENT_FACTOR_FOR_TRACE_LEARNING = 0.18;
|
|
10
|
+
export const LAMBDA_ONE_FAST_DECAY_RATE = 0.015;
|
|
11
|
+
export const LAMBDA_TWO_SLOW_DECAY_RATE = 0.002;
|
|
12
|
+
export const TAU_ENERGY_THRESHOLD_FOR_RETRIEVAL = 0.4;
|
|
13
|
+
|
|
14
|
+
export const SECTORAL_INTERDEPENDENCE_MATRIX_FOR_COGNITIVE_RESONANCE = [
|
|
15
|
+
[1.0, 0.7, 0.3, 0.6, 0.6],
|
|
16
|
+
[0.7, 1.0, 0.4, 0.7, 0.8],
|
|
17
|
+
[0.3, 0.4, 1.0, 0.5, 0.2],
|
|
18
|
+
[0.6, 0.7, 0.5, 1.0, 0.8],
|
|
19
|
+
[0.6, 0.8, 0.2, 0.8, 1.0],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const SECTOR_INDEX_MAPPING_FOR_MATRIX_LOOKUP = {
|
|
23
|
+
episodic: 0,
|
|
24
|
+
semantic: 1,
|
|
25
|
+
procedural: 2,
|
|
26
|
+
emotional: 3,
|
|
27
|
+
reflective: 4,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface DynamicSalienceWeightingParameters {
|
|
31
|
+
initial_salience_value: number;
|
|
32
|
+
decay_constant_lambda: number;
|
|
33
|
+
recall_reinforcement_count: number;
|
|
34
|
+
emotional_frequency_metric: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AssociativeWaypointGraphNode {
|
|
38
|
+
node_memory_id: string;
|
|
39
|
+
activation_energy_level: number;
|
|
40
|
+
connected_waypoint_edges: Array<{
|
|
41
|
+
target_node_id: string;
|
|
42
|
+
link_weight_value: number;
|
|
43
|
+
time_gap_delta_t: number;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sig = (x: number) => 1 / (1 + Math.exp(-x));
|
|
48
|
+
export const linkW = (sem: number, emo: number, α = 0.7, β = 0.3) =>
|
|
49
|
+
sig(α * sem + β * emo);
|
|
50
|
+
|
|
51
|
+
export async function calculateDynamicSalienceWithTimeDecay(
|
|
52
|
+
i: number,
|
|
53
|
+
λ: number,
|
|
54
|
+
r: number,
|
|
55
|
+
e: number,
|
|
56
|
+
t: number,
|
|
57
|
+
): Promise<number> {
|
|
58
|
+
const d = i * Math.exp(-λ * t);
|
|
59
|
+
const rc = ALPHA_LEARNING_RATE_FOR_RECALL_REINFORCEMENT * r;
|
|
60
|
+
const ef = BETA_LEARNING_RATE_FOR_EMOTIONAL_FREQUENCY * e;
|
|
61
|
+
return Math.max(0, Math.min(1, d + rc + ef));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function calculateDualPhaseDecayMemoryRetention(
|
|
65
|
+
t: number,
|
|
66
|
+
): Promise<number> {
|
|
67
|
+
const f = Math.exp(-LAMBDA_ONE_FAST_DECAY_RATE * t);
|
|
68
|
+
const s =
|
|
69
|
+
THETA_CONSOLIDATION_COEFFICIENT_FOR_LONG_TERM *
|
|
70
|
+
Math.exp(-LAMBDA_TWO_SLOW_DECAY_RATE * t);
|
|
71
|
+
return Math.max(0, Math.min(1, f + s));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function calculateAssociativeWaypointLinkWeight(
|
|
75
|
+
sv: number[],
|
|
76
|
+
tv: number[],
|
|
77
|
+
tg: number,
|
|
78
|
+
): Promise<number> {
|
|
79
|
+
const sim = cosineSimilarity(sv, tv);
|
|
80
|
+
const td = tg / 86400000;
|
|
81
|
+
return Math.max(0, sim / (1 + td));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function calculateSpreadingActivationEnergyForNode(
|
|
85
|
+
nid: string,
|
|
86
|
+
an: Map<string, number>,
|
|
87
|
+
gr: Map<string, AssociativeWaypointGraphNode>,
|
|
88
|
+
): Promise<number> {
|
|
89
|
+
const nd = gr.get(nid);
|
|
90
|
+
if (!nd) return 0;
|
|
91
|
+
let tot = 0;
|
|
92
|
+
for (const e of nd.connected_waypoint_edges) {
|
|
93
|
+
const na = an.get(e.target_node_id) || 0;
|
|
94
|
+
const att = Math.exp(
|
|
95
|
+
-GAMMA_ATTENUATION_CONSTANT_FOR_GRAPH_DISTANCE * 1,
|
|
96
|
+
);
|
|
97
|
+
tot += e.link_weight_value * na * att;
|
|
98
|
+
}
|
|
99
|
+
return tot;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function applyRetrievalTraceReinforcementToMemory(
|
|
103
|
+
mid: string,
|
|
104
|
+
sal: number,
|
|
105
|
+
): Promise<number> {
|
|
106
|
+
return Math.min(
|
|
107
|
+
1,
|
|
108
|
+
sal + ETA_REINFORCEMENT_FACTOR_FOR_TRACE_LEARNING * (1 - sal),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function propagateAssociativeReinforcementToLinkedNodes(
|
|
113
|
+
sid: string,
|
|
114
|
+
ssal: number,
|
|
115
|
+
wps: Array<{ target_id: string; weight: number }>,
|
|
116
|
+
): Promise<Array<{ node_id: string; new_salience: number }>> {
|
|
117
|
+
const ups: Array<{ node_id: string; new_salience: number }> = [];
|
|
118
|
+
for (const wp of wps) {
|
|
119
|
+
const ld = (await get_async(
|
|
120
|
+
"select salience from memories where id=?",
|
|
121
|
+
[wp.target_id],
|
|
122
|
+
)) as any;
|
|
123
|
+
if (ld) {
|
|
124
|
+
const pr =
|
|
125
|
+
ETA_REINFORCEMENT_FACTOR_FOR_TRACE_LEARNING * wp.weight * ssal;
|
|
126
|
+
ups.push({
|
|
127
|
+
node_id: wp.target_id,
|
|
128
|
+
new_salience: Math.min(1, ld.salience + pr),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return ups;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function calculateCrossSectorResonanceScore(
|
|
136
|
+
ms: string,
|
|
137
|
+
qs: string,
|
|
138
|
+
bs: number,
|
|
139
|
+
): Promise<number> {
|
|
140
|
+
const si = (SECTOR_INDEX_MAPPING_FOR_MATRIX_LOOKUP as any)[ms] ?? 1;
|
|
141
|
+
const ti = (SECTOR_INDEX_MAPPING_FOR_MATRIX_LOOKUP as any)[qs] ?? 1;
|
|
142
|
+
return bs * SECTORAL_INTERDEPENDENCE_MATRIX_FOR_COGNITIVE_RESONANCE[si][ti];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function determineEnergyBasedRetrievalThreshold(
|
|
146
|
+
act: number,
|
|
147
|
+
tau: number,
|
|
148
|
+
): Promise<number> {
|
|
149
|
+
const nrm = Math.max(0.1, act);
|
|
150
|
+
return Math.max(0.1, Math.min(0.9, tau * (1 + Math.log(nrm + 1))));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function applyDualPhaseDecayToAllMemories(): Promise<void> {
|
|
154
|
+
const mems = await all_async(
|
|
155
|
+
"select id,salience,decay_lambda,last_seen_at,updated_at,created_at from memories",
|
|
156
|
+
);
|
|
157
|
+
const ts = now();
|
|
158
|
+
const ops = mems.map(async (m: any) => {
|
|
159
|
+
const tms = Math.max(0, ts - (m.last_seen_at || m.updated_at));
|
|
160
|
+
const td = tms / 86400000;
|
|
161
|
+
const rt = await calculateDualPhaseDecayMemoryRetention(td);
|
|
162
|
+
const nsal = m.salience * rt;
|
|
163
|
+
await run_async(
|
|
164
|
+
`update ${memories_table} set salience=?,updated_at=? where id=?`,
|
|
165
|
+
[Math.max(0, nsal), ts, m.id],
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
await Promise.all(ops);
|
|
169
|
+
console.log(`[DECAY] Applied to ${mems.length} memories`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function buildAssociativeWaypointGraphFromMemories(): Promise<
|
|
173
|
+
Map<string, AssociativeWaypointGraphNode>
|
|
174
|
+
> {
|
|
175
|
+
const gr = new Map<string, AssociativeWaypointGraphNode>();
|
|
176
|
+
const wps = (await all_async(
|
|
177
|
+
"select src_id,dst_id,weight,created_at from waypoints",
|
|
178
|
+
)) as any[];
|
|
179
|
+
const ids = new Set<string>();
|
|
180
|
+
for (const wp of wps) {
|
|
181
|
+
ids.add(wp.src_id);
|
|
182
|
+
ids.add(wp.dst_id);
|
|
183
|
+
}
|
|
184
|
+
for (const id of ids)
|
|
185
|
+
gr.set(id, {
|
|
186
|
+
node_memory_id: id,
|
|
187
|
+
activation_energy_level: 0,
|
|
188
|
+
connected_waypoint_edges: [],
|
|
189
|
+
});
|
|
190
|
+
for (const wp of wps) {
|
|
191
|
+
const sn = gr.get(wp.src_id);
|
|
192
|
+
if (sn) {
|
|
193
|
+
const tg = Math.abs(now() - wp.created_at);
|
|
194
|
+
sn.connected_waypoint_edges.push({
|
|
195
|
+
target_node_id: wp.dst_id,
|
|
196
|
+
link_weight_value: wp.weight,
|
|
197
|
+
time_gap_delta_t: tg,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return gr;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function performSpreadingActivationRetrieval(
|
|
205
|
+
init: string[],
|
|
206
|
+
max: number,
|
|
207
|
+
): Promise<Map<string, number>> {
|
|
208
|
+
const gr = await buildAssociativeWaypointGraphFromMemories();
|
|
209
|
+
const act = new Map<string, number>();
|
|
210
|
+
for (const id of init) act.set(id, 1.0);
|
|
211
|
+
for (let i = 0; i < max; i++) {
|
|
212
|
+
const ups = new Map<string, number>();
|
|
213
|
+
for (const [nid, ca] of act) {
|
|
214
|
+
const nd = gr.get(nid);
|
|
215
|
+
if (!nd) continue;
|
|
216
|
+
for (const e of nd.connected_waypoint_edges) {
|
|
217
|
+
const pe = await calculateSpreadingActivationEnergyForNode(
|
|
218
|
+
e.target_node_id,
|
|
219
|
+
act,
|
|
220
|
+
gr,
|
|
221
|
+
);
|
|
222
|
+
const ex = ups.get(e.target_node_id) || 0;
|
|
223
|
+
ups.set(e.target_node_id, ex + pe);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const [uid, nav] of ups) {
|
|
227
|
+
const cv = act.get(uid) || 0;
|
|
228
|
+
act.set(uid, Math.max(cv, nav));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return act;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function retrieveMemoriesWithEnergyThresholding(
|
|
235
|
+
qv: number[],
|
|
236
|
+
qs: string,
|
|
237
|
+
me: number,
|
|
238
|
+
): Promise<any[]> {
|
|
239
|
+
const mems = (await all_async(
|
|
240
|
+
"select id,content,primary_sector,salience,mean_vec from memories where salience>0.01",
|
|
241
|
+
)) as any[];
|
|
242
|
+
const sc = new Map<string, number>();
|
|
243
|
+
for (const m of mems) {
|
|
244
|
+
if (!m.mean_vec) continue;
|
|
245
|
+
const buf = Buffer.isBuffer(m.mean_vec)
|
|
246
|
+
? m.mean_vec
|
|
247
|
+
: Buffer.from(m.mean_vec);
|
|
248
|
+
const ev: number[] = [];
|
|
249
|
+
for (let i = 0; i < buf.length; i += 4) ev.push(buf.readFloatLE(i));
|
|
250
|
+
const bs = cosineSimilarity(qv, ev);
|
|
251
|
+
const cs = await calculateCrossSectorResonanceScore(
|
|
252
|
+
m.primary_sector,
|
|
253
|
+
qs,
|
|
254
|
+
bs,
|
|
255
|
+
);
|
|
256
|
+
sc.set(m.id, cs * m.salience);
|
|
257
|
+
}
|
|
258
|
+
const sp = await performSpreadingActivationRetrieval(
|
|
259
|
+
Array.from(sc.keys()).slice(0, 5),
|
|
260
|
+
3,
|
|
261
|
+
);
|
|
262
|
+
const cmb = new Map<string, number>();
|
|
263
|
+
for (const m of mems)
|
|
264
|
+
cmb.set(m.id, (sc.get(m.id) || 0) + (sp.get(m.id) || 0) * 0.3);
|
|
265
|
+
const te = Array.from(cmb.values()).reduce((s, v) => s + v, 0);
|
|
266
|
+
const thr = await determineEnergyBasedRetrievalThreshold(te, me);
|
|
267
|
+
return mems
|
|
268
|
+
.filter((m: any) => (cmb.get(m.id) || 0) > thr)
|
|
269
|
+
.map((m: any) => ({ ...m, activation_energy: cmb.get(m.id) }));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export const apply_decay = applyDualPhaseDecayToAllMemories;
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import mammoth from "mammoth";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import ffmpeg from "fluent-ffmpeg";
|
|
8
|
+
import OpenAI from "openai";
|
|
9
|
+
const TurndownService = require("turndown");
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
export interface ExtractionResult {
|
|
14
|
+
text: string;
|
|
15
|
+
metadata: {
|
|
16
|
+
content_type: string;
|
|
17
|
+
char_count: number;
|
|
18
|
+
estimated_tokens: number;
|
|
19
|
+
extraction_method: string;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function estimateTokens(text: string): number {
|
|
25
|
+
return Math.ceil(text.length / 4);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function extractPDF(buffer: Buffer): Promise<ExtractionResult> {
|
|
29
|
+
const { PDFParse } = await import("pdf-parse");
|
|
30
|
+
const parser = new PDFParse({ data: buffer });
|
|
31
|
+
const textResult = await parser.getText();
|
|
32
|
+
const infoResult = await parser.getInfo();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
text: textResult.text,
|
|
36
|
+
metadata: {
|
|
37
|
+
content_type: "pdf",
|
|
38
|
+
char_count: textResult.text.length,
|
|
39
|
+
estimated_tokens: estimateTokens(textResult.text),
|
|
40
|
+
extraction_method: "pdf-parse",
|
|
41
|
+
pages: textResult.total,
|
|
42
|
+
info: infoResult,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function extractDOCX(buffer: Buffer): Promise<ExtractionResult> {
|
|
48
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
text: result.value,
|
|
52
|
+
metadata: {
|
|
53
|
+
content_type: "docx",
|
|
54
|
+
char_count: result.value.length,
|
|
55
|
+
estimated_tokens: estimateTokens(result.value),
|
|
56
|
+
extraction_method: "mammoth",
|
|
57
|
+
messages: result.messages,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function extractHTML(html: string): Promise<ExtractionResult> {
|
|
63
|
+
const turndown = new TurndownService({
|
|
64
|
+
headingStyle: "atx",
|
|
65
|
+
codeBlockStyle: "fenced",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const markdown = turndown.turndown(html);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
text: markdown,
|
|
72
|
+
metadata: {
|
|
73
|
+
content_type: "html",
|
|
74
|
+
char_count: markdown.length,
|
|
75
|
+
estimated_tokens: estimateTokens(markdown),
|
|
76
|
+
extraction_method: "turndown",
|
|
77
|
+
original_html_length: html.length,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function extractURL(url: string): Promise<ExtractionResult> {
|
|
83
|
+
const response = await fetch(url);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const html = await response.text();
|
|
89
|
+
|
|
90
|
+
const turndown = new TurndownService({
|
|
91
|
+
headingStyle: "atx",
|
|
92
|
+
codeBlockStyle: "fenced",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const markdown = turndown.turndown(html);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
text: markdown,
|
|
99
|
+
metadata: {
|
|
100
|
+
content_type: "url",
|
|
101
|
+
char_count: markdown.length,
|
|
102
|
+
estimated_tokens: estimateTokens(markdown),
|
|
103
|
+
extraction_method: "node-fetch+turndown",
|
|
104
|
+
source_url: url,
|
|
105
|
+
fetched_at: new Date().toISOString(),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function extractAudio(
|
|
111
|
+
buffer: Buffer,
|
|
112
|
+
mimeType: string,
|
|
113
|
+
): Promise<ExtractionResult> {
|
|
114
|
+
const apiKey = process.env.OPENAI_API_KEY || process.env.OM_OPENAI_API_KEY;
|
|
115
|
+
if (!apiKey) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"OpenAI API key required for audio transcription. Set OPENAI_API_KEY in .env",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check file size (Whisper API limit is 25MB)
|
|
122
|
+
const maxSize = 25 * 1024 * 1024; // 25MB
|
|
123
|
+
if (buffer.length > maxSize) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Audio file too large: ${(buffer.length / 1024 / 1024).toFixed(2)}MB. Maximum size is 25MB.`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Create temporary file for Whisper API
|
|
130
|
+
const tempDir = os.tmpdir();
|
|
131
|
+
const ext = getAudioExtension(mimeType);
|
|
132
|
+
const tempFilePath = path.join(tempDir, `audio-${Date.now()}${ext}`);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Write buffer to temp file
|
|
136
|
+
fs.writeFileSync(tempFilePath, buffer);
|
|
137
|
+
|
|
138
|
+
// Initialize OpenAI client
|
|
139
|
+
const openai = new OpenAI({ apiKey });
|
|
140
|
+
|
|
141
|
+
// Transcribe audio using Whisper
|
|
142
|
+
const transcription = await openai.audio.transcriptions.create({
|
|
143
|
+
file: fs.createReadStream(tempFilePath),
|
|
144
|
+
model: "whisper-1",
|
|
145
|
+
response_format: "verbose_json",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const text = transcription.text;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
text,
|
|
152
|
+
metadata: {
|
|
153
|
+
content_type: "audio",
|
|
154
|
+
char_count: text.length,
|
|
155
|
+
estimated_tokens: estimateTokens(text),
|
|
156
|
+
extraction_method: "whisper",
|
|
157
|
+
audio_format: ext.replace(".", ""),
|
|
158
|
+
file_size_bytes: buffer.length,
|
|
159
|
+
file_size_mb: (buffer.length / 1024 / 1024).toFixed(2),
|
|
160
|
+
duration_seconds: (transcription as any).duration || null,
|
|
161
|
+
language: (transcription as any).language || null,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
} catch (error: any) {
|
|
165
|
+
console.error("[EXTRACT] Audio transcription failed:", error);
|
|
166
|
+
throw new Error(`Audio transcription failed: ${error.message}`);
|
|
167
|
+
} finally {
|
|
168
|
+
// Clean up temp file
|
|
169
|
+
try {
|
|
170
|
+
if (fs.existsSync(tempFilePath)) {
|
|
171
|
+
fs.unlinkSync(tempFilePath);
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.warn("[EXTRACT] Failed to clean up temp file:", e);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function extractVideo(
|
|
180
|
+
buffer: Buffer,
|
|
181
|
+
): Promise<ExtractionResult> {
|
|
182
|
+
// Create temporary files for video and audio
|
|
183
|
+
const tempDir = os.tmpdir();
|
|
184
|
+
const videoPath = path.join(
|
|
185
|
+
tempDir,
|
|
186
|
+
`video-${Date.now()}.mp4`,
|
|
187
|
+
);
|
|
188
|
+
const audioPath = path.join(
|
|
189
|
+
tempDir,
|
|
190
|
+
`audio-${Date.now()}.mp3`,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Write video buffer to temp file
|
|
195
|
+
fs.writeFileSync(videoPath, buffer);
|
|
196
|
+
|
|
197
|
+
// Extract audio using ffmpeg
|
|
198
|
+
await new Promise<void>((resolve, reject) => {
|
|
199
|
+
ffmpeg(videoPath)
|
|
200
|
+
.output(audioPath)
|
|
201
|
+
.noVideo()
|
|
202
|
+
.audioCodec("libmp3lame")
|
|
203
|
+
.on("end", () => resolve())
|
|
204
|
+
.on("error", (err: Error) => reject(err))
|
|
205
|
+
.run();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Read extracted audio
|
|
209
|
+
const audioBuffer = fs.readFileSync(audioPath);
|
|
210
|
+
|
|
211
|
+
// Transcribe extracted audio
|
|
212
|
+
const result = await extractAudio(audioBuffer, "audio/mpeg");
|
|
213
|
+
|
|
214
|
+
// Update metadata to reflect video source
|
|
215
|
+
result.metadata.content_type = "video";
|
|
216
|
+
result.metadata.extraction_method = "ffmpeg+whisper";
|
|
217
|
+
result.metadata.video_file_size_bytes = buffer.length;
|
|
218
|
+
result.metadata.video_file_size_mb = (
|
|
219
|
+
buffer.length /
|
|
220
|
+
1024 /
|
|
221
|
+
1024
|
|
222
|
+
).toFixed(2);
|
|
223
|
+
|
|
224
|
+
return result;
|
|
225
|
+
} catch (error: any) {
|
|
226
|
+
if (error.message?.includes("ffmpeg")) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
"FFmpeg not found. Please install FFmpeg to process video files. Visit: https://ffmpeg.org/download.html",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
console.error("[EXTRACT] Video processing failed:", error);
|
|
232
|
+
throw new Error(`Video processing failed: ${error.message}`);
|
|
233
|
+
} finally {
|
|
234
|
+
// Clean up temp files
|
|
235
|
+
try {
|
|
236
|
+
if (fs.existsSync(videoPath)) fs.unlinkSync(videoPath);
|
|
237
|
+
if (fs.existsSync(audioPath)) fs.unlinkSync(audioPath);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.warn("[EXTRACT] Failed to clean up temp files:", e);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getAudioExtension(mimeType: string): string {
|
|
245
|
+
const mimeMap: Record<string, string> = {
|
|
246
|
+
"audio/mpeg": ".mp3",
|
|
247
|
+
"audio/mp3": ".mp3",
|
|
248
|
+
"audio/wav": ".wav",
|
|
249
|
+
"audio/wave": ".wav",
|
|
250
|
+
"audio/x-wav": ".wav",
|
|
251
|
+
"audio/mp4": ".m4a",
|
|
252
|
+
"audio/m4a": ".m4a",
|
|
253
|
+
"audio/x-m4a": ".m4a",
|
|
254
|
+
"audio/webm": ".webm",
|
|
255
|
+
"audio/ogg": ".ogg",
|
|
256
|
+
};
|
|
257
|
+
return mimeMap[mimeType.toLowerCase()] || ".mp3";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function extractText(
|
|
261
|
+
contentType: string,
|
|
262
|
+
data: string | Buffer,
|
|
263
|
+
): Promise<ExtractionResult> {
|
|
264
|
+
const type = contentType.toLowerCase();
|
|
265
|
+
|
|
266
|
+
// Audio formats
|
|
267
|
+
if (
|
|
268
|
+
type === "mp3" ||
|
|
269
|
+
type === "audio" ||
|
|
270
|
+
type === "audio/mpeg" ||
|
|
271
|
+
type === "audio/mp3" ||
|
|
272
|
+
type === "audio/wav" ||
|
|
273
|
+
type === "audio/wave" ||
|
|
274
|
+
type === "audio/x-wav" ||
|
|
275
|
+
type === "wav" ||
|
|
276
|
+
type === "m4a" ||
|
|
277
|
+
type === "audio/mp4" ||
|
|
278
|
+
type === "audio/m4a" ||
|
|
279
|
+
type === "audio/x-m4a" ||
|
|
280
|
+
type === "webm" ||
|
|
281
|
+
type === "audio/webm" ||
|
|
282
|
+
type === "ogg" ||
|
|
283
|
+
type === "audio/ogg"
|
|
284
|
+
) {
|
|
285
|
+
const buffer = Buffer.isBuffer(data)
|
|
286
|
+
? data
|
|
287
|
+
: Buffer.from(data as string, "base64");
|
|
288
|
+
return extractAudio(buffer, type.startsWith("audio/") ? type : `audio/${type}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Video formats
|
|
292
|
+
if (
|
|
293
|
+
type === "mp4" ||
|
|
294
|
+
type === "video" ||
|
|
295
|
+
type === "video/mp4" ||
|
|
296
|
+
type === "video/webm" ||
|
|
297
|
+
type === "video/mpeg" ||
|
|
298
|
+
type === "avi" ||
|
|
299
|
+
type === "video/avi" ||
|
|
300
|
+
type === "mov" ||
|
|
301
|
+
type === "video/quicktime"
|
|
302
|
+
) {
|
|
303
|
+
const buffer = Buffer.isBuffer(data)
|
|
304
|
+
? data
|
|
305
|
+
: Buffer.from(data as string, "base64");
|
|
306
|
+
return extractVideo(buffer);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
switch (type) {
|
|
310
|
+
case "pdf":
|
|
311
|
+
return extractPDF(
|
|
312
|
+
Buffer.isBuffer(data)
|
|
313
|
+
? data
|
|
314
|
+
: Buffer.from(data as string, "base64"),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
case "docx":
|
|
318
|
+
case "doc":
|
|
319
|
+
return extractDOCX(
|
|
320
|
+
Buffer.isBuffer(data)
|
|
321
|
+
? data
|
|
322
|
+
: Buffer.from(data as string, "base64"),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
case "html":
|
|
326
|
+
case "htm":
|
|
327
|
+
return extractHTML(data.toString());
|
|
328
|
+
|
|
329
|
+
case "md":
|
|
330
|
+
case "markdown": {
|
|
331
|
+
const text = data.toString();
|
|
332
|
+
return {
|
|
333
|
+
text,
|
|
334
|
+
metadata: {
|
|
335
|
+
content_type: "markdown",
|
|
336
|
+
char_count: text.length,
|
|
337
|
+
estimated_tokens: estimateTokens(text),
|
|
338
|
+
extraction_method: "passthrough",
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case "txt":
|
|
344
|
+
case "text": {
|
|
345
|
+
const text = data.toString();
|
|
346
|
+
return {
|
|
347
|
+
text,
|
|
348
|
+
metadata: {
|
|
349
|
+
content_type: "txt",
|
|
350
|
+
char_count: text.length,
|
|
351
|
+
estimated_tokens: estimateTokens(text),
|
|
352
|
+
extraction_method: "passthrough",
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
default:
|
|
358
|
+
throw new Error(`Unsupported content type: ${contentType}`);
|
|
359
|
+
}
|
|
360
|
+
}
|