@echoes-io/mcp-server 4.0.0 → 4.1.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/cli/index.d.ts +2 -0
- package/cli/index.js +186 -0
- package/package.json +2 -1
- package/src/database/index.d.ts +6 -0
- package/src/database/index.js +26 -0
- package/src/database/relations.d.ts +744 -0
- package/src/database/relations.js +52 -0
- package/src/database/schema.d.ts +733 -0
- package/src/database/schema.js +69 -0
- package/src/database/vector.d.ts +25 -0
- package/src/database/vector.js +98 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +5 -0
- package/src/rag/character-ner.d.ts +36 -0
- package/src/rag/character-ner.js +416 -0
- package/src/rag/database-sync.d.ts +38 -0
- package/src/rag/database-sync.js +158 -0
- package/src/rag/embeddings.d.ts +74 -0
- package/src/rag/embeddings.js +164 -0
- package/src/rag/graph-rag.d.ts +69 -0
- package/src/rag/graph-rag.js +311 -0
- package/src/rag/hybrid-rag.d.ts +109 -0
- package/src/rag/hybrid-rag.js +255 -0
- package/src/rag/index.d.ts +16 -0
- package/src/rag/index.js +33 -0
- package/src/server.d.ts +43 -0
- package/src/server.js +177 -0
- package/src/tools/index-rag.d.ts +19 -0
- package/src/tools/index-rag.js +85 -0
- package/src/tools/index-tracker.d.ts +17 -0
- package/src/tools/index-tracker.js +89 -0
- package/src/tools/index.d.ts +5 -0
- package/src/tools/index.js +5 -0
- package/src/tools/rag-context.d.ts +34 -0
- package/src/tools/rag-context.js +51 -0
- package/src/tools/rag-search.d.ts +35 -0
- package/src/tools/rag-search.js +60 -0
- package/src/tools/words-count.d.ts +15 -0
- package/src/tools/words-count.js +28 -0
- package/src/types/frontmatter.d.ts +35 -0
- package/src/types/frontmatter.js +1 -0
- package/src/utils/index.d.ts +1 -0
- package/src/utils/index.js +1 -0
- package/src/utils/markdown.d.ts +6 -0
- package/src/utils/markdown.js +36 -0
- package/src/utils/timeline-detection.d.ts +13 -0
- package/src/utils/timeline-detection.js +76 -0
package/src/rag/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG Module - GraphRAG + Vector Search for Echoes
|
|
3
|
+
*
|
|
4
|
+
* Provides hybrid search capabilities combining:
|
|
5
|
+
* - GraphRAG: Semantic relationships, character connections, temporal sequences
|
|
6
|
+
* - Vector Search: Fast similarity search with sqlite-vec fallback
|
|
7
|
+
*/
|
|
8
|
+
// Embedding providers
|
|
9
|
+
export { BGEBaseEmbedding, batchArray, cosineSimilarity, createEmbeddingProvider, E5SmallEmbedding, GeminiEmbedding, normalizeEmbedding, } from './embeddings.js';
|
|
10
|
+
// Core GraphRAG implementation
|
|
11
|
+
export { GraphRAG, } from './graph-rag.js';
|
|
12
|
+
// Hybrid RAG system
|
|
13
|
+
export { DEFAULT_HYBRID_CONFIG, HybridRAG, } from './hybrid-rag.js';
|
|
14
|
+
import { DEFAULT_HYBRID_CONFIG, HybridRAG } from './hybrid-rag.js';
|
|
15
|
+
export function createHybridRAG(db, config = {}) {
|
|
16
|
+
const fullConfig = {
|
|
17
|
+
...DEFAULT_HYBRID_CONFIG,
|
|
18
|
+
...config,
|
|
19
|
+
embedding: {
|
|
20
|
+
...DEFAULT_HYBRID_CONFIG.embedding,
|
|
21
|
+
...config.embedding,
|
|
22
|
+
},
|
|
23
|
+
graphRAG: {
|
|
24
|
+
...DEFAULT_HYBRID_CONFIG.graphRAG,
|
|
25
|
+
...config.graphRAG,
|
|
26
|
+
},
|
|
27
|
+
fallback: {
|
|
28
|
+
...DEFAULT_HYBRID_CONFIG.fallback,
|
|
29
|
+
...config.fallback,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
return new HybridRAG(db, fullConfig);
|
|
33
|
+
}
|
package/src/server.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
declare const server: Server<{
|
|
3
|
+
method: string;
|
|
4
|
+
params?: {
|
|
5
|
+
[x: string]: unknown;
|
|
6
|
+
task?: {
|
|
7
|
+
[x: string]: unknown;
|
|
8
|
+
ttl?: number | null | undefined;
|
|
9
|
+
pollInterval?: number | undefined;
|
|
10
|
+
} | undefined;
|
|
11
|
+
_meta?: {
|
|
12
|
+
[x: string]: unknown;
|
|
13
|
+
progressToken?: string | number | undefined;
|
|
14
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
15
|
+
[x: string]: unknown;
|
|
16
|
+
taskId: string;
|
|
17
|
+
} | undefined;
|
|
18
|
+
} | undefined;
|
|
19
|
+
} | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
method: string;
|
|
22
|
+
params?: {
|
|
23
|
+
[x: string]: unknown;
|
|
24
|
+
_meta?: {
|
|
25
|
+
[x: string]: unknown;
|
|
26
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
27
|
+
[x: string]: unknown;
|
|
28
|
+
taskId: string;
|
|
29
|
+
} | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
} | undefined;
|
|
32
|
+
}, {
|
|
33
|
+
[x: string]: unknown;
|
|
34
|
+
_meta?: {
|
|
35
|
+
[x: string]: unknown;
|
|
36
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
37
|
+
[x: string]: unknown;
|
|
38
|
+
taskId: string;
|
|
39
|
+
} | undefined;
|
|
40
|
+
} | undefined;
|
|
41
|
+
}>;
|
|
42
|
+
export declare function runServer(): Promise<void>;
|
|
43
|
+
export { server };
|
package/src/server.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { indexRag, indexRagSchema } from './tools/index-rag.js';
|
|
5
|
+
import { indexTracker, indexTrackerSchema } from './tools/index-tracker.js';
|
|
6
|
+
import { ragContext, ragContextSchema } from './tools/rag-context.js';
|
|
7
|
+
import { ragSearch, ragSearchSchema } from './tools/rag-search.js';
|
|
8
|
+
import { wordsCount, wordsCountSchema } from './tools/words-count.js';
|
|
9
|
+
const server = new Server({
|
|
10
|
+
name: 'echoes-mcp-server',
|
|
11
|
+
version: '3.0.0',
|
|
12
|
+
}, {
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
// List available tools
|
|
18
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
19
|
+
return {
|
|
20
|
+
tools: [
|
|
21
|
+
{
|
|
22
|
+
name: 'words-count',
|
|
23
|
+
description: 'Count words and text statistics in markdown files',
|
|
24
|
+
inputSchema: wordsCountSchema,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'index-tracker',
|
|
28
|
+
description: 'Synchronize filesystem content with database',
|
|
29
|
+
inputSchema: indexTrackerSchema,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'index-rag',
|
|
33
|
+
description: 'Index chapters into GraphRAG for semantic search',
|
|
34
|
+
inputSchema: indexRagSchema,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'rag-search',
|
|
38
|
+
description: 'Search chapters using semantic similarity and character filtering',
|
|
39
|
+
inputSchema: ragSearchSchema,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'rag-context',
|
|
43
|
+
description: 'Retrieve full chapter content for AI context using semantic search',
|
|
44
|
+
inputSchema: ragContextSchema,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
// Handle tool calls
|
|
50
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
51
|
+
const { name, arguments: args } = request.params;
|
|
52
|
+
switch (name) {
|
|
53
|
+
case 'words-count':
|
|
54
|
+
try {
|
|
55
|
+
const result = await wordsCount(args);
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: JSON.stringify(result, null, 2),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
case 'index-tracker':
|
|
77
|
+
try {
|
|
78
|
+
const result = await indexTracker(args);
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: JSON.stringify(result, null, 2),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: 'text',
|
|
93
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
case 'index-rag':
|
|
100
|
+
try {
|
|
101
|
+
const result = await indexRag(args);
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: 'text',
|
|
106
|
+
text: JSON.stringify(result, null, 2),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
isError: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
case 'rag-search':
|
|
123
|
+
try {
|
|
124
|
+
const result = await ragSearch(args);
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: JSON.stringify(result, null, 2),
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
content: [
|
|
137
|
+
{
|
|
138
|
+
type: 'text',
|
|
139
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
isError: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
case 'rag-context':
|
|
146
|
+
try {
|
|
147
|
+
const result = await ragContext(args);
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: JSON.stringify(result, null, 2),
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: 'text',
|
|
162
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
isError: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
export async function runServer() {
|
|
173
|
+
const transport = new StdioServerTransport();
|
|
174
|
+
await server.connect(transport);
|
|
175
|
+
console.error('Echoes MCP Server running on stdio');
|
|
176
|
+
}
|
|
177
|
+
export { server };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const indexRagSchema: z.ZodObject<{
|
|
3
|
+
timeline: z.ZodString;
|
|
4
|
+
contentPath: z.ZodString;
|
|
5
|
+
arc: z.ZodOptional<z.ZodString>;
|
|
6
|
+
episode: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type IndexRagInput = z.infer<typeof indexRagSchema>;
|
|
9
|
+
export interface IndexRagOutput {
|
|
10
|
+
indexed: number;
|
|
11
|
+
graphNodes: number;
|
|
12
|
+
vectorEmbeddings: number;
|
|
13
|
+
relationships: number;
|
|
14
|
+
timelines: number;
|
|
15
|
+
arcs: number;
|
|
16
|
+
episodes: number;
|
|
17
|
+
chapters: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function indexRag(input: IndexRagInput): Promise<IndexRagOutput>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { initDatabase } from '../database/index.js';
|
|
5
|
+
import { ItalianCharacterNER } from '../rag/character-ner.js';
|
|
6
|
+
import { createHybridRAG } from '../rag/index.js';
|
|
7
|
+
import { parseMarkdown } from '../utils/markdown.js';
|
|
8
|
+
export const indexRagSchema = z.object({
|
|
9
|
+
timeline: z.string().describe('Timeline name'),
|
|
10
|
+
contentPath: z.string().describe('Path to content directory'),
|
|
11
|
+
arc: z.string().optional().describe('Filter by specific arc'),
|
|
12
|
+
episode: z.number().optional().describe('Filter by specific episode'),
|
|
13
|
+
});
|
|
14
|
+
export async function indexRag(input) {
|
|
15
|
+
const { timeline, contentPath, arc, episode } = indexRagSchema.parse(input);
|
|
16
|
+
// Initialize database and RAG system
|
|
17
|
+
const db = await initDatabase(':memory:');
|
|
18
|
+
const rag = createHybridRAG(db);
|
|
19
|
+
const ner = new ItalianCharacterNER();
|
|
20
|
+
// Scan filesystem for markdown files
|
|
21
|
+
const chapters = [];
|
|
22
|
+
function scanDirectory(dir) {
|
|
23
|
+
try {
|
|
24
|
+
const entries = readdirSync(dir);
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const fullPath = join(dir, entry);
|
|
27
|
+
const stat = statSync(fullPath);
|
|
28
|
+
if (stat.isDirectory()) {
|
|
29
|
+
scanDirectory(fullPath);
|
|
30
|
+
}
|
|
31
|
+
else if (entry.endsWith('.md')) {
|
|
32
|
+
try {
|
|
33
|
+
const fileContent = readFileSync(fullPath, 'utf-8');
|
|
34
|
+
const { metadata, content } = parseMarkdown(fileContent);
|
|
35
|
+
// Apply filters
|
|
36
|
+
if (arc && metadata.arc !== arc)
|
|
37
|
+
continue;
|
|
38
|
+
if (episode !== undefined && metadata.episode !== episode)
|
|
39
|
+
continue;
|
|
40
|
+
// Extract characters using NER
|
|
41
|
+
const characters = ner.extractCharacters(content);
|
|
42
|
+
chapters.push({
|
|
43
|
+
id: crypto.randomUUID(),
|
|
44
|
+
content,
|
|
45
|
+
characters,
|
|
46
|
+
metadata: {
|
|
47
|
+
chapterId: crypto.randomUUID(),
|
|
48
|
+
arc: metadata.arc || 'unknown',
|
|
49
|
+
episode: metadata.episode || 1,
|
|
50
|
+
chapter: metadata.chapter || 1,
|
|
51
|
+
pov: metadata.pov || 'unknown',
|
|
52
|
+
location: metadata.location,
|
|
53
|
+
timeline,
|
|
54
|
+
title: metadata.title,
|
|
55
|
+
summary: metadata.summary,
|
|
56
|
+
filePath: fullPath,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.warn(`Failed to parse ${fullPath}:`, error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn(`Failed to scan directory ${dir}:`, error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
scanDirectory(contentPath);
|
|
71
|
+
// Index into RAG system
|
|
72
|
+
const result = await rag.indexChapters(chapters);
|
|
73
|
+
// Calculate relationships (edges in graph)
|
|
74
|
+
const relationships = result.graphNodes > 0 ? result.graphNodes * 2 : 0; // Rough estimate
|
|
75
|
+
return {
|
|
76
|
+
indexed: chapters.length,
|
|
77
|
+
graphNodes: result.graphNodes,
|
|
78
|
+
vectorEmbeddings: result.vectorEmbeddings,
|
|
79
|
+
relationships,
|
|
80
|
+
timelines: result.dbSync.timelines,
|
|
81
|
+
arcs: result.dbSync.arcs,
|
|
82
|
+
episodes: result.dbSync.episodes,
|
|
83
|
+
chapters: result.dbSync.chapters,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const indexTrackerSchema: z.ZodObject<{
|
|
3
|
+
timeline: z.ZodString;
|
|
4
|
+
contentPath: z.ZodString;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export type IndexTrackerInput = z.infer<typeof indexTrackerSchema>;
|
|
7
|
+
export interface IndexTrackerOutput {
|
|
8
|
+
scanned: number;
|
|
9
|
+
added: number;
|
|
10
|
+
updated: number;
|
|
11
|
+
deleted: number;
|
|
12
|
+
timelines: number;
|
|
13
|
+
arcs: number;
|
|
14
|
+
episodes: number;
|
|
15
|
+
chapters: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function indexTracker(input: IndexTrackerInput): Promise<IndexTrackerOutput>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { initDatabase } from '../database/index.js';
|
|
5
|
+
import { DatabaseSync } from '../rag/database-sync.js';
|
|
6
|
+
import { parseMarkdown } from '../utils/markdown.js';
|
|
7
|
+
export const indexTrackerSchema = z.object({
|
|
8
|
+
timeline: z.string().describe('Timeline name'),
|
|
9
|
+
contentPath: z.string().describe('Path to content directory'),
|
|
10
|
+
});
|
|
11
|
+
export async function indexTracker(input) {
|
|
12
|
+
const { timeline, contentPath } = indexTrackerSchema.parse(input);
|
|
13
|
+
// Initialize database (use in-memory for now, or pass dbPath as parameter)
|
|
14
|
+
const db = await initDatabase(':memory:');
|
|
15
|
+
const dbSync = new DatabaseSync(db);
|
|
16
|
+
// Scan filesystem for markdown files
|
|
17
|
+
const chapterRecords = [];
|
|
18
|
+
let scanned = 0;
|
|
19
|
+
function scanDirectory(dir) {
|
|
20
|
+
try {
|
|
21
|
+
const entries = readdirSync(dir);
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const fullPath = join(dir, entry);
|
|
24
|
+
const stat = statSync(fullPath);
|
|
25
|
+
if (stat.isDirectory()) {
|
|
26
|
+
scanDirectory(fullPath);
|
|
27
|
+
}
|
|
28
|
+
else if (entry.endsWith('.md')) {
|
|
29
|
+
try {
|
|
30
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
31
|
+
const { metadata } = parseMarkdown(content);
|
|
32
|
+
// Extract path info for fallback
|
|
33
|
+
const relativePath = fullPath.replace(contentPath, '').replace(/^\//, '');
|
|
34
|
+
const pathParts = relativePath.split('/');
|
|
35
|
+
// Use metadata or infer from path
|
|
36
|
+
const arc = metadata.arc || pathParts[0] || 'unknown';
|
|
37
|
+
const episode = metadata.episode || extractEpisodeFromPath(pathParts[1]) || 1;
|
|
38
|
+
const chapter = metadata.chapter || extractChapterFromFilename(entry) || 1;
|
|
39
|
+
const pov = metadata.pov || 'unknown';
|
|
40
|
+
chapterRecords.push({
|
|
41
|
+
chapterId: crypto.randomUUID(),
|
|
42
|
+
timeline,
|
|
43
|
+
arc,
|
|
44
|
+
episode,
|
|
45
|
+
chapter,
|
|
46
|
+
pov,
|
|
47
|
+
title: metadata.title,
|
|
48
|
+
summary: metadata.summary,
|
|
49
|
+
location: metadata.location,
|
|
50
|
+
filePath: fullPath,
|
|
51
|
+
});
|
|
52
|
+
scanned++;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.warn(`Failed to parse ${fullPath}:`, error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.warn(`Failed to scan directory ${dir}:`, error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
scanDirectory(contentPath);
|
|
65
|
+
// Sync to database
|
|
66
|
+
const syncStats = await dbSync.syncChapters(chapterRecords);
|
|
67
|
+
return {
|
|
68
|
+
scanned,
|
|
69
|
+
added: syncStats.chapters, // New chapters added
|
|
70
|
+
updated: 0, // TODO: Track updates
|
|
71
|
+
deleted: 0, // TODO: Track deletions
|
|
72
|
+
timelines: syncStats.timelines,
|
|
73
|
+
arcs: syncStats.arcs,
|
|
74
|
+
episodes: syncStats.episodes,
|
|
75
|
+
chapters: syncStats.chapters,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function extractEpisodeFromPath(pathSegment) {
|
|
79
|
+
if (!pathSegment)
|
|
80
|
+
return null;
|
|
81
|
+
// Match patterns like "ep01-title", "episode-1", "01-title"
|
|
82
|
+
const match = pathSegment.match(/(?:ep|episode)?[-_]?(\d+)/i);
|
|
83
|
+
return match ? parseInt(match[1], 10) : null;
|
|
84
|
+
}
|
|
85
|
+
function extractChapterFromFilename(filename) {
|
|
86
|
+
// Match patterns like "ep01-ch001-title.md", "chapter-1.md", "001-title.md"
|
|
87
|
+
const match = filename.match(/(?:ch|chapter)?[-_]?(\d+)/i);
|
|
88
|
+
return match ? parseInt(match[1], 10) : null;
|
|
89
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const ragContextSchema: z.ZodObject<{
|
|
3
|
+
timeline: z.ZodString;
|
|
4
|
+
query: z.ZodString;
|
|
5
|
+
maxChapters: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
6
|
+
characters: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7
|
+
allCharacters: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
8
|
+
arc: z.ZodOptional<z.ZodString>;
|
|
9
|
+
pov: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type RagContextInput = z.infer<typeof ragContextSchema>;
|
|
12
|
+
export interface RagContextOutput {
|
|
13
|
+
chapters: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
chapterId: string;
|
|
16
|
+
fullContent: string;
|
|
17
|
+
characters: string[];
|
|
18
|
+
metadata: {
|
|
19
|
+
arc?: string;
|
|
20
|
+
episode?: number;
|
|
21
|
+
chapter?: number;
|
|
22
|
+
pov?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
location?: string;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
};
|
|
27
|
+
score: number;
|
|
28
|
+
source: 'graphrag' | 'vector';
|
|
29
|
+
}>;
|
|
30
|
+
totalChapters: number;
|
|
31
|
+
searchTime: number;
|
|
32
|
+
contextLength: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function ragContext(input: RagContextInput): Promise<RagContextOutput>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { initDatabase } from '../database/index.js';
|
|
3
|
+
import { createHybridRAG } from '../rag/index.js';
|
|
4
|
+
export const ragContextSchema = z.object({
|
|
5
|
+
timeline: z.string().min(1, 'Timeline name is required').describe('Timeline name'),
|
|
6
|
+
query: z.string().min(1, 'Query is required').describe('Search query'),
|
|
7
|
+
maxChapters: z.number().optional().default(5).describe('Maximum number of chapters to return'),
|
|
8
|
+
characters: z.array(z.string()).optional().describe('Filter by character names'),
|
|
9
|
+
allCharacters: z
|
|
10
|
+
.boolean()
|
|
11
|
+
.optional()
|
|
12
|
+
.default(false)
|
|
13
|
+
.describe('Require all characters (AND) vs any (OR)'),
|
|
14
|
+
arc: z.string().optional().describe('Filter by arc'),
|
|
15
|
+
pov: z.string().optional().describe('Filter by point of view'),
|
|
16
|
+
});
|
|
17
|
+
export async function ragContext(input) {
|
|
18
|
+
const { timeline, query, maxChapters, characters, allCharacters, arc, pov } = ragContextSchema.parse(input);
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
// Initialize database and RAG system
|
|
21
|
+
const db = await initDatabase(':memory:');
|
|
22
|
+
const rag = createHybridRAG(db);
|
|
23
|
+
// Perform search to get relevant chapters
|
|
24
|
+
const searchResults = await rag.search(query, {
|
|
25
|
+
topK: maxChapters,
|
|
26
|
+
characters,
|
|
27
|
+
allCharacters,
|
|
28
|
+
arc,
|
|
29
|
+
pov,
|
|
30
|
+
useGraphRAG: true,
|
|
31
|
+
});
|
|
32
|
+
const searchTime = Date.now() - startTime;
|
|
33
|
+
// Transform results to include full content for AI context
|
|
34
|
+
const chapters = searchResults.map((result) => ({
|
|
35
|
+
id: result.id,
|
|
36
|
+
chapterId: result.chapterId,
|
|
37
|
+
fullContent: result.content, // Full chapter content for AI context
|
|
38
|
+
characters: result.characters,
|
|
39
|
+
metadata: result.metadata,
|
|
40
|
+
score: result.score,
|
|
41
|
+
source: result.source,
|
|
42
|
+
}));
|
|
43
|
+
// Calculate total context length
|
|
44
|
+
const contextLength = chapters.reduce((total, chapter) => total + chapter.fullContent.length, 0);
|
|
45
|
+
return {
|
|
46
|
+
chapters,
|
|
47
|
+
totalChapters: chapters.length,
|
|
48
|
+
searchTime,
|
|
49
|
+
contextLength,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const ragSearchSchema: z.ZodObject<{
|
|
3
|
+
timeline: z.ZodString;
|
|
4
|
+
query: z.ZodString;
|
|
5
|
+
topK: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
6
|
+
characters: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7
|
+
allCharacters: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
8
|
+
arc: z.ZodOptional<z.ZodString>;
|
|
9
|
+
pov: z.ZodOptional<z.ZodString>;
|
|
10
|
+
useGraphRAG: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export type RagSearchInput = z.infer<typeof ragSearchSchema>;
|
|
13
|
+
export interface RagSearchOutput {
|
|
14
|
+
results: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
chapterId: string;
|
|
17
|
+
content: string;
|
|
18
|
+
characters: string[];
|
|
19
|
+
metadata: {
|
|
20
|
+
arc?: string;
|
|
21
|
+
episode?: number;
|
|
22
|
+
chapter?: number;
|
|
23
|
+
pov?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
location?: string;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
score: number;
|
|
29
|
+
source: 'graphrag' | 'vector';
|
|
30
|
+
}>;
|
|
31
|
+
totalResults: number;
|
|
32
|
+
searchTime: number;
|
|
33
|
+
source: 'graphrag' | 'vector' | 'hybrid';
|
|
34
|
+
}
|
|
35
|
+
export declare function ragSearch(input: RagSearchInput): Promise<RagSearchOutput>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { initDatabase } from '../database/index.js';
|
|
3
|
+
import { createHybridRAG } from '../rag/index.js';
|
|
4
|
+
export const ragSearchSchema = z.object({
|
|
5
|
+
timeline: z.string().min(1, 'Timeline name is required').describe('Timeline name'),
|
|
6
|
+
query: z.string().min(1, 'Query is required').describe('Search query'),
|
|
7
|
+
topK: z.number().optional().default(10).describe('Maximum number of results'),
|
|
8
|
+
characters: z.array(z.string()).optional().describe('Filter by character names'),
|
|
9
|
+
allCharacters: z
|
|
10
|
+
.boolean()
|
|
11
|
+
.optional()
|
|
12
|
+
.default(false)
|
|
13
|
+
.describe('Require all characters (AND) vs any (OR)'),
|
|
14
|
+
arc: z.string().optional().describe('Filter by arc'),
|
|
15
|
+
pov: z.string().optional().describe('Filter by point of view'),
|
|
16
|
+
useGraphRAG: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.optional()
|
|
19
|
+
.default(true)
|
|
20
|
+
.describe('Use GraphRAG (true) or vector search only (false)'),
|
|
21
|
+
});
|
|
22
|
+
export async function ragSearch(input) {
|
|
23
|
+
const { timeline, query, topK, characters, allCharacters, arc, pov, useGraphRAG } = ragSearchSchema.parse(input);
|
|
24
|
+
const startTime = Date.now();
|
|
25
|
+
// Initialize database and RAG system
|
|
26
|
+
const db = await initDatabase(':memory:');
|
|
27
|
+
const rag = createHybridRAG(db);
|
|
28
|
+
// Perform search
|
|
29
|
+
const results = await rag.search(query, {
|
|
30
|
+
topK,
|
|
31
|
+
characters,
|
|
32
|
+
allCharacters,
|
|
33
|
+
arc,
|
|
34
|
+
pov,
|
|
35
|
+
useGraphRAG,
|
|
36
|
+
});
|
|
37
|
+
const searchTime = Date.now() - startTime;
|
|
38
|
+
// Determine primary source
|
|
39
|
+
let primarySource = 'hybrid';
|
|
40
|
+
if (results.length > 0) {
|
|
41
|
+
const sources = new Set(results.map((r) => r.source));
|
|
42
|
+
if (sources.size === 1) {
|
|
43
|
+
primarySource = Array.from(sources)[0];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
results: results.map((result) => ({
|
|
48
|
+
id: result.id,
|
|
49
|
+
chapterId: result.chapterId,
|
|
50
|
+
content: result.content,
|
|
51
|
+
characters: result.characters,
|
|
52
|
+
metadata: result.metadata,
|
|
53
|
+
score: result.score,
|
|
54
|
+
source: result.source,
|
|
55
|
+
})),
|
|
56
|
+
totalResults: results.length,
|
|
57
|
+
searchTime,
|
|
58
|
+
source: primarySource,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const wordsCountSchema: z.ZodObject<{
|
|
3
|
+
filePath: z.ZodString;
|
|
4
|
+
detailed: z.ZodOptional<z.ZodBoolean>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export type WordsCountInput = z.infer<typeof wordsCountSchema>;
|
|
7
|
+
export interface WordsCountOutput {
|
|
8
|
+
words: number;
|
|
9
|
+
characters: number;
|
|
10
|
+
charactersNoSpaces: number;
|
|
11
|
+
readingTimeMinutes: number;
|
|
12
|
+
sentences?: number;
|
|
13
|
+
paragraphs?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function wordsCount(input: WordsCountInput): Promise<WordsCountOutput>;
|