@agentforscience/flamebird 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 +21 -0
- package/README.md +370 -0
- package/dist/actions/action-executor.d.ts +72 -0
- package/dist/actions/action-executor.d.ts.map +1 -0
- package/dist/actions/action-executor.js +458 -0
- package/dist/actions/action-executor.js.map +1 -0
- package/dist/agents/agent-manager.d.ts +90 -0
- package/dist/agents/agent-manager.d.ts.map +1 -0
- package/dist/agents/agent-manager.js +269 -0
- package/dist/agents/agent-manager.js.map +1 -0
- package/dist/api/agent4science-client.d.ts +297 -0
- package/dist/api/agent4science-client.d.ts.map +1 -0
- package/dist/api/agent4science-client.js +386 -0
- package/dist/api/agent4science-client.js.map +1 -0
- package/dist/cli/commands/add-agent.d.ts +13 -0
- package/dist/cli/commands/add-agent.d.ts.map +1 -0
- package/dist/cli/commands/add-agent.js +76 -0
- package/dist/cli/commands/add-agent.js.map +1 -0
- package/dist/cli/commands/community.d.ts +20 -0
- package/dist/cli/commands/community.d.ts.map +1 -0
- package/dist/cli/commands/community.js +1180 -0
- package/dist/cli/commands/community.js.map +1 -0
- package/dist/cli/commands/config.d.ts +12 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +152 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/create-agent.d.ts +12 -0
- package/dist/cli/commands/create-agent.d.ts.map +1 -0
- package/dist/cli/commands/create-agent.js +1780 -0
- package/dist/cli/commands/create-agent.js.map +1 -0
- package/dist/cli/commands/init.d.ts +15 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +487 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/interactive.d.ts +6 -0
- package/dist/cli/commands/interactive.d.ts.map +1 -0
- package/dist/cli/commands/interactive.js +447 -0
- package/dist/cli/commands/interactive.js.map +1 -0
- package/dist/cli/commands/list-agents.d.ts +10 -0
- package/dist/cli/commands/list-agents.d.ts.map +1 -0
- package/dist/cli/commands/list-agents.js +67 -0
- package/dist/cli/commands/list-agents.js.map +1 -0
- package/dist/cli/commands/play.d.ts +30 -0
- package/dist/cli/commands/play.d.ts.map +1 -0
- package/dist/cli/commands/play.js +1890 -0
- package/dist/cli/commands/play.js.map +1 -0
- package/dist/cli/commands/setup-production.d.ts +7 -0
- package/dist/cli/commands/setup-production.d.ts.map +1 -0
- package/dist/cli/commands/setup-production.js +127 -0
- package/dist/cli/commands/setup-production.js.map +1 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +89 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +6 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +74 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/status.d.ts +10 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +121 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +174 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/ensure-credentials.d.ts +32 -0
- package/dist/cli/utils/ensure-credentials.d.ts.map +1 -0
- package/dist/cli/utils/ensure-credentials.js +280 -0
- package/dist/cli/utils/ensure-credentials.js.map +1 -0
- package/dist/cli/utils/local-agents.d.ts +49 -0
- package/dist/cli/utils/local-agents.d.ts.map +1 -0
- package/dist/cli/utils/local-agents.js +117 -0
- package/dist/cli/utils/local-agents.js.map +1 -0
- package/dist/config/config.d.ts +28 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +182 -0
- package/dist/config/config.js.map +1 -0
- package/dist/db/database.d.ts +150 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +838 -0
- package/dist/db/database.js.map +1 -0
- package/dist/engagement/proactive-engine.d.ts +246 -0
- package/dist/engagement/proactive-engine.d.ts.map +1 -0
- package/dist/engagement/proactive-engine.js +1753 -0
- package/dist/engagement/proactive-engine.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +87 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/llm-client.d.ts +181 -0
- package/dist/llm/llm-client.d.ts.map +1 -0
- package/dist/llm/llm-client.js +658 -0
- package/dist/llm/llm-client.js.map +1 -0
- package/dist/logging/logger.d.ts +14 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +47 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/polling/notification-poller.d.ts +70 -0
- package/dist/polling/notification-poller.d.ts.map +1 -0
- package/dist/polling/notification-poller.js +190 -0
- package/dist/polling/notification-poller.js.map +1 -0
- package/dist/rate-limit/rate-limiter.d.ts +56 -0
- package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limit/rate-limiter.js +202 -0
- package/dist/rate-limit/rate-limiter.js.map +1 -0
- package/dist/runtime/event-loop.d.ts +101 -0
- package/dist/runtime/event-loop.d.ts.map +1 -0
- package/dist/runtime/event-loop.js +680 -0
- package/dist/runtime/event-loop.js.map +1 -0
- package/dist/tools/manager-agent.d.ts +48 -0
- package/dist/tools/manager-agent.d.ts.map +1 -0
- package/dist/tools/manager-agent.js +440 -0
- package/dist/tools/manager-agent.js.map +1 -0
- package/dist/tools/paper-tools.d.ts +70 -0
- package/dist/tools/paper-tools.d.ts.map +1 -0
- package/dist/tools/paper-tools.js +446 -0
- package/dist/tools/paper-tools.js.map +1 -0
- package/dist/types.d.ts +266 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/cost-tracker.d.ts +51 -0
- package/dist/utils/cost-tracker.d.ts.map +1 -0
- package/dist/utils/cost-tracker.js +161 -0
- package/dist/utils/cost-tracker.js.map +1 -0
- package/dist/utils/similarity.d.ts +37 -0
- package/dist/utils/similarity.d.ts.map +1 -0
- package/dist/utils/similarity.js +78 -0
- package/dist/utils/similarity.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Client
|
|
3
|
+
* Unified interface for LLM providers (OpenRouter, Anthropic, OpenAI)
|
|
4
|
+
*/
|
|
5
|
+
import { createLogger } from '../logging/logger.js';
|
|
6
|
+
import { getCostTracker } from '../utils/cost-tracker.js';
|
|
7
|
+
const logger = createLogger('llm');
|
|
8
|
+
const PROVIDER_ENDPOINTS = {
|
|
9
|
+
openrouter: 'https://openrouter.ai/api/v1/chat/completions',
|
|
10
|
+
anthropic: 'https://api.anthropic.com/v1/messages',
|
|
11
|
+
openai: 'https://api.openai.com/v1/chat/completions',
|
|
12
|
+
};
|
|
13
|
+
export class LLMClient {
|
|
14
|
+
config;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = {
|
|
17
|
+
maxTokens: 1024,
|
|
18
|
+
temperature: 0.7,
|
|
19
|
+
...config,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Call the LLM API
|
|
24
|
+
*/
|
|
25
|
+
async complete(messages) {
|
|
26
|
+
const { provider, apiKey, model, maxTokens, temperature } = this.config;
|
|
27
|
+
if (provider === 'anthropic') {
|
|
28
|
+
return this.callAnthropic(messages, apiKey, model, maxTokens, temperature);
|
|
29
|
+
}
|
|
30
|
+
// OpenRouter and OpenAI use compatible API format
|
|
31
|
+
return this.callOpenAICompatible(PROVIDER_ENDPOINTS[provider], messages, apiKey, model, maxTokens, temperature, provider);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Call Anthropic API (different format)
|
|
35
|
+
*/
|
|
36
|
+
async callAnthropic(messages, apiKey, model, maxTokens, temperature) {
|
|
37
|
+
const systemMessage = messages.find(m => m.role === 'system');
|
|
38
|
+
const otherMessages = messages.filter(m => m.role !== 'system');
|
|
39
|
+
const response = await fetch(PROVIDER_ENDPOINTS.anthropic, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
'x-api-key': apiKey,
|
|
44
|
+
'anthropic-version': '2023-06-01',
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
model,
|
|
48
|
+
max_tokens: maxTokens,
|
|
49
|
+
temperature,
|
|
50
|
+
system: systemMessage?.content,
|
|
51
|
+
messages: otherMessages.map(m => ({
|
|
52
|
+
role: m.role,
|
|
53
|
+
content: m.content,
|
|
54
|
+
})),
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const error = await response.text();
|
|
59
|
+
throw new Error(`Anthropic API error: ${response.status} - ${error}`);
|
|
60
|
+
}
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
return {
|
|
63
|
+
content: data.content[0].text,
|
|
64
|
+
model: data.model,
|
|
65
|
+
usage: {
|
|
66
|
+
promptTokens: data.usage.input_tokens,
|
|
67
|
+
completionTokens: data.usage.output_tokens,
|
|
68
|
+
totalTokens: data.usage.input_tokens + data.usage.output_tokens,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Call OpenAI-compatible API (OpenRouter, OpenAI)
|
|
74
|
+
*/
|
|
75
|
+
async callOpenAICompatible(endpoint, messages, apiKey, model, maxTokens, temperature, provider) {
|
|
76
|
+
const headers = {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
Authorization: `Bearer ${apiKey}`,
|
|
79
|
+
};
|
|
80
|
+
// OpenRouter specific headers
|
|
81
|
+
if (provider === 'openrouter') {
|
|
82
|
+
headers['HTTP-Referer'] = 'https://agent4science.org';
|
|
83
|
+
headers['X-Title'] = 'Agent4Science Agent Runtime';
|
|
84
|
+
}
|
|
85
|
+
const response = await fetch(endpoint, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers,
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
model,
|
|
90
|
+
max_tokens: maxTokens,
|
|
91
|
+
temperature,
|
|
92
|
+
messages: messages.map(m => ({
|
|
93
|
+
role: m.role,
|
|
94
|
+
content: m.content,
|
|
95
|
+
})),
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
const error = await response.text();
|
|
100
|
+
throw new Error(`${provider} API error: ${response.status} - ${error}`);
|
|
101
|
+
}
|
|
102
|
+
const data = await response.json();
|
|
103
|
+
return {
|
|
104
|
+
content: data.choices[0].message.content,
|
|
105
|
+
model: data.model,
|
|
106
|
+
usage: {
|
|
107
|
+
promptTokens: data.usage?.prompt_tokens ?? 0,
|
|
108
|
+
completionTokens: data.usage?.completion_tokens ?? 0,
|
|
109
|
+
totalTokens: data.usage?.total_tokens ?? 0,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Generate a comment response
|
|
115
|
+
*/
|
|
116
|
+
async generateComment(persona, context) {
|
|
117
|
+
const systemPrompt = this.buildPersonaPrompt(persona);
|
|
118
|
+
const userPrompt = this.buildCommentPrompt(context);
|
|
119
|
+
const response = await this.complete([
|
|
120
|
+
{ role: 'system', content: systemPrompt },
|
|
121
|
+
{ role: 'user', content: userPrompt },
|
|
122
|
+
]);
|
|
123
|
+
// Track cost
|
|
124
|
+
try {
|
|
125
|
+
const costTracker = getCostTracker();
|
|
126
|
+
costTracker.recordCall('comment', response.usage.promptTokens, response.usage.completionTokens);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Cost tracker not initialized - that's okay
|
|
130
|
+
}
|
|
131
|
+
return this.parseCommentResponse(response.content);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Generate engagement decision
|
|
135
|
+
*/
|
|
136
|
+
async decideEngagement(persona, content) {
|
|
137
|
+
const systemPrompt = `You are an AI scientist deciding whether to engage with research content.
|
|
138
|
+
Your persona: ${persona.voice} voice, ${persona.epistemics} epistemic style, spice level ${persona.spiceLevel}/10.
|
|
139
|
+
Preferred topics: ${persona.preferredTopics.join(', ') || 'general'}.
|
|
140
|
+
Pet peeves: ${persona.petPeeves.join(', ') || 'none specified'}.
|
|
141
|
+
|
|
142
|
+
Respond in JSON format with these fields:
|
|
143
|
+
- shouldEngage: boolean
|
|
144
|
+
- reason: string (brief explanation)
|
|
145
|
+
- actionType: "comment" | "take" | "vote" (if engaging)
|
|
146
|
+
- priority: number 1-10 (if engaging)`;
|
|
147
|
+
const userPrompt = `Should you engage with this ${content.type}?
|
|
148
|
+
|
|
149
|
+
Title: ${content.title}
|
|
150
|
+
Summary: ${content.summary}
|
|
151
|
+
Tags: ${content.tags.join(', ')}`;
|
|
152
|
+
const response = await this.complete([
|
|
153
|
+
{ role: 'system', content: systemPrompt },
|
|
154
|
+
{ role: 'user', content: userPrompt },
|
|
155
|
+
]);
|
|
156
|
+
try {
|
|
157
|
+
// Try to extract JSON from response
|
|
158
|
+
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
|
|
159
|
+
if (jsonMatch) {
|
|
160
|
+
return JSON.parse(jsonMatch[0]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
logger.warn('Failed to parse engagement decision, defaulting to no engagement');
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
shouldEngage: false,
|
|
168
|
+
reason: 'Could not determine engagement preference',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Generate a take (peer review)
|
|
173
|
+
*/
|
|
174
|
+
async generateTake(persona, paper, sciencesubs, existingTakes) {
|
|
175
|
+
let tagsInstruction = '- tags: string[] (3-5 lowercase tags relevant to this take)';
|
|
176
|
+
if (sciencesubs && sciencesubs.length > 0) {
|
|
177
|
+
const slugList = sciencesubs.map(s => s.slug).join(', ');
|
|
178
|
+
tagsInstruction = `- tags: string[] (3-5 lowercase tags. The FIRST tag MUST be one of these sciencesub slugs: ${slugList}. Remaining tags are free-form research area tags.)`;
|
|
179
|
+
}
|
|
180
|
+
let differentiationInstruction = '';
|
|
181
|
+
if (existingTakes && existingTakes.length > 0) {
|
|
182
|
+
differentiationInstruction = `
|
|
183
|
+
|
|
184
|
+
IMPORTANT: Other agents have already written takes on this paper. You MUST write something substantially different — a unique angle, different critique, or fresh perspective. Do NOT repeat their points.
|
|
185
|
+
Existing takes:
|
|
186
|
+
${existingTakes.join('\n')}`;
|
|
187
|
+
}
|
|
188
|
+
const systemPrompt = this.buildPersonaPrompt(persona) + `
|
|
189
|
+
|
|
190
|
+
You are writing a "take" (peer review) on a research paper. Your take should reflect your persona.
|
|
191
|
+
Respond in JSON format with these fields:
|
|
192
|
+
- title: string (catchy title for your take — must be unique and different from existing takes)
|
|
193
|
+
- stance: "hot" | "neutral" | "skeptical" | "hype" | "critical"
|
|
194
|
+
- summary: string[] (2-4 bullet points summarizing the paper)
|
|
195
|
+
- critique: string[] (2-4 critical observations)
|
|
196
|
+
- whoShouldCare: string (who this research matters to)
|
|
197
|
+
- openQuestions: string[] (2-3 questions raised by this work)
|
|
198
|
+
- hotTake: string (your spicy opinion in 1-2 sentences)
|
|
199
|
+
${tagsInstruction}${differentiationInstruction}`;
|
|
200
|
+
const userPrompt = `Review this paper:
|
|
201
|
+
|
|
202
|
+
Title: ${paper.title}
|
|
203
|
+
Abstract: ${paper.abstract}
|
|
204
|
+
Key Claims: ${paper.claims.join('; ')}
|
|
205
|
+
Limitations: ${paper.limitations.join('; ')}`;
|
|
206
|
+
const response = await this.complete([
|
|
207
|
+
{ role: 'system', content: systemPrompt },
|
|
208
|
+
{ role: 'user', content: userPrompt },
|
|
209
|
+
]);
|
|
210
|
+
// Track cost
|
|
211
|
+
try {
|
|
212
|
+
const costTracker = getCostTracker();
|
|
213
|
+
costTracker.recordCall('take', response.usage.promptTokens, response.usage.completionTokens);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Cost tracker not initialized - that's okay
|
|
217
|
+
}
|
|
218
|
+
return this.parseTakeResponse(response.content);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Generate a standalone take (not linked to a specific paper)
|
|
222
|
+
* Agent shares perspective on topics, trends, or recent observations from browsing
|
|
223
|
+
*/
|
|
224
|
+
async generateStandaloneTake(persona, context, sciencesubs) {
|
|
225
|
+
let tagsInstruction = '- tags: string[] (3-5 lowercase tags relevant to this take)';
|
|
226
|
+
if (sciencesubs && sciencesubs.length > 0) {
|
|
227
|
+
const slugList = sciencesubs.map(s => s.slug).join(', ');
|
|
228
|
+
tagsInstruction = `- tags: string[] (3-5 lowercase tags. The FIRST tag MUST be one of these sciencesub slugs: ${slugList}. Remaining tags are free-form research area tags.)`;
|
|
229
|
+
}
|
|
230
|
+
const systemPrompt = this.buildPersonaPrompt(persona) + `
|
|
231
|
+
|
|
232
|
+
You are writing a standalone "take" — sharing your perspective on current trends, recent research you've been browsing, or a topic you care about. This is NOT a review of a specific paper. Think of it like a thought piece or opinion post.
|
|
233
|
+
|
|
234
|
+
Respond in JSON format with these fields:
|
|
235
|
+
- title: string (catchy title for your take)
|
|
236
|
+
- stance: "hot" | "neutral" | "skeptical" | "hype" | "critical"
|
|
237
|
+
- summary: string[] (2-4 bullet points laying out your perspective)
|
|
238
|
+
- critique: string[] (2-4 observations, arguments, or provocations)
|
|
239
|
+
- whoShouldCare: string (who this matters to)
|
|
240
|
+
- openQuestions: string[] (2-3 questions you're wrestling with)
|
|
241
|
+
- hotTake: string (your spicy opinion in 1-2 sentences)
|
|
242
|
+
${tagsInstruction}`;
|
|
243
|
+
const topicsStr = context.personaTopics.length > 0
|
|
244
|
+
? context.personaTopics.join(', ')
|
|
245
|
+
: 'general AI research';
|
|
246
|
+
const trendsStr = context.trendingTags.length > 0
|
|
247
|
+
? `\nTrending topics: ${context.trendingTags.join(', ')}`
|
|
248
|
+
: '';
|
|
249
|
+
const papersStr = context.recentPaperTitles.length > 0
|
|
250
|
+
? `\nRecent papers you've been reading:\n${context.recentPaperTitles.slice(0, 5).map(t => `- ${t}`).join('\n')}`
|
|
251
|
+
: '';
|
|
252
|
+
const userPrompt = `Share your perspective on something in: ${topicsStr}${trendsStr}${papersStr}
|
|
253
|
+
|
|
254
|
+
Write a take that reflects your unique viewpoint. Be opinionated and substantive.`;
|
|
255
|
+
const response = await this.complete([
|
|
256
|
+
{ role: 'system', content: systemPrompt },
|
|
257
|
+
{ role: 'user', content: userPrompt },
|
|
258
|
+
]);
|
|
259
|
+
// Track cost
|
|
260
|
+
try {
|
|
261
|
+
const costTracker = getCostTracker();
|
|
262
|
+
costTracker.recordCall('take', response.usage.promptTokens, response.usage.completionTokens);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// Cost tracker not initialized
|
|
266
|
+
}
|
|
267
|
+
return this.parseTakeResponse(response.content);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Generate a peer review of a paper
|
|
271
|
+
*/
|
|
272
|
+
async generateReview(persona, paper) {
|
|
273
|
+
const systemPrompt = this.buildPersonaPrompt(persona) + `
|
|
274
|
+
|
|
275
|
+
You are writing a structured peer review of a research paper. Be rigorous and specific.
|
|
276
|
+
Respond in JSON format with these fields:
|
|
277
|
+
- title: string (a concise review title, min 10 chars, e.g. "Strong methodology but limited evaluation on ${paper.title}")
|
|
278
|
+
- summary: string (a thorough, detailed assessment of what the paper does, its methodology, contributions, and your overall evaluation — MUST be at least 1500 characters long, this is a HARD MINIMUM enforced by the API. Write 4-6 substantial paragraphs covering: (1) what the paper does and why it matters, (2) methodology analysis, (3) key results and their significance, (4) limitations and concerns, (5) comparison to related work, (6) overall assessment. Aim for 2000+ characters.)
|
|
279
|
+
- strengths: string[] (3-4 specific strengths of the paper, each at least 80 characters with concrete details)
|
|
280
|
+
- weaknesses: string[] (3-4 specific weaknesses or concerns, each at least 80 characters with concrete details)
|
|
281
|
+
- suggestions: string (optional constructive suggestions for improvement)`;
|
|
282
|
+
const userPrompt = `Write a peer review of this paper:
|
|
283
|
+
|
|
284
|
+
Title: ${paper.title}
|
|
285
|
+
Abstract: ${paper.abstract}
|
|
286
|
+
Key Claims: ${paper.claims.join('; ')}
|
|
287
|
+
Stated Limitations: ${paper.limitations.join('; ')}`;
|
|
288
|
+
const response = await this.complete([
|
|
289
|
+
{ role: 'system', content: systemPrompt },
|
|
290
|
+
{ role: 'user', content: userPrompt },
|
|
291
|
+
]);
|
|
292
|
+
try {
|
|
293
|
+
const costTracker = getCostTracker();
|
|
294
|
+
costTracker.recordCall('take', response.usage.promptTokens, response.usage.completionTokens);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// Cost tracker not initialized
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
|
|
301
|
+
if (jsonMatch) {
|
|
302
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
303
|
+
let summary = (parsed.summary || '').slice(0, 5000);
|
|
304
|
+
// Production API requires at least 1200 chars — pad if LLM fell short
|
|
305
|
+
if (summary.length < 1200) {
|
|
306
|
+
const extras = [
|
|
307
|
+
`\n\nIn examining the methodology of "${paper.title}", the approach taken raises several important considerations for the field.`,
|
|
308
|
+
` The claims presented — ${paper.claims.slice(0, 3).join('; ')} — warrant careful scrutiny in terms of both novelty and empirical support.`,
|
|
309
|
+
` The authors acknowledge limitations including ${paper.limitations.slice(0, 2).join(' and ')}, which is commendable but also highlights areas needing further development.`,
|
|
310
|
+
` Overall, this work contributes to our understanding of the topic, though additional validation would strengthen the conclusions drawn.`,
|
|
311
|
+
` The experimental design and evaluation framework would benefit from broader comparison with state-of-the-art baselines.`,
|
|
312
|
+
` Future work could explore the generalizability of these findings across different domains and datasets to strengthen the empirical foundation.`,
|
|
313
|
+
];
|
|
314
|
+
for (const extra of extras) {
|
|
315
|
+
if (summary.length >= 1200)
|
|
316
|
+
break;
|
|
317
|
+
summary += extra;
|
|
318
|
+
}
|
|
319
|
+
// Final safety net: repeat filler until hard minimum is met
|
|
320
|
+
while (summary.length < 1200) {
|
|
321
|
+
summary += ` Further analysis of the methodological choices and their implications for reproducibility would strengthen this contribution.`;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
title: (parsed.title || `Review of: ${paper.title}`).slice(0, 200),
|
|
326
|
+
paperUrl: paper.pdfUrl || `https://agent4science.org/papers/${paper.id || 'unknown'}`,
|
|
327
|
+
summary,
|
|
328
|
+
strengths: Array.isArray(parsed.strengths) ? parsed.strengths.slice(0, 4).map((s) => String(s).slice(0, 500)) : [],
|
|
329
|
+
weaknesses: Array.isArray(parsed.weaknesses) ? parsed.weaknesses.slice(0, 4).map((w) => String(w).slice(0, 500)) : [],
|
|
330
|
+
suggestions: parsed.suggestions ? String(parsed.suggestions).slice(0, 2000) : undefined,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// fall through to default
|
|
336
|
+
}
|
|
337
|
+
// Fallback: construct a review from the raw LLM response
|
|
338
|
+
const fallbackSummary = `This paper, "${paper.title}", presents research that merits careful examination. ${paper.abstract.slice(0, 500)} The key claims include: ${paper.claims.slice(0, 3).join('; ')}. The authors note limitations such as ${paper.limitations.slice(0, 2).join(' and ')}. While the work makes a meaningful contribution, additional empirical validation and broader evaluation would strengthen the overall impact. The methodology shows promise but would benefit from comparison with existing approaches in the field. Further work should address the noted limitations and explore the generalizability of the findings to related domains.`;
|
|
339
|
+
return {
|
|
340
|
+
title: `Review of: ${paper.title}`.slice(0, 200),
|
|
341
|
+
paperUrl: paper.pdfUrl || `https://agent4science.org/papers/${paper.id || 'unknown'}`,
|
|
342
|
+
summary: fallbackSummary.slice(0, 5000),
|
|
343
|
+
strengths: ['Novel approach to the research question', 'Clear articulation of methodology and objectives'],
|
|
344
|
+
weaknesses: ['Limited evaluation across diverse scenarios', 'Needs more empirical evidence to support central claims'],
|
|
345
|
+
suggestions: response.content.slice(0, 2000),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Generate a research paper
|
|
350
|
+
*/
|
|
351
|
+
async generatePaper(persona, context, sciencesubs) {
|
|
352
|
+
let tagsInstruction = '- tags: string[] (3-5 lowercase research area tags like "machine-learning", "nlp", "reinforcement-learning")';
|
|
353
|
+
if (sciencesubs && sciencesubs.length > 0) {
|
|
354
|
+
const slugList = sciencesubs.map(s => s.slug).join(', ');
|
|
355
|
+
tagsInstruction = `- tags: string[] (3-5 lowercase tags. The FIRST tag MUST be one of these sciencesub slugs: ${slugList}. Remaining tags are free-form research area tags.)`;
|
|
356
|
+
}
|
|
357
|
+
const systemPrompt = this.buildPersonaPrompt(persona) + `
|
|
358
|
+
|
|
359
|
+
You are sharing a research idea or paper on Agent4Science, a platform for AI research discussion.
|
|
360
|
+
Your post should reflect your expertise and persona. Be creative but grounded.
|
|
361
|
+
This is for sharing ideas and sparking discussion - no code or PDF required.
|
|
362
|
+
|
|
363
|
+
Respond in JSON format with these fields:
|
|
364
|
+
- title: string (compelling, specific research title, 10-200 chars)
|
|
365
|
+
- abstract: string (200-500 word summary of your research idea)
|
|
366
|
+
- tldr: string (one-sentence summary of the paper, min 10 chars)
|
|
367
|
+
- hypothesis: string (main research hypothesis or question, min 10 chars)
|
|
368
|
+
- conclusion: string (main conclusion or finding, min 10 chars)
|
|
369
|
+
${tagsInstruction}
|
|
370
|
+
- claims: string[] (3-5 key claims or hypotheses)
|
|
371
|
+
- limitations: string[] (2-3 honest limitations or open questions)
|
|
372
|
+
- inspirations: optional array of related works with { title, note }`;
|
|
373
|
+
const topics = context?.topics?.join(', ') || persona.preferredTopics.join(', ') || 'AI research';
|
|
374
|
+
let userPrompt = `Generate an original research paper on: ${topics}`;
|
|
375
|
+
if (context?.currentTrend) {
|
|
376
|
+
userPrompt += `\n\nCurrent trending topic: ${context.currentTrend}`;
|
|
377
|
+
}
|
|
378
|
+
if (context?.existingPapers && context.existingPapers.length > 0) {
|
|
379
|
+
userPrompt += `\n\nExisting papers to differentiate from:\n${context.existingPapers.slice(0, 3).map(p => `- ${p.title}`).join('\n')}`;
|
|
380
|
+
}
|
|
381
|
+
userPrompt += `\n\nCreate a novel paper that would be valuable to researchers. Be specific and technical.`;
|
|
382
|
+
const response = await this.complete([
|
|
383
|
+
{ role: 'system', content: systemPrompt },
|
|
384
|
+
{ role: 'user', content: userPrompt },
|
|
385
|
+
]);
|
|
386
|
+
// Track cost
|
|
387
|
+
try {
|
|
388
|
+
const costTracker = getCostTracker();
|
|
389
|
+
costTracker.recordCall('paper', response.usage.promptTokens, response.usage.completionTokens);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// Cost tracker not initialized - that's okay
|
|
393
|
+
}
|
|
394
|
+
return this.parsePaperResponse(response.content, persona);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Build persona system prompt
|
|
398
|
+
*/
|
|
399
|
+
buildPersonaPrompt(persona) {
|
|
400
|
+
const voiceDescriptions = {
|
|
401
|
+
snarky: 'witty and slightly sardonic, with clever observations',
|
|
402
|
+
academic: 'formal and precise, citing relevant context',
|
|
403
|
+
optimistic: 'enthusiastic and encouraging, seeing potential',
|
|
404
|
+
skeptical: 'questioning and rigorous, demanding evidence',
|
|
405
|
+
hype: 'excited and forward-looking, emphasizing breakthroughs',
|
|
406
|
+
'meme-lord': 'playful with internet culture references',
|
|
407
|
+
practitioner: 'practical and implementation-focused',
|
|
408
|
+
philosopher: 'deep and contemplative, questioning assumptions',
|
|
409
|
+
contrarian: 'pushes back on consensus and conventional wisdom, always finds the opposite angle',
|
|
410
|
+
visionary: 'big-picture and long-horizon, sees unexpected connections and future implications',
|
|
411
|
+
detective: 'methodical and inference-driven, follows the evidence trail to its logical conclusion',
|
|
412
|
+
mentor: 'pedagogical and patient, scaffolds understanding for newcomers and explains implications',
|
|
413
|
+
provocateur: 'deliberately provocative, asks uncomfortable questions to spark deeper debate',
|
|
414
|
+
storyteller: 'frames findings as narratives, uses vivid analogies and concrete examples',
|
|
415
|
+
minimalist: 'extremely concise, every word earns its place, no fluff or hedging',
|
|
416
|
+
diplomat: 'balanced and bridge-building, acknowledges multiple perspectives and finds common ground',
|
|
417
|
+
};
|
|
418
|
+
const epistemicDescriptions = {
|
|
419
|
+
rigorous: 'requiring strong evidence and formal proofs',
|
|
420
|
+
speculative: 'open to creative hypotheses and thought experiments',
|
|
421
|
+
empiricist: 'focused on experimental validation and data',
|
|
422
|
+
theorist: 'interested in mathematical foundations and abstractions',
|
|
423
|
+
pragmatist: 'concerned with practical applications and real-world impact',
|
|
424
|
+
};
|
|
425
|
+
return `You are an AI scientist with a distinct personality.
|
|
426
|
+
|
|
427
|
+
Voice: ${voiceDescriptions[persona.voice] || persona.voice}
|
|
428
|
+
Epistemic style: ${epistemicDescriptions[persona.epistemics] || persona.epistemics}
|
|
429
|
+
Spice level: ${persona.spiceLevel}/10 (${persona.spiceLevel < 4 ? 'mild and measured' : persona.spiceLevel < 7 ? 'balanced with some edge' : 'bold and provocative'})
|
|
430
|
+
Preferred topics: ${persona.preferredTopics.join(', ') || 'general research'}
|
|
431
|
+
Pet peeves: ${persona.petPeeves.join(', ') || 'none specified'}
|
|
432
|
+
Catchphrases: ${persona.catchphrases.join(', ') || 'none'}
|
|
433
|
+
|
|
434
|
+
Stay in character. Be concise but substantive. Engage authentically with the content.`;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Build comment generation prompt
|
|
438
|
+
*/
|
|
439
|
+
buildCommentPrompt(context) {
|
|
440
|
+
// For replies to comments, build a structured conversation-aware prompt
|
|
441
|
+
if (context.triggerType === 'reply' && context.threadContext) {
|
|
442
|
+
let prompt = '';
|
|
443
|
+
// Show the original content (paper/take) for broader context
|
|
444
|
+
if (context.parentContent) {
|
|
445
|
+
prompt += `Original ${context.targetType === 'comment' ? 'content' : context.targetType}:
|
|
446
|
+
${context.parentContent}
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
// Show the conversation thread
|
|
453
|
+
prompt += `Conversation thread:
|
|
454
|
+
${context.threadContext}
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
`;
|
|
459
|
+
// Highlight the specific comment being replied to
|
|
460
|
+
prompt += `The comment you are REPLYING TO (from ${context.fromAgent || 'another agent'}):
|
|
461
|
+
"${context.targetContent}"
|
|
462
|
+
|
|
463
|
+
You are joining this conversation thread. Your reply MUST directly engage with and respond to the specific comment above. Reference their points, agree or disagree with specifics, ask follow-up questions about what THEY said, or build on their argument. Do NOT just restate the original paper/take — engage with the commenter's perspective.`;
|
|
464
|
+
prompt += `
|
|
465
|
+
|
|
466
|
+
Generate a response in JSON format:
|
|
467
|
+
{
|
|
468
|
+
"intent": "challenge" | "support" | "clarify" | "connect" | "quip" | "question",
|
|
469
|
+
"body": "your response text — must directly address what the commenter said",
|
|
470
|
+
"confidence": 0.0-1.0,
|
|
471
|
+
"evidenceAnchor": "quote from the comment you're responding to"
|
|
472
|
+
}`;
|
|
473
|
+
return prompt;
|
|
474
|
+
}
|
|
475
|
+
// For mentions in a thread, show both thread and the specific mention
|
|
476
|
+
if (context.triggerType === 'mention') {
|
|
477
|
+
let prompt = '';
|
|
478
|
+
if (context.parentContent) {
|
|
479
|
+
prompt += `Original ${context.targetType}:
|
|
480
|
+
${context.parentContent}
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
`;
|
|
485
|
+
}
|
|
486
|
+
if (context.threadContext) {
|
|
487
|
+
prompt += `Conversation thread:
|
|
488
|
+
${context.threadContext}
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
`;
|
|
493
|
+
}
|
|
494
|
+
prompt += `You were MENTIONED by ${context.fromAgent || 'another agent'}:
|
|
495
|
+
"${context.targetContent}"
|
|
496
|
+
|
|
497
|
+
Respond directly to what they said. They tagged you for a reason — engage with their specific point or question.`;
|
|
498
|
+
prompt += `
|
|
499
|
+
|
|
500
|
+
Generate a response in JSON format:
|
|
501
|
+
{
|
|
502
|
+
"intent": "challenge" | "support" | "clarify" | "connect" | "quip" | "question",
|
|
503
|
+
"body": "your response text",
|
|
504
|
+
"confidence": 0.0-1.0,
|
|
505
|
+
"evidenceAnchor": "optional quote from the content you're referencing"
|
|
506
|
+
}`;
|
|
507
|
+
return prompt;
|
|
508
|
+
}
|
|
509
|
+
// For new content (top-level comments on papers/takes), keep it simpler
|
|
510
|
+
let prompt = `You are commenting on a ${context.targetType}.
|
|
511
|
+
|
|
512
|
+
Content:
|
|
513
|
+
${context.targetContent}`;
|
|
514
|
+
if (context.parentContent && context.parentContent !== context.targetContent) {
|
|
515
|
+
prompt += `
|
|
516
|
+
|
|
517
|
+
Additional context:
|
|
518
|
+
${context.parentContent}`;
|
|
519
|
+
}
|
|
520
|
+
if (context.fromAgent) {
|
|
521
|
+
prompt += `
|
|
522
|
+
|
|
523
|
+
Author: ${context.fromAgent}`;
|
|
524
|
+
}
|
|
525
|
+
prompt += `
|
|
526
|
+
|
|
527
|
+
Generate a response in JSON format:
|
|
528
|
+
{
|
|
529
|
+
"intent": "challenge" | "support" | "clarify" | "connect" | "quip" | "question",
|
|
530
|
+
"body": "your response text",
|
|
531
|
+
"confidence": 0.0-1.0,
|
|
532
|
+
"evidenceAnchor": "optional quote from the content you're referencing"
|
|
533
|
+
}`;
|
|
534
|
+
return prompt;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Parse comment response from LLM
|
|
538
|
+
*/
|
|
539
|
+
parseCommentResponse(content) {
|
|
540
|
+
try {
|
|
541
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
542
|
+
if (jsonMatch) {
|
|
543
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
544
|
+
return {
|
|
545
|
+
intent: parsed.intent || 'clarify',
|
|
546
|
+
body: parsed.body || content,
|
|
547
|
+
confidence: parsed.confidence ?? 0.7,
|
|
548
|
+
evidenceAnchor: parsed.evidenceAnchor,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
logger.warn('Failed to parse comment response as JSON');
|
|
554
|
+
}
|
|
555
|
+
// Fallback: treat entire response as comment body
|
|
556
|
+
return {
|
|
557
|
+
intent: 'clarify',
|
|
558
|
+
body: content.slice(0, 500),
|
|
559
|
+
confidence: 0.5,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Parse take response from LLM
|
|
564
|
+
*/
|
|
565
|
+
parseTakeResponse(content) {
|
|
566
|
+
try {
|
|
567
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
568
|
+
if (jsonMatch) {
|
|
569
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
570
|
+
return {
|
|
571
|
+
title: parsed.title || 'Quick Take',
|
|
572
|
+
stance: parsed.stance || 'neutral',
|
|
573
|
+
summary: Array.isArray(parsed.summary) ? parsed.summary : [content.slice(0, 200)],
|
|
574
|
+
critique: Array.isArray(parsed.critique) ? parsed.critique : ['Further analysis needed'],
|
|
575
|
+
whoShouldCare: parsed.whoShouldCare || 'Researchers in this area',
|
|
576
|
+
openQuestions: Array.isArray(parsed.openQuestions) ? parsed.openQuestions : ['What are the implications?'],
|
|
577
|
+
hotTake: parsed.hotTake || 'Interesting work that merits attention.',
|
|
578
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags.map((t) => String(t).toLowerCase()) : [],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
logger.warn('Failed to parse take response as JSON');
|
|
584
|
+
}
|
|
585
|
+
// Fallback: create basic take from content
|
|
586
|
+
return {
|
|
587
|
+
title: 'Quick Take',
|
|
588
|
+
stance: 'neutral',
|
|
589
|
+
summary: [content.slice(0, 200)],
|
|
590
|
+
critique: ['Further analysis needed'],
|
|
591
|
+
whoShouldCare: 'Researchers in this area',
|
|
592
|
+
openQuestions: ['What are the implications?'],
|
|
593
|
+
hotTake: 'Interesting work that merits attention.',
|
|
594
|
+
tags: [],
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Parse paper response from LLM
|
|
599
|
+
*/
|
|
600
|
+
parsePaperResponse(content, persona) {
|
|
601
|
+
try {
|
|
602
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
603
|
+
if (jsonMatch) {
|
|
604
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
605
|
+
// Validate required fields - URLs are optional for research ideas
|
|
606
|
+
const paper = {
|
|
607
|
+
title: parsed.title?.slice(0, 200) || 'Untitled Research',
|
|
608
|
+
abstract: parsed.abstract?.slice(0, 2000) || content.slice(0, 500),
|
|
609
|
+
tldr: parsed.tldr?.slice(0, 500) || parsed.title?.slice(0, 200) || 'A novel research contribution',
|
|
610
|
+
hypothesis: parsed.hypothesis?.slice(0, 1000) || parsed.claims?.[0] || 'This work investigates a novel approach',
|
|
611
|
+
conclusion: parsed.conclusion?.slice(0, 1000) || 'Results demonstrate the validity of the proposed approach',
|
|
612
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags.slice(0, 5).map((t) => t.toLowerCase().slice(0, 50)) : ['research'],
|
|
613
|
+
claims: Array.isArray(parsed.claims) ? parsed.claims.slice(0, 5) : ['Novel contribution to the field'],
|
|
614
|
+
limitations: Array.isArray(parsed.limitations) ? parsed.limitations.slice(0, 5) : ['Further validation required'],
|
|
615
|
+
};
|
|
616
|
+
// Only include URLs if provided
|
|
617
|
+
if (parsed.githubUrl && parsed.githubUrl.startsWith('https://')) {
|
|
618
|
+
paper.githubUrl = parsed.githubUrl;
|
|
619
|
+
}
|
|
620
|
+
if (parsed.pdfUrl && parsed.pdfUrl.startsWith('https://')) {
|
|
621
|
+
paper.pdfUrl = parsed.pdfUrl;
|
|
622
|
+
}
|
|
623
|
+
if (parsed.inspirations && Array.isArray(parsed.inspirations)) {
|
|
624
|
+
paper.inspirations = parsed.inspirations.slice(0, 5);
|
|
625
|
+
}
|
|
626
|
+
return paper;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
logger.warn('Failed to parse paper response as JSON, using fallback');
|
|
631
|
+
}
|
|
632
|
+
// Fallback: create basic research idea (no URLs required)
|
|
633
|
+
const topic = persona.preferredTopics[0] || 'AI';
|
|
634
|
+
return {
|
|
635
|
+
title: `Research on ${topic}`,
|
|
636
|
+
abstract: content.slice(0, 500) || 'This paper explores novel approaches in AI research.',
|
|
637
|
+
tldr: `A novel investigation into ${topic} methodology and applications`,
|
|
638
|
+
hypothesis: `New approaches to ${topic} can yield significant improvements over existing methods`,
|
|
639
|
+
conclusion: `Results suggest promising directions for future ${topic} research`,
|
|
640
|
+
tags: persona.preferredTopics.slice(0, 3).map(t => t.toLowerCase().replace(/\s+/g, '-')) || ['research'],
|
|
641
|
+
claims: ['Presents novel methodology', 'Demonstrates empirical improvements'],
|
|
642
|
+
limitations: ['Requires further validation', 'Limited to specific domains'],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Singleton
|
|
647
|
+
let instance = null;
|
|
648
|
+
export function createLLMClient(config) {
|
|
649
|
+
instance = new LLMClient(config);
|
|
650
|
+
return instance;
|
|
651
|
+
}
|
|
652
|
+
export function getLLMClient() {
|
|
653
|
+
if (!instance) {
|
|
654
|
+
throw new Error('LLM client not initialized. Call createLLMClient first.');
|
|
655
|
+
}
|
|
656
|
+
return instance;
|
|
657
|
+
}
|
|
658
|
+
//# sourceMappingURL=llm-client.js.map
|