@auxiora/research 1.0.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 +191 -0
- package/dist/brave-search.d.ts +15 -0
- package/dist/brave-search.d.ts.map +1 -0
- package/dist/brave-search.js +106 -0
- package/dist/brave-search.js.map +1 -0
- package/dist/citation.d.ts +12 -0
- package/dist/citation.d.ts.map +1 -0
- package/dist/citation.js +64 -0
- package/dist/citation.js.map +1 -0
- package/dist/credibility.d.ts +7 -0
- package/dist/credibility.d.ts.map +1 -0
- package/dist/credibility.js +57 -0
- package/dist/credibility.js.map +1 -0
- package/dist/engine.d.ts +24 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +204 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge-graph.d.ts +22 -0
- package/dist/knowledge-graph.d.ts.map +1 -0
- package/dist/knowledge-graph.js +74 -0
- package/dist/knowledge-graph.js.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -0
- package/src/brave-search.ts +130 -0
- package/src/citation.ts +80 -0
- package/src/credibility.ts +65 -0
- package/src/engine.ts +244 -0
- package/src/index.ts +18 -0
- package/src/knowledge-graph.ts +87 -0
- package/src/types.ts +78 -0
- package/tests/citation.test.ts +72 -0
- package/tests/credibility.test.ts +53 -0
- package/tests/engine.test.ts +104 -0
- package/tests/knowledge-graph.test.ts +68 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/citation.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import type { Source, Finding } from './types.js';
|
|
3
|
+
|
|
4
|
+
export class CitationTracker {
|
|
5
|
+
private sources = new Map<string, Source>();
|
|
6
|
+
private findings: Finding[] = [];
|
|
7
|
+
|
|
8
|
+
addSource(url: string, title: string, credibilityScore?: number): Source {
|
|
9
|
+
const parsed = new URL(url);
|
|
10
|
+
const domain = parsed.hostname.replace(/^www\./, '');
|
|
11
|
+
|
|
12
|
+
const source: Source = {
|
|
13
|
+
id: nanoid(),
|
|
14
|
+
url,
|
|
15
|
+
title,
|
|
16
|
+
domain,
|
|
17
|
+
accessedAt: Date.now(),
|
|
18
|
+
credibilityScore: credibilityScore ?? 0.5,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.sources.set(source.id, source);
|
|
22
|
+
return source;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
addFinding(content: string, sourceId: string, relevance?: number, category?: string): Finding {
|
|
26
|
+
const finding: Finding = {
|
|
27
|
+
id: nanoid(),
|
|
28
|
+
content,
|
|
29
|
+
sourceId,
|
|
30
|
+
relevance: relevance ?? 0.5,
|
|
31
|
+
category: category ?? 'general',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
this.findings.push(finding);
|
|
35
|
+
return finding;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getSources(): Source[] {
|
|
39
|
+
return Array.from(this.sources.values());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getFindings(sourceId?: string): Finding[] {
|
|
43
|
+
if (sourceId) {
|
|
44
|
+
return this.findings.filter((f) => f.sourceId === sourceId);
|
|
45
|
+
}
|
|
46
|
+
return [...this.findings];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
formatCitations(style: 'inline' | 'footnote' | 'bibliography'): string {
|
|
50
|
+
const sources = this.getSources();
|
|
51
|
+
|
|
52
|
+
switch (style) {
|
|
53
|
+
case 'inline':
|
|
54
|
+
return sources
|
|
55
|
+
.map((s, i) => `[${i + 1}] ${s.title} (${s.domain})`)
|
|
56
|
+
.join('\n');
|
|
57
|
+
|
|
58
|
+
case 'footnote':
|
|
59
|
+
return sources
|
|
60
|
+
.map((s, i) => {
|
|
61
|
+
const date = new Date(s.accessedAt).toISOString().split('T')[0];
|
|
62
|
+
return `^${i + 1} ${s.title}. ${s.url}. Accessed: ${date}`;
|
|
63
|
+
})
|
|
64
|
+
.join('\n');
|
|
65
|
+
|
|
66
|
+
case 'bibliography':
|
|
67
|
+
return sources
|
|
68
|
+
.map(
|
|
69
|
+
(s) =>
|
|
70
|
+
`${s.title}. ${s.domain}. ${s.url}. Credibility: ${s.credibilityScore.toFixed(2)}`,
|
|
71
|
+
)
|
|
72
|
+
.join('\n');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
clear(): void {
|
|
77
|
+
this.sources.clear();
|
|
78
|
+
this.findings = [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CredibilityFactors } from './types.js';
|
|
2
|
+
|
|
3
|
+
const KNOWN_DOMAINS = new Map<string, number>([
|
|
4
|
+
['wikipedia.org', 0.95],
|
|
5
|
+
['arxiv.org', 0.95],
|
|
6
|
+
['nature.com', 0.95],
|
|
7
|
+
['science.org', 0.95],
|
|
8
|
+
['bbc.com', 0.9],
|
|
9
|
+
['reuters.com', 0.9],
|
|
10
|
+
['apnews.com', 0.9],
|
|
11
|
+
['nytimes.com', 0.8],
|
|
12
|
+
['washingtonpost.com', 0.8],
|
|
13
|
+
['theguardian.com', 0.8],
|
|
14
|
+
['techcrunch.com', 0.8],
|
|
15
|
+
['medium.com', 0.7],
|
|
16
|
+
['stackoverflow.com', 0.7],
|
|
17
|
+
['github.com', 0.7],
|
|
18
|
+
['reddit.com', 0.5],
|
|
19
|
+
['quora.com', 0.5],
|
|
20
|
+
['twitter.com', 0.3],
|
|
21
|
+
['facebook.com', 0.3],
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export class CredibilityScorer {
|
|
25
|
+
score(url: string, factors?: Partial<CredibilityFactors>): number {
|
|
26
|
+
const domain = this.extractDomain(url);
|
|
27
|
+
let score = this.getDomainReputation(domain);
|
|
28
|
+
|
|
29
|
+
if (factors?.isHttps) {
|
|
30
|
+
score += 0.05;
|
|
31
|
+
}
|
|
32
|
+
if (factors?.hasAuthor) {
|
|
33
|
+
score += 0.05;
|
|
34
|
+
}
|
|
35
|
+
if (factors?.hasDate) {
|
|
36
|
+
score += 0.05;
|
|
37
|
+
}
|
|
38
|
+
if (factors?.crossReferenced) {
|
|
39
|
+
score += 0.1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return Math.min(Math.max(score, 0), 1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getDomainReputation(domain: string): number {
|
|
46
|
+
const lookup = KNOWN_DOMAINS.get(domain);
|
|
47
|
+
if (lookup !== undefined) {
|
|
48
|
+
return lookup;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (domain.endsWith('.gov')) {
|
|
52
|
+
return 0.9;
|
|
53
|
+
}
|
|
54
|
+
if (domain.endsWith('.edu')) {
|
|
55
|
+
return 0.9;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return 0.5;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
extractDomain(url: string): string {
|
|
62
|
+
const parsed = new URL(url);
|
|
63
|
+
return parsed.hostname.replace(/^www\./, '');
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import type { ResearchDepth, ResearchQuery, ResearchResult, Finding, Source, ResearchProvider } from './types.js';
|
|
3
|
+
import { BraveSearchClient } from './brave-search.js';
|
|
4
|
+
import { CredibilityScorer } from './credibility.js';
|
|
5
|
+
import { CitationTracker } from './citation.js';
|
|
6
|
+
|
|
7
|
+
export interface ResearchEngineConfig {
|
|
8
|
+
maxConcurrentSources?: number;
|
|
9
|
+
defaultDepth?: ResearchDepth;
|
|
10
|
+
braveApiKey?: string;
|
|
11
|
+
searchTimeout?: number;
|
|
12
|
+
fetchTimeout?: number;
|
|
13
|
+
provider?: ResearchProvider;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ResearchEngine {
|
|
17
|
+
private readonly config: Required<Omit<ResearchEngineConfig, 'provider' | 'braveApiKey'>>;
|
|
18
|
+
private readonly braveClient: BraveSearchClient;
|
|
19
|
+
private readonly provider?: ResearchProvider;
|
|
20
|
+
private readonly credibilityScorer = new CredibilityScorer();
|
|
21
|
+
|
|
22
|
+
constructor(config?: ResearchEngineConfig) {
|
|
23
|
+
const apiKey = config?.braveApiKey ?? process.env.AUXIORA_RESEARCH_BRAVE_API_KEY;
|
|
24
|
+
if (!apiKey) {
|
|
25
|
+
throw new Error('Research engine requires a Brave Search API key (braveApiKey or AUXIORA_RESEARCH_BRAVE_API_KEY)');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.config = {
|
|
29
|
+
maxConcurrentSources: config?.maxConcurrentSources ?? 5,
|
|
30
|
+
defaultDepth: config?.defaultDepth ?? 'standard',
|
|
31
|
+
searchTimeout: config?.searchTimeout ?? 10_000,
|
|
32
|
+
fetchTimeout: config?.fetchTimeout ?? 15_000,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this.braveClient = new BraveSearchClient({
|
|
36
|
+
apiKey,
|
|
37
|
+
searchTimeout: this.config.searchTimeout,
|
|
38
|
+
fetchTimeout: this.config.fetchTimeout,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.provider = config?.provider;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async research(query: ResearchQuery): Promise<ResearchResult> {
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
const depth = query.depth ?? this.config.defaultDepth;
|
|
47
|
+
const maxSources = query.maxSources ?? this.getMaxSources(depth);
|
|
48
|
+
const searchQueries = this.planResearch(query.topic, depth, query.focusAreas);
|
|
49
|
+
|
|
50
|
+
const tracker = new CitationTracker();
|
|
51
|
+
const findings: Finding[] = [];
|
|
52
|
+
|
|
53
|
+
// Execute searches in parallel, limited to maxSources total results
|
|
54
|
+
const searchPromises = searchQueries.map((q) => this.braveClient.search(q, 3));
|
|
55
|
+
const searchResults = await Promise.all(searchPromises);
|
|
56
|
+
|
|
57
|
+
// Flatten and deduplicate by URL, cap at maxSources
|
|
58
|
+
const seenUrls = new Set<string>();
|
|
59
|
+
const uniqueResults: { query: string; url: string; title: string; snippet: string }[] = [];
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < searchResults.length; i++) {
|
|
62
|
+
for (const result of searchResults[i]) {
|
|
63
|
+
if (!seenUrls.has(result.url) && uniqueResults.length < maxSources) {
|
|
64
|
+
seenUrls.add(result.url);
|
|
65
|
+
uniqueResults.push({
|
|
66
|
+
query: searchQueries[i],
|
|
67
|
+
url: result.url,
|
|
68
|
+
title: result.title,
|
|
69
|
+
snippet: result.description,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fetch pages in parallel
|
|
76
|
+
const fetchPromises = uniqueResults.map((r) => this.braveClient.fetchPage(r.url));
|
|
77
|
+
const pageContents = await Promise.all(fetchPromises);
|
|
78
|
+
|
|
79
|
+
// Process each result
|
|
80
|
+
for (let i = 0; i < uniqueResults.length; i++) {
|
|
81
|
+
const r = uniqueResults[i];
|
|
82
|
+
const credibility = this.credibilityScorer.score(r.url, {
|
|
83
|
+
isHttps: r.url.startsWith('https'),
|
|
84
|
+
});
|
|
85
|
+
const source = tracker.addSource(r.url, r.title, credibility);
|
|
86
|
+
|
|
87
|
+
const content = pageContents[i];
|
|
88
|
+
if (this.provider && content) {
|
|
89
|
+
// AI-powered extraction
|
|
90
|
+
const extracted = await this.extractFindings(r.query, content, r.title);
|
|
91
|
+
for (const text of extracted) {
|
|
92
|
+
tracker.addFinding(text, source.id, credibility, 'general');
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// Fallback: use search snippet + first paragraph from fetched content
|
|
96
|
+
let text = r.snippet;
|
|
97
|
+
if (content) {
|
|
98
|
+
const firstParagraph = content.split('\n\n').find((p) => p.trim().length > 50);
|
|
99
|
+
if (firstParagraph) {
|
|
100
|
+
text += ' ' + firstParagraph.trim();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
tracker.addFinding(text, source.id, credibility, 'general');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const allFindings = tracker.getFindings();
|
|
108
|
+
const deduplicated = this.deduplicateFindings(allFindings);
|
|
109
|
+
const sources = tracker.getSources();
|
|
110
|
+
|
|
111
|
+
// Synthesize summary
|
|
112
|
+
let executiveSummary: string;
|
|
113
|
+
if (this.provider && deduplicated.length > 0) {
|
|
114
|
+
executiveSummary = await this.synthesizeWithAI(query.topic, deduplicated, sources);
|
|
115
|
+
} else {
|
|
116
|
+
executiveSummary = this.synthesize(deduplicated);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
id: nanoid(),
|
|
121
|
+
query,
|
|
122
|
+
findings: deduplicated,
|
|
123
|
+
executiveSummary,
|
|
124
|
+
sources,
|
|
125
|
+
confidence: Math.min(deduplicated.length / maxSources, 1),
|
|
126
|
+
generatedAt: Date.now(),
|
|
127
|
+
durationMs: Date.now() - startTime,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
planResearch(topic: string, depth: ResearchDepth, focusAreas?: string[]): string[] {
|
|
132
|
+
let queries: string[];
|
|
133
|
+
switch (depth) {
|
|
134
|
+
case 'quick':
|
|
135
|
+
queries = [topic];
|
|
136
|
+
break;
|
|
137
|
+
case 'standard':
|
|
138
|
+
queries = [topic, `${topic} overview`, `${topic} analysis`];
|
|
139
|
+
break;
|
|
140
|
+
case 'deep':
|
|
141
|
+
queries = [
|
|
142
|
+
topic,
|
|
143
|
+
`${topic} overview`,
|
|
144
|
+
`${topic} analysis`,
|
|
145
|
+
`${topic} comparison`,
|
|
146
|
+
`${topic} best practices`,
|
|
147
|
+
`${topic} research papers`,
|
|
148
|
+
];
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (focusAreas?.length) {
|
|
153
|
+
for (const area of focusAreas) {
|
|
154
|
+
queries.push(`${topic} ${area}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return queries;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
synthesize(findings: Finding[]): string {
|
|
162
|
+
const sourceIds = new Set(findings.map((f) => f.sourceId));
|
|
163
|
+
const topFindings = findings
|
|
164
|
+
.sort((a, b) => b.relevance - a.relevance)
|
|
165
|
+
.slice(0, 3)
|
|
166
|
+
.map((f) => f.content);
|
|
167
|
+
|
|
168
|
+
return `Based on ${findings.length} findings from ${sourceIds.size} sources: ${topFindings.join('. ')}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
deduplicateFindings(findings: Finding[]): Finding[] {
|
|
172
|
+
const seen = new Set<string>();
|
|
173
|
+
return findings.filter((f) => {
|
|
174
|
+
if (seen.has(f.content)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
seen.add(f.content);
|
|
178
|
+
return true;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async extractFindings(query: string, content: string, title: string): Promise<string[]> {
|
|
183
|
+
if (!this.provider) return [];
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const result = await this.provider.complete(
|
|
187
|
+
[{ role: 'user', content: `Extract key findings from this source about "${query}".\n\nSource: ${title}\n\nContent:\n${content.slice(0, 8000)}` }],
|
|
188
|
+
{
|
|
189
|
+
systemPrompt: 'You are a research assistant. Extract 2-4 distinct factual findings from the source. Return each finding on a separate line, prefixed with "- ". Be concise and factual.',
|
|
190
|
+
maxTokens: 500,
|
|
191
|
+
temperature: 0.1,
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return result.content
|
|
196
|
+
.split('\n')
|
|
197
|
+
.filter((line) => line.startsWith('- '))
|
|
198
|
+
.map((line) => line.slice(2).trim())
|
|
199
|
+
.filter((line) => line.length > 10);
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async synthesizeWithAI(topic: string, findings: Finding[], sources: Source[]): Promise<string> {
|
|
206
|
+
if (!this.provider) return this.synthesize(findings);
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const findingsText = findings
|
|
210
|
+
.sort((a, b) => b.relevance - a.relevance)
|
|
211
|
+
.slice(0, 10)
|
|
212
|
+
.map((f, i) => `${i + 1}. ${f.content}`)
|
|
213
|
+
.join('\n');
|
|
214
|
+
|
|
215
|
+
const sourcesText = sources
|
|
216
|
+
.map((s) => `- ${s.title} (${s.domain}, credibility: ${s.credibilityScore.toFixed(2)})`)
|
|
217
|
+
.join('\n');
|
|
218
|
+
|
|
219
|
+
const result = await this.provider.complete(
|
|
220
|
+
[{ role: 'user', content: `Synthesize these research findings about "${topic}" into a concise executive summary.\n\nFindings:\n${findingsText}\n\nSources:\n${sourcesText}` }],
|
|
221
|
+
{
|
|
222
|
+
systemPrompt: 'You are a research analyst. Write a concise executive summary (2-4 paragraphs) synthesizing the findings. Mention source credibility where relevant. Be factual and balanced.',
|
|
223
|
+
maxTokens: 800,
|
|
224
|
+
temperature: 0.3,
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
return result.content;
|
|
229
|
+
} catch {
|
|
230
|
+
return this.synthesize(findings);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private getMaxSources(depth: ResearchDepth): number {
|
|
235
|
+
switch (depth) {
|
|
236
|
+
case 'quick':
|
|
237
|
+
return 3;
|
|
238
|
+
case 'standard':
|
|
239
|
+
return 5;
|
|
240
|
+
case 'deep':
|
|
241
|
+
return 10;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ResearchDepth,
|
|
3
|
+
ResearchQuery,
|
|
4
|
+
ResearchResult,
|
|
5
|
+
Finding,
|
|
6
|
+
Source,
|
|
7
|
+
CredibilityFactors,
|
|
8
|
+
KnowledgeEntity,
|
|
9
|
+
KnowledgeRelation,
|
|
10
|
+
ResearchProvider,
|
|
11
|
+
BraveWebResult,
|
|
12
|
+
BraveSearchResponse,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
export { ResearchEngine, type ResearchEngineConfig } from './engine.js';
|
|
15
|
+
export { BraveSearchClient, type BraveSearchOptions } from './brave-search.js';
|
|
16
|
+
export { CredibilityScorer } from './credibility.js';
|
|
17
|
+
export { CitationTracker } from './citation.js';
|
|
18
|
+
export { KnowledgeGraph } from './knowledge-graph.js';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import type { KnowledgeEntity, KnowledgeRelation } from './types.js';
|
|
3
|
+
|
|
4
|
+
export class KnowledgeGraph {
|
|
5
|
+
private entities = new Map<string, KnowledgeEntity>();
|
|
6
|
+
private relations: KnowledgeRelation[] = [];
|
|
7
|
+
|
|
8
|
+
addEntity(name: string, type: string, properties?: Record<string, string>): KnowledgeEntity {
|
|
9
|
+
const entity: KnowledgeEntity = {
|
|
10
|
+
id: nanoid(),
|
|
11
|
+
name,
|
|
12
|
+
type,
|
|
13
|
+
properties: properties ?? {},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
this.entities.set(entity.id, entity);
|
|
17
|
+
return entity;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getEntity(id: string): KnowledgeEntity | undefined {
|
|
21
|
+
return this.entities.get(id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
findByName(name: string): KnowledgeEntity | undefined {
|
|
25
|
+
for (const entity of this.entities.values()) {
|
|
26
|
+
if (entity.name === name) {
|
|
27
|
+
return entity;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
findByType(type: string): KnowledgeEntity[] {
|
|
34
|
+
const result: KnowledgeEntity[] = [];
|
|
35
|
+
for (const entity of this.entities.values()) {
|
|
36
|
+
if (entity.type === type) {
|
|
37
|
+
result.push(entity);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
addRelation(fromId: string, toId: string, relation: string): void {
|
|
44
|
+
this.relations.push({ fromId, toId, relation });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getRelated(entityId: string): Array<{ entity: KnowledgeEntity; relation: string; direction: 'from' | 'to' }> {
|
|
48
|
+
const result: Array<{ entity: KnowledgeEntity; relation: string; direction: 'from' | 'to' }> = [];
|
|
49
|
+
|
|
50
|
+
for (const rel of this.relations) {
|
|
51
|
+
if (rel.fromId === entityId) {
|
|
52
|
+
const entity = this.entities.get(rel.toId);
|
|
53
|
+
if (entity) {
|
|
54
|
+
result.push({ entity, relation: rel.relation, direction: 'from' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (rel.toId === entityId) {
|
|
58
|
+
const entity = this.entities.get(rel.fromId);
|
|
59
|
+
if (entity) {
|
|
60
|
+
result.push({ entity, relation: rel.relation, direction: 'to' });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
removeEntity(id: string): boolean {
|
|
69
|
+
const deleted = this.entities.delete(id);
|
|
70
|
+
if (deleted) {
|
|
71
|
+
this.relations = this.relations.filter((r) => r.fromId !== id && r.toId !== id);
|
|
72
|
+
}
|
|
73
|
+
return deleted;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
toJSON(): { entities: KnowledgeEntity[]; relations: KnowledgeRelation[] } {
|
|
77
|
+
return {
|
|
78
|
+
entities: Array.from(this.entities.values()),
|
|
79
|
+
relations: [...this.relations],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
clear(): void {
|
|
84
|
+
this.entities.clear();
|
|
85
|
+
this.relations = [];
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type ResearchDepth = 'quick' | 'standard' | 'deep';
|
|
2
|
+
|
|
3
|
+
export interface ResearchQuery {
|
|
4
|
+
topic: string;
|
|
5
|
+
depth: ResearchDepth;
|
|
6
|
+
maxSources?: number;
|
|
7
|
+
focusAreas?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ResearchResult {
|
|
11
|
+
id: string;
|
|
12
|
+
query: ResearchQuery;
|
|
13
|
+
findings: Finding[];
|
|
14
|
+
executiveSummary: string;
|
|
15
|
+
sources: Source[];
|
|
16
|
+
confidence: number; // 0-1
|
|
17
|
+
generatedAt: number;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Finding {
|
|
22
|
+
id: string;
|
|
23
|
+
content: string;
|
|
24
|
+
sourceId: string;
|
|
25
|
+
relevance: number; // 0-1
|
|
26
|
+
category: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Source {
|
|
30
|
+
id: string;
|
|
31
|
+
url: string;
|
|
32
|
+
title: string;
|
|
33
|
+
domain: string;
|
|
34
|
+
accessedAt: number;
|
|
35
|
+
credibilityScore: number; // 0-1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CredibilityFactors {
|
|
39
|
+
domainReputation: number;
|
|
40
|
+
hasAuthor: boolean;
|
|
41
|
+
hasDate: boolean;
|
|
42
|
+
isHttps: boolean;
|
|
43
|
+
crossReferenced: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface KnowledgeEntity {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
type: string;
|
|
50
|
+
properties: Record<string, string>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface KnowledgeRelation {
|
|
54
|
+
fromId: string;
|
|
55
|
+
toId: string;
|
|
56
|
+
relation: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Minimal provider interface — avoids hard dep on @auxiora/providers */
|
|
60
|
+
export interface ResearchProvider {
|
|
61
|
+
complete(
|
|
62
|
+
messages: { role: 'user' | 'assistant' | 'system'; content: string }[],
|
|
63
|
+
options?: { systemPrompt?: string; maxTokens?: number; temperature?: number },
|
|
64
|
+
): Promise<{ content: string }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface BraveWebResult {
|
|
68
|
+
title: string;
|
|
69
|
+
url: string;
|
|
70
|
+
description: string;
|
|
71
|
+
extra_snippets?: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BraveSearchResponse {
|
|
75
|
+
web?: {
|
|
76
|
+
results: BraveWebResult[];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CitationTracker } from '../src/citation.js';
|
|
3
|
+
|
|
4
|
+
describe('CitationTracker', () => {
|
|
5
|
+
it('addSource creates source with ID', () => {
|
|
6
|
+
const tracker = new CitationTracker();
|
|
7
|
+
const source = tracker.addSource('https://example.com/article', 'Test Article');
|
|
8
|
+
expect(source.id).toBeDefined();
|
|
9
|
+
expect(source.title).toBe('Test Article');
|
|
10
|
+
expect(source.url).toBe('https://example.com/article');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('addSource extracts domain', () => {
|
|
14
|
+
const tracker = new CitationTracker();
|
|
15
|
+
const source = tracker.addSource('https://www.example.com/article', 'Test');
|
|
16
|
+
expect(source.domain).toBe('example.com');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('addFinding links to source', () => {
|
|
20
|
+
const tracker = new CitationTracker();
|
|
21
|
+
const source = tracker.addSource('https://example.com', 'Source');
|
|
22
|
+
const finding = tracker.addFinding('Important fact', source.id);
|
|
23
|
+
expect(finding.sourceId).toBe(source.id);
|
|
24
|
+
expect(finding.content).toBe('Important fact');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('getFindings filters by sourceId', () => {
|
|
28
|
+
const tracker = new CitationTracker();
|
|
29
|
+
const s1 = tracker.addSource('https://a.com', 'A');
|
|
30
|
+
const s2 = tracker.addSource('https://b.com', 'B');
|
|
31
|
+
tracker.addFinding('Fact 1', s1.id);
|
|
32
|
+
tracker.addFinding('Fact 2', s2.id);
|
|
33
|
+
tracker.addFinding('Fact 3', s1.id);
|
|
34
|
+
|
|
35
|
+
const filtered = tracker.getFindings(s1.id);
|
|
36
|
+
expect(filtered).toHaveLength(2);
|
|
37
|
+
expect(filtered.every((f) => f.sourceId === s1.id)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('formatCitations inline format', () => {
|
|
41
|
+
const tracker = new CitationTracker();
|
|
42
|
+
tracker.addSource('https://example.com/page', 'Example Page', 0.8);
|
|
43
|
+
const output = tracker.formatCitations('inline');
|
|
44
|
+
expect(output).toContain('[1]');
|
|
45
|
+
expect(output).toContain('Example Page');
|
|
46
|
+
expect(output).toContain('example.com');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('formatCitations bibliography format', () => {
|
|
50
|
+
const tracker = new CitationTracker();
|
|
51
|
+
tracker.addSource('https://example.com/page', 'Example Page', 0.8);
|
|
52
|
+
const output = tracker.formatCitations('bibliography');
|
|
53
|
+
expect(output).toContain('Example Page');
|
|
54
|
+
expect(output).toContain('Credibility: 0.80');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('getSources returns all', () => {
|
|
58
|
+
const tracker = new CitationTracker();
|
|
59
|
+
tracker.addSource('https://a.com', 'A');
|
|
60
|
+
tracker.addSource('https://b.com', 'B');
|
|
61
|
+
expect(tracker.getSources()).toHaveLength(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('clear empties everything', () => {
|
|
65
|
+
const tracker = new CitationTracker();
|
|
66
|
+
tracker.addSource('https://a.com', 'A');
|
|
67
|
+
tracker.addFinding('fact', 'some-id');
|
|
68
|
+
tracker.clear();
|
|
69
|
+
expect(tracker.getSources()).toHaveLength(0);
|
|
70
|
+
expect(tracker.getFindings()).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CredibilityScorer } from '../src/credibility.js';
|
|
3
|
+
|
|
4
|
+
describe('CredibilityScorer', () => {
|
|
5
|
+
const scorer = new CredibilityScorer();
|
|
6
|
+
|
|
7
|
+
it('wikipedia.org scores 0.95', () => {
|
|
8
|
+
const score = scorer.score('https://wikipedia.org/wiki/Test');
|
|
9
|
+
expect(score).toBe(0.95);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('.gov domain scores 0.9', () => {
|
|
13
|
+
const score = scorer.score('https://data.gov/dataset');
|
|
14
|
+
expect(score).toBe(0.9);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('.edu domain scores 0.9', () => {
|
|
18
|
+
const score = scorer.score('https://mit.edu/research');
|
|
19
|
+
expect(score).toBe(0.9);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('unknown domain scores 0.5', () => {
|
|
23
|
+
const score = scorer.score('https://randomsite.xyz/page');
|
|
24
|
+
expect(score).toBe(0.5);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('HTTPS bonus adds 0.05', () => {
|
|
28
|
+
const base = scorer.score('https://randomsite.xyz/page');
|
|
29
|
+
const withHttps = scorer.score('https://randomsite.xyz/page', { isHttps: true });
|
|
30
|
+
expect(withHttps - base).toBeCloseTo(0.05);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('crossReferenced adds 0.1', () => {
|
|
34
|
+
const base = scorer.score('https://randomsite.xyz/page');
|
|
35
|
+
const withCross = scorer.score('https://randomsite.xyz/page', { crossReferenced: true });
|
|
36
|
+
expect(withCross - base).toBeCloseTo(0.1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('score clamped to max 1.0', () => {
|
|
40
|
+
const score = scorer.score('https://wikipedia.org/wiki/Test', {
|
|
41
|
+
isHttps: true,
|
|
42
|
+
hasAuthor: true,
|
|
43
|
+
hasDate: true,
|
|
44
|
+
crossReferenced: true,
|
|
45
|
+
});
|
|
46
|
+
expect(score).toBeLessThanOrEqual(1.0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('extractDomain handles full URLs', () => {
|
|
50
|
+
expect(scorer.extractDomain('https://www.example.com/path?q=1')).toBe('example.com');
|
|
51
|
+
expect(scorer.extractDomain('https://sub.domain.org/page')).toBe('sub.domain.org');
|
|
52
|
+
});
|
|
53
|
+
});
|