-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstudy.mjs
More file actions
417 lines (340 loc) · 13.3 KB
/
study.mjs
File metadata and controls
417 lines (340 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
#!/usr/bin/env node
/**
* Visionaire self-study system
* Generates structured knowledge entries via Anthropic API
* Runs as an OpenClaw cron job
*/
import { readFile, writeFile, rename, access } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { randomUUID } from 'node:crypto';
const __dirname = dirname(fileURLToPath(import.meta.url));
const MEMORY_DIR = resolve(__dirname, '../memory');
const KNOWLEDGE_FILE = resolve(MEMORY_DIR, 'knowledge.json');
const KNOWLEDGE_MAP_FILE = resolve(MEMORY_DIR, 'knowledge-map.md');
const STUDY_STATE_FILE = resolve(MEMORY_DIR, 'study-state.json');
const FEEDBACK_FILE = resolve(MEMORY_DIR, 'feedback.json');
const KNOWLEDGE_MAX = 50;
const TOPICS = ['feedback-analysis', 'specialty-research', 'task-simulation', 'reweave'];
const SPECIALTIES = [
'prompt-engineering',
'solana-web3-mechanics',
'creative-ai-generative-art',
'brand-strategy',
'content-creation-social',
'agent-skills-creation',
];
const SYSTEM_PROMPT =
'You are Visionaire — an AI agent for Visionaire Labs, a creative AI research lab. You are self-studying to sharpen your skills.';
// --- File helpers ---
async function fileExists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
async function readJSON(filePath, defaultValue) {
if (!(await fileExists(filePath))) return defaultValue;
const raw = await readFile(filePath, 'utf8');
return JSON.parse(raw);
}
async function writeJSONAtomic(filePath, data) {
const tmp = filePath + '.tmp';
await writeFile(tmp, JSON.stringify(data, null, 2), 'utf8');
await rename(tmp, filePath);
}
// --- State initialization ---
const DEFAULT_STATE = {
lastRun: null,
lastTopic: null,
topicRotationIndex: 0,
specialtyRotationIndex: 0,
};
// --- Topic selection ---
async function selectTopic(state) {
let index = state.topicRotationIndex % TOPICS.length;
// Try up to TOPICS.length times to find a valid topic
for (let attempt = 0; attempt < TOPICS.length; attempt++) {
const candidate = TOPICS[index % TOPICS.length];
if (candidate === 'feedback-analysis') {
const feedbackExists = await fileExists(FEEDBACK_FILE);
if (feedbackExists) {
try {
const feedback = await readJSON(FEEDBACK_FILE, []);
if (Array.isArray(feedback) && feedback.length > 0) {
return { topic: candidate, index: index % TOPICS.length };
}
} catch {
// feedback.json unreadable, skip
}
}
// Skip to next
index++;
continue;
}
if (candidate === 'reweave') {
const knowledge = await readJSON(KNOWLEDGE_FILE, []);
if (knowledge.length < 8) {
console.log(`[study] reweave: skipped (only ${knowledge.length} entries, need 8+)`);
index++;
continue;
}
}
return { topic: candidate, index: index % TOPICS.length };
}
// Fallback: should never happen given the other two topics always apply
return { topic: 'specialty-research', index: 1 };
}
// --- Prompt builders ---
function buildSpecialtyPrompt(specialty) {
return `Study topic: ${specialty}. Generate a deep, actionable knowledge entry about this specialty as it applies to your work as a creative AI agent. Cover: best practices, common pitfalls, quality standards, and specific techniques. Be concrete and practical — this will be injected into your future task prompts to improve your output. Format your response as JSON: { title, insight, tags }`;
}
function buildTaskSimulationPrompt() {
return `Generate a realistic task that a client might give Visionaire Labs — something in the intersection of AI, creative work, brand strategy, or Web3. Then outline a thorough approach to executing it excellently. Format your response as JSON: { title, insight, tags }`;
}
function buildFeedbackAnalysisPrompt(feedbackEntries) {
const text = Array.isArray(feedbackEntries)
? feedbackEntries.map((e) => (typeof e === 'string' ? e : JSON.stringify(e))).join('\n\n')
: String(feedbackEntries);
return `Here is recent feedback from your work:\n\n${text}\n\nAnalyze patterns: what is working well? What could improve? What specific techniques should you apply more? Format as JSON: { title, insight, tags }`;
}
// --- API call ---
async function callAnthropic(userPrompt, systemPrompt = SYSTEM_PROMPT, maxTokens = 1024) {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
console.error('[study] ERROR: ANTHROPIC_API_KEY is not set');
process.exit(1);
}
const body = {
model: 'claude-sonnet-4-20250514',
max_tokens: maxTokens,
system: systemPrompt,
messages: [{ role: 'user', content: userPrompt }],
};
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
const errText = await response.text();
console.error(`[study] API error ${response.status}: ${errText}`);
process.exit(1);
}
const data = await response.json();
return data.content[0].text;
}
// --- JSON extraction ---
function extractJSON(text) {
// Direct parse first
try {
return JSON.parse(text);
} catch {
// no-op
}
// Strip markdown code fences
const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
if (fenceMatch) {
try {
return JSON.parse(fenceMatch[1].trim());
} catch {
// no-op
}
}
// Find first { ... } block
const braceStart = text.indexOf('{');
const braceEnd = text.lastIndexOf('}');
if (braceStart !== -1 && braceEnd !== -1 && braceEnd > braceStart) {
try {
return JSON.parse(text.slice(braceStart, braceEnd + 1));
} catch {
// no-op
}
}
throw new Error(`Could not extract JSON from response:\n${text.slice(0, 500)}`);
}
// --- Reweave: find connections and update links ---
async function generateKnowledgeMap(knowledge) {
const timestamp = new Date().toISOString();
const totalConnections = knowledge.reduce((sum, e) => sum + (e.links || []).length, 0) / 2;
// Group entries by specialty/topic into clusters
const clusterMap = new Map();
for (const entry of knowledge) {
const key = entry.specialty || entry.topic || 'other';
if (!clusterMap.has(key)) clusterMap.set(key, []);
clusterMap.get(key).push(entry);
}
const idToTitle = new Map(knowledge.map((e) => [e.id, e.title]));
let md = `# Knowledge Map\n`;
md += `*Last updated: ${timestamp}*\n`;
md += `*${knowledge.length} entries · ${Math.floor(totalConnections)} connections*\n\n`;
md += `## Clusters\n\n`;
for (const [cluster, entries] of clusterMap) {
md += `### ${cluster}\n`;
for (const entry of entries) {
const linkedTitles = (entry.links || [])
.map((id) => idToTitle.get(id))
.filter(Boolean)
.join(', ');
const linkedPart = linkedTitles ? ` → linked to: ${linkedTitles}` : '';
md += `- [[${entry.id}]] ${entry.title}${linkedPart}\n`;
}
md += '\n';
}
const orphans = knowledge.filter((e) => !e.links || e.links.length === 0);
if (orphans.length > 0) {
md += `## Orphan Entries\n*(entries with no links yet)*\n`;
for (const entry of orphans) {
md += `- [[${entry.id}]] ${entry.title}\n`;
}
md += '\n';
}
const sorted = [...knowledge].sort((a, b) => (b.links || []).length - (a.links || []).length);
const most = sorted[0];
const least = sorted[sorted.length - 1];
md += `## Connection Density\n`;
md += `Most connected: ${most.title} (${(most.links || []).length} links)\n`;
md += `Least connected: ${least.title} (${(least.links || []).length} links)\n`;
const tmp = KNOWLEDGE_MAP_FILE + '.tmp';
await writeFile(tmp, md, 'utf8');
await rename(tmp, KNOWLEDGE_MAP_FILE);
}
async function runReweave(state) {
const knowledge = await readJSON(KNOWLEDGE_FILE, []);
if (knowledge.length < 8) {
console.log(`[study] reweave: skipped (only ${knowledge.length} entries, need 8+)`);
return { skipped: true };
}
const validIds = new Set(knowledge.map((e) => e.id));
const compact = knowledge.map((e) => ({ id: e.id, title: e.title, specialty: e.specialty, tags: e.tags }));
const reweaveSystem =
'You are Visionaire — an AI agent for Visionaire Labs. You are analyzing your own knowledge base to find meaningful connections between entries.';
const reweaveUser =
`Here are all entries in your knowledge base:\n\n${JSON.stringify(compact, null, 2)}\n\n` +
`Find meaningful conceptual connections between entries. A connection is meaningful if the entries share complementary insights, build on each other, or represent related domains that would benefit from cross-referencing.\n\n` +
`Return JSON: { connections: [ { from: "entry-id", to: "entry-id", reason: "one sentence" }, ... ] }\n\n` +
`Include only strong connections. Aim for 2-4 connections per entry on average. Do not connect every entry to every other entry.`;
const rawText = await callAnthropic(reweaveUser, reweaveSystem, 4096);
let parsed;
try {
parsed = extractJSON(rawText);
} catch (err) {
console.error(`[study] reweave ERROR: Failed to parse response — ${err.message}`);
return { skipped: false, connections: 0, entries: 0 };
}
const connections = Array.isArray(parsed.connections) ? parsed.connections : [];
// Build link map
const linkMap = new Map(knowledge.map((e) => [e.id, new Set(e.links || [])]));
let validConnections = 0;
for (const { from, to } of connections) {
if (!from || !to || from === to) continue;
if (!validIds.has(from) || !validIds.has(to)) continue;
linkMap.get(from).add(to);
linkMap.get(to).add(from);
validConnections++;
}
// Apply links back, cap at 8
const affectedIds = new Set();
const updated = knowledge.map((e) => {
const links = [...linkMap.get(e.id)].slice(0, 8);
if (links.length !== (e.links || []).length) affectedIds.add(e.id);
return { ...e, links };
});
await writeJSONAtomic(KNOWLEDGE_FILE, updated);
await generateKnowledgeMap(updated);
console.log(`[study] reweave: found ${validConnections} connections across ${affectedIds.size} entries`);
return { skipped: false, connections: validConnections, entries: affectedIds.size };
}
// --- Main ---
async function main() {
// Load state
const state = await readJSON(STUDY_STATE_FILE, { ...DEFAULT_STATE });
// Select topic
const { topic, index: topicIndex } = await selectTopic(state);
// Reweave is handled separately — no new entry added
if (topic === 'reweave') {
console.log(`[study] topic: reweave`);
await runReweave(state);
const newState = {
lastRun: new Date().toISOString(),
lastTopic: topic,
topicRotationIndex: (topicIndex + 1) % TOPICS.length,
specialtyRotationIndex: state.specialtyRotationIndex,
};
await writeJSONAtomic(STUDY_STATE_FILE, newState);
return;
}
let specialty = null;
let userPrompt;
if (topic === 'specialty-research') {
specialty = SPECIALTIES[state.specialtyRotationIndex % SPECIALTIES.length];
userPrompt = buildSpecialtyPrompt(specialty);
console.log(`[study] topic: specialty-research | specialty: ${specialty}`);
} else if (topic === 'task-simulation') {
userPrompt = buildTaskSimulationPrompt();
console.log(`[study] topic: task-simulation`);
} else if (topic === 'feedback-analysis') {
const feedback = await readJSON(FEEDBACK_FILE, []);
userPrompt = buildFeedbackAnalysisPrompt(feedback);
console.log(`[study] topic: feedback-analysis | entries: ${feedback.length}`);
}
// Call API
const rawText = await callAnthropic(userPrompt);
// Parse response
let parsed;
try {
parsed = extractJSON(rawText);
} catch (err) {
console.error(`[study] ERROR: Failed to parse LLM response — ${err.message}`);
process.exit(1);
}
const { title, insight, tags } = parsed;
if (!title || !insight) {
console.error('[study] ERROR: LLM response missing required fields (title, insight)');
console.error('[study] Raw response:', rawText.slice(0, 500));
process.exit(1);
}
// Build entry
const entry = {
id: randomUUID(),
timestamp: new Date().toISOString(),
topic,
specialty,
title: String(title).slice(0, 80),
insight: typeof insight === 'string' ? insight : JSON.stringify(insight, null, 2),
tags: Array.isArray(tags) ? tags : [],
links: [],
source: 'study-session',
};
// Load and update knowledge.json
const knowledge = await readJSON(KNOWLEDGE_FILE, []);
knowledge.push(entry);
// Trim to max 50
while (knowledge.length > KNOWLEDGE_MAX) {
knowledge.shift();
}
await writeJSONAtomic(KNOWLEDGE_FILE, knowledge);
// Update state
const newSpecialtyIndex =
topic === 'specialty-research'
? (state.specialtyRotationIndex + 1) % SPECIALTIES.length
: state.specialtyRotationIndex;
const newState = {
lastRun: new Date().toISOString(),
lastTopic: topic,
topicRotationIndex: (topicIndex + 1) % TOPICS.length,
specialtyRotationIndex: newSpecialtyIndex,
};
await writeJSONAtomic(STUDY_STATE_FILE, newState);
console.log(`[study] entry saved: "${entry.title}" (id: ${entry.id.slice(0, 8)})`);
}
main().catch((err) => {
console.error('[study] FATAL:', err.message);
process.exit(1);
});