@iflow-mcp/apple-rag-mcp 4.6.2
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/.github/workflows/release.yml +62 -0
- package/.releaserc.json +38 -0
- package/CHANGELOG.md +161 -0
- package/README.md +114 -0
- package/README.zh-CN.md +119 -0
- package/apple-rag-mcp_process.log +8 -0
- package/biome.json +59 -0
- package/dist/src/auth/auth-middleware.d.ts +26 -0
- package/dist/src/auth/auth-middleware.d.ts.map +1 -0
- package/dist/src/auth/auth-middleware.js +77 -0
- package/dist/src/auth/auth-middleware.js.map +1 -0
- package/dist/src/auth/token-validator.d.ts +22 -0
- package/dist/src/auth/token-validator.d.ts.map +1 -0
- package/dist/src/auth/token-validator.js +64 -0
- package/dist/src/auth/token-validator.js.map +1 -0
- package/dist/src/mcp/formatters/response-formatter.d.ts +26 -0
- package/dist/src/mcp/formatters/response-formatter.d.ts.map +1 -0
- package/dist/src/mcp/formatters/response-formatter.js +119 -0
- package/dist/src/mcp/formatters/response-formatter.js.map +1 -0
- package/dist/src/mcp/manifest.d.ts +48 -0
- package/dist/src/mcp/manifest.d.ts.map +1 -0
- package/dist/src/mcp/manifest.js +46 -0
- package/dist/src/mcp/manifest.js.map +1 -0
- package/dist/src/mcp/middleware/request-validator.d.ts +48 -0
- package/dist/src/mcp/middleware/request-validator.d.ts.map +1 -0
- package/dist/src/mcp/middleware/request-validator.js +102 -0
- package/dist/src/mcp/middleware/request-validator.js.map +1 -0
- package/dist/src/mcp/protocol-handler.d.ts +70 -0
- package/dist/src/mcp/protocol-handler.d.ts.map +1 -0
- package/dist/src/mcp/protocol-handler.js +285 -0
- package/dist/src/mcp/protocol-handler.js.map +1 -0
- package/dist/src/mcp/tools/fetch-tool.d.ts +18 -0
- package/dist/src/mcp/tools/fetch-tool.d.ts.map +1 -0
- package/dist/src/mcp/tools/fetch-tool.js +76 -0
- package/dist/src/mcp/tools/fetch-tool.js.map +1 -0
- package/dist/src/mcp/tools/search-tool.d.ts +20 -0
- package/dist/src/mcp/tools/search-tool.d.ts.map +1 -0
- package/dist/src/mcp/tools/search-tool.js +86 -0
- package/dist/src/mcp/tools/search-tool.js.map +1 -0
- package/dist/src/services/database.d.ts +37 -0
- package/dist/src/services/database.d.ts.map +1 -0
- package/dist/src/services/database.js +166 -0
- package/dist/src/services/database.js.map +1 -0
- package/dist/src/services/deepinfra-base.d.ts +22 -0
- package/dist/src/services/deepinfra-base.d.ts.map +1 -0
- package/dist/src/services/deepinfra-base.js +55 -0
- package/dist/src/services/deepinfra-base.js.map +1 -0
- package/dist/src/services/embedding.d.ts +44 -0
- package/dist/src/services/embedding.d.ts.map +1 -0
- package/dist/src/services/embedding.js +61 -0
- package/dist/src/services/embedding.js.map +1 -0
- package/dist/src/services/index.d.ts +10 -0
- package/dist/src/services/index.d.ts.map +1 -0
- package/dist/src/services/index.js +52 -0
- package/dist/src/services/index.js.map +1 -0
- package/dist/src/services/ip-authentication.d.ts +12 -0
- package/dist/src/services/ip-authentication.d.ts.map +1 -0
- package/dist/src/services/ip-authentication.js +39 -0
- package/dist/src/services/ip-authentication.js.map +1 -0
- package/dist/src/services/rag.d.ts +35 -0
- package/dist/src/services/rag.d.ts.map +1 -0
- package/dist/src/services/rag.js +106 -0
- package/dist/src/services/rag.js.map +1 -0
- package/dist/src/services/rate-limit.d.ts +27 -0
- package/dist/src/services/rate-limit.d.ts.map +1 -0
- package/dist/src/services/rate-limit.js +91 -0
- package/dist/src/services/rate-limit.js.map +1 -0
- package/dist/src/services/reranker.d.ts +40 -0
- package/dist/src/services/reranker.d.ts.map +1 -0
- package/dist/src/services/reranker.js +97 -0
- package/dist/src/services/reranker.js.map +1 -0
- package/dist/src/services/search-engine.d.ts +89 -0
- package/dist/src/services/search-engine.d.ts.map +1 -0
- package/dist/src/services/search-engine.js +225 -0
- package/dist/src/services/search-engine.js.map +1 -0
- package/dist/src/services/tool-call-logger.d.ts +36 -0
- package/dist/src/services/tool-call-logger.d.ts.map +1 -0
- package/dist/src/services/tool-call-logger.js +34 -0
- package/dist/src/services/tool-call-logger.js.map +1 -0
- package/dist/src/types/env.d.ts +18 -0
- package/dist/src/types/env.d.ts.map +1 -0
- package/dist/src/types/env.js +2 -0
- package/dist/src/types/env.js.map +1 -0
- package/dist/src/types/index.d.ts +145 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +6 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/utils/d1-utils.d.ts +6 -0
- package/dist/src/utils/d1-utils.d.ts.map +1 -0
- package/dist/src/utils/d1-utils.js +29 -0
- package/dist/src/utils/d1-utils.js.map +1 -0
- package/dist/src/utils/logger.d.ts +11 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +26 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/query-cleaner.d.ts +20 -0
- package/dist/src/utils/query-cleaner.d.ts.map +1 -0
- package/dist/src/utils/query-cleaner.js +117 -0
- package/dist/src/utils/query-cleaner.js.map +1 -0
- package/dist/src/utils/request-info.d.ts +18 -0
- package/dist/src/utils/request-info.d.ts.map +1 -0
- package/dist/src/utils/request-info.js +32 -0
- package/dist/src/utils/request-info.js.map +1 -0
- package/dist/src/utils/telegram-notifier.d.ts +4 -0
- package/dist/src/utils/telegram-notifier.d.ts.map +1 -0
- package/dist/src/utils/telegram-notifier.js +33 -0
- package/dist/src/utils/telegram-notifier.js.map +1 -0
- package/dist/src/utils/url-processor.d.ts +15 -0
- package/dist/src/utils/url-processor.d.ts.map +1 -0
- package/dist/src/utils/url-processor.js +54 -0
- package/dist/src/utils/url-processor.js.map +1 -0
- package/dist/src/worker.d.ts +15 -0
- package/dist/src/worker.d.ts.map +1 -0
- package/dist/src/worker.js +136 -0
- package/dist/src/worker.js.map +1 -0
- package/migrations/schema.sql +155 -0
- package/package.json +49 -0
- package/scripts/semantic-release-server-json.js +34 -0
- package/server.json +25 -0
- package/src/auth/auth-middleware.ts +104 -0
- package/src/auth/token-validator.ts +96 -0
- package/src/mcp/formatters/response-formatter.ts +157 -0
- package/src/mcp/manifest.ts +48 -0
- package/src/mcp/middleware/request-validator.ts +135 -0
- package/src/mcp/protocol-handler.ts +412 -0
- package/src/mcp/tools/fetch-tool.ts +146 -0
- package/src/mcp/tools/search-tool.ts +165 -0
- package/src/services/database.ts +202 -0
- package/src/services/deepinfra-base.ts +81 -0
- package/src/services/embedding.ts +96 -0
- package/src/services/index.ts +59 -0
- package/src/services/ip-authentication.ts +62 -0
- package/src/services/rag.ts +158 -0
- package/src/services/rate-limit.ts +141 -0
- package/src/services/reranker.ts +171 -0
- package/src/services/search-engine.ts +333 -0
- package/src/services/tool-call-logger.ts +98 -0
- package/src/types/env.ts +22 -0
- package/src/types/index.ts +189 -0
- package/src/utils/d1-utils.ts +45 -0
- package/src/utils/logger.ts +33 -0
- package/src/utils/query-cleaner.ts +151 -0
- package/src/utils/request-info.ts +47 -0
- package/src/utils/telegram-notifier.ts +47 -0
- package/src/utils/url-processor.ts +65 -0
- package/src/worker.ts +176 -0
- package/tsconfig.json +32 -0
- package/wrangler.toml.example +39 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Database Service with pgvector
|
|
3
|
+
* Optimized for Cloudflare Workers with external database connection
|
|
4
|
+
*/
|
|
5
|
+
import postgres from "postgres";
|
|
6
|
+
import type { AppConfig, SearchOptions, SearchResult } from "../types/index.js";
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
|
|
9
|
+
export class DatabaseService {
|
|
10
|
+
private sql: ReturnType<typeof postgres>;
|
|
11
|
+
constructor(config: AppConfig) {
|
|
12
|
+
// Direct PostgreSQL connection - no checks, no logs
|
|
13
|
+
this.sql = postgres({
|
|
14
|
+
host: config.RAG_DB_HOST,
|
|
15
|
+
port: config.RAG_DB_PORT,
|
|
16
|
+
database: config.RAG_DB_DATABASE,
|
|
17
|
+
username: config.RAG_DB_USER,
|
|
18
|
+
password: config.RAG_DB_PASSWORD,
|
|
19
|
+
ssl: config.RAG_DB_SSLMODE === "require",
|
|
20
|
+
max: 5,
|
|
21
|
+
idle_timeout: 60000,
|
|
22
|
+
connect_timeout: 10000,
|
|
23
|
+
prepare: true,
|
|
24
|
+
connection: {
|
|
25
|
+
application_name: "apple-rag-mcp",
|
|
26
|
+
},
|
|
27
|
+
transform: {
|
|
28
|
+
undefined: null,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initialize database - no checks, trust ready state
|
|
35
|
+
*/
|
|
36
|
+
async initialize(): Promise<void> {
|
|
37
|
+
// Database assumed ready - no checks, no logs, instant return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Semantic search using vector similarity
|
|
42
|
+
*/
|
|
43
|
+
async semanticSearch(
|
|
44
|
+
queryEmbedding: number[],
|
|
45
|
+
options: SearchOptions = {}
|
|
46
|
+
): Promise<SearchResult[]> {
|
|
47
|
+
const { resultCount = 5 } = options;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const results = await this.sql`
|
|
51
|
+
SELECT id, url, title, content, chunk_index, total_chunks
|
|
52
|
+
FROM chunks
|
|
53
|
+
WHERE embedding IS NOT NULL
|
|
54
|
+
ORDER BY embedding <=> ${JSON.stringify(queryEmbedding)}::halfvec
|
|
55
|
+
LIMIT ${resultCount}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
return results.map((row) => ({
|
|
59
|
+
id: row.id as string,
|
|
60
|
+
url: row.url as string,
|
|
61
|
+
title: row.title as string | null,
|
|
62
|
+
content: row.content as string,
|
|
63
|
+
contentLength: (row.content as string).length,
|
|
64
|
+
chunk_index: row.chunk_index as number,
|
|
65
|
+
total_chunks: row.total_chunks as number,
|
|
66
|
+
}));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error(
|
|
69
|
+
`Database semantic search failed (operation: semantic_search, embeddingDimensions: ${queryEmbedding.length}, resultCount: ${resultCount}): ${String(error)}`
|
|
70
|
+
);
|
|
71
|
+
throw new Error(`Vector search failed: ${error}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Keyword search optimized for Apple Developer Documentation
|
|
77
|
+
* Uses PostgreSQL 'simple' configuration for precise matching of technical terms,
|
|
78
|
+
* API names, and special symbols (@State, SecItemAdd, etc.)
|
|
79
|
+
*/
|
|
80
|
+
async keywordSearch(
|
|
81
|
+
query: string,
|
|
82
|
+
options: SearchOptions = {}
|
|
83
|
+
): Promise<SearchResult[]> {
|
|
84
|
+
const { resultCount = 5 } = options;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const results = await this.sql`
|
|
88
|
+
SELECT id, url, title, content, chunk_index, total_chunks
|
|
89
|
+
FROM chunks
|
|
90
|
+
WHERE to_tsvector('simple', COALESCE(title, '') || ' ' || content)
|
|
91
|
+
@@ plainto_tsquery('simple', ${query})
|
|
92
|
+
LIMIT ${resultCount}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
return results.map((row) => ({
|
|
96
|
+
id: row.id as string,
|
|
97
|
+
url: row.url as string,
|
|
98
|
+
title: row.title as string | null,
|
|
99
|
+
content: row.content as string,
|
|
100
|
+
contentLength: (row.content as string).length,
|
|
101
|
+
chunk_index: row.chunk_index as number,
|
|
102
|
+
total_chunks: row.total_chunks as number,
|
|
103
|
+
}));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.error(
|
|
106
|
+
`Database keyword search failed (operation: keyword_search, query: ${query.substring(0, 50)}, resultCount: ${resultCount}): ${String(error)}`
|
|
107
|
+
);
|
|
108
|
+
throw new Error(`Keyword search failed: ${error}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Normalize URL for flexible matching
|
|
114
|
+
*/
|
|
115
|
+
private normalizeUrl(url: string): string {
|
|
116
|
+
// Remove trailing slash
|
|
117
|
+
let normalized = url.replace(/\/$/, "");
|
|
118
|
+
|
|
119
|
+
// Ensure https:// prefix
|
|
120
|
+
if (
|
|
121
|
+
!normalized.startsWith("http://") &&
|
|
122
|
+
!normalized.startsWith("https://")
|
|
123
|
+
) {
|
|
124
|
+
normalized = `https://${normalized}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Convert http:// to https://
|
|
128
|
+
if (normalized.startsWith("http://")) {
|
|
129
|
+
normalized = normalized.replace("http://", "https://");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return normalized;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get page content by URL from pages table with flexible matching
|
|
137
|
+
*/
|
|
138
|
+
async getPageByUrl(url: string): Promise<{
|
|
139
|
+
id: string;
|
|
140
|
+
url: string;
|
|
141
|
+
title: string | null;
|
|
142
|
+
content: string;
|
|
143
|
+
} | null> {
|
|
144
|
+
const normalizedUrl = this.normalizeUrl(url);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// Try exact match first
|
|
148
|
+
let results = await this.sql`
|
|
149
|
+
SELECT id, url, title, content
|
|
150
|
+
FROM pages
|
|
151
|
+
WHERE url = ${normalizedUrl}
|
|
152
|
+
LIMIT 1
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
// If no exact match, try flexible matching
|
|
156
|
+
if (results.length === 0) {
|
|
157
|
+
// Try with/without trailing slash
|
|
158
|
+
const alternativeUrl = normalizedUrl.endsWith("/")
|
|
159
|
+
? normalizedUrl.slice(0, -1)
|
|
160
|
+
: `${normalizedUrl}/`;
|
|
161
|
+
|
|
162
|
+
results = await this.sql`
|
|
163
|
+
SELECT id, url, title, content
|
|
164
|
+
FROM pages
|
|
165
|
+
WHERE url = ${alternativeUrl}
|
|
166
|
+
LIMIT 1
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (results.length === 0) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const row = results[0];
|
|
175
|
+
return {
|
|
176
|
+
id: row.id as string,
|
|
177
|
+
url: row.url as string,
|
|
178
|
+
title: row.title as string | null,
|
|
179
|
+
content: row.content as string,
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.error(
|
|
183
|
+
`Database page lookup failed (operation: page_lookup, url: ${url.substring(0, 100)}, normalizedUrl: ${this.normalizeUrl(url).substring(0, 100)}): ${String(error)}`
|
|
184
|
+
);
|
|
185
|
+
throw new Error(`Page lookup failed: ${error}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Close database connection
|
|
191
|
+
*/
|
|
192
|
+
async close(): Promise<void> {
|
|
193
|
+
try {
|
|
194
|
+
await this.sql.end();
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.error(
|
|
197
|
+
`Database close failed (operation: database_close): ${String(error)}`
|
|
198
|
+
);
|
|
199
|
+
// Don't re-throw - closing errors are not critical
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepInfra client utilities (config + base service)
|
|
3
|
+
* Minimal single-key client with retry support.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
|
|
8
|
+
export const DEEPINFRA_CONFIG = {
|
|
9
|
+
BASE_URL: "https://api.deepinfra.com",
|
|
10
|
+
TIMEOUT_MS: 5_000,
|
|
11
|
+
USER_AGENT: "Apple-RAG-MCP/2.0.0",
|
|
12
|
+
EMBEDDING_MODEL: "Qwen/Qwen3-Embedding-4B",
|
|
13
|
+
RERANKER_MODEL_PRIMARY: "Qwen/Qwen3-Reranker-8B",
|
|
14
|
+
RERANKER_MODEL_FALLBACK: "Qwen/Qwen3-Reranker-4B",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export abstract class DeepInfraService<TRequest, TResponse, TResult> {
|
|
18
|
+
protected abstract readonly endpoint: string;
|
|
19
|
+
private readonly apiKey: string;
|
|
20
|
+
|
|
21
|
+
constructor(apiKey: string) {
|
|
22
|
+
if (!apiKey) throw new Error("DEEPINFRA_API_KEY is required");
|
|
23
|
+
this.apiKey = apiKey;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protected async call(
|
|
27
|
+
input: TRequest,
|
|
28
|
+
operationName: string
|
|
29
|
+
): Promise<TResult> {
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
const payload = this.buildPayload(input);
|
|
32
|
+
let lastError!: Error;
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < 3; i++) {
|
|
35
|
+
try {
|
|
36
|
+
const json = await this.singleRequest(this.endpoint, payload);
|
|
37
|
+
logger.info(
|
|
38
|
+
`${operationName} completed (${((Date.now() - startTime) / 1000).toFixed(1)}s)`
|
|
39
|
+
);
|
|
40
|
+
return this.processResponse(json, input);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
logger.error(
|
|
47
|
+
`${operationName} failed after 3 attempts (${((Date.now() - startTime) / 1000).toFixed(1)}s): ${lastError.message}`
|
|
48
|
+
);
|
|
49
|
+
throw lastError;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected async singleRequest(
|
|
53
|
+
endpoint: string,
|
|
54
|
+
payload: unknown
|
|
55
|
+
): Promise<TResponse> {
|
|
56
|
+
const res = await fetch(`${DEEPINFRA_CONFIG.BASE_URL}${endpoint}`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"User-Agent": DEEPINFRA_CONFIG.USER_AGENT,
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify(payload),
|
|
64
|
+
signal: AbortSignal.timeout(DEEPINFRA_CONFIG.TIMEOUT_MS),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`API error ${res.status}: ${await res.text().catch(() => "")}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (await res.json()) as TResponse;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
protected abstract buildPayload(input: TRequest): unknown;
|
|
77
|
+
protected abstract processResponse(
|
|
78
|
+
response: TResponse,
|
|
79
|
+
input: TRequest
|
|
80
|
+
): TResult;
|
|
81
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern Embedding Service - MCP Optimized
|
|
3
|
+
* DeepInfra API integration (single provider, no fallback)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EmbeddingService as IEmbeddingService } from "../types/index.js";
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
import { DEEPINFRA_CONFIG, DeepInfraService } from "./deepinfra-base.js";
|
|
9
|
+
|
|
10
|
+
interface EmbeddingInput {
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface EmbeddingPayload {
|
|
15
|
+
model: "Qwen/Qwen3-Embedding-4B";
|
|
16
|
+
input: string;
|
|
17
|
+
encoding_format: "float";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface EmbeddingResponse {
|
|
21
|
+
data: Array<{
|
|
22
|
+
embedding: number[];
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class EmbeddingService
|
|
27
|
+
extends DeepInfraService<EmbeddingInput, EmbeddingResponse, number[]>
|
|
28
|
+
implements IEmbeddingService
|
|
29
|
+
{
|
|
30
|
+
protected readonly endpoint = "/v1/openai/embeddings";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create embedding (single provider)
|
|
34
|
+
*/
|
|
35
|
+
async createEmbedding(text: string): Promise<number[]> {
|
|
36
|
+
if (!text?.trim()) {
|
|
37
|
+
throw new Error("Text cannot be empty for embedding generation");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const input: EmbeddingInput = { text: text.trim() };
|
|
41
|
+
return this.call(input, "Embedding generation");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build API payload from request
|
|
46
|
+
*/
|
|
47
|
+
protected buildPayload(input: EmbeddingInput): EmbeddingPayload {
|
|
48
|
+
return {
|
|
49
|
+
model: DEEPINFRA_CONFIG.EMBEDDING_MODEL,
|
|
50
|
+
input: input.text,
|
|
51
|
+
encoding_format: "float",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Process API response and return normalized embedding
|
|
57
|
+
*/
|
|
58
|
+
protected processResponse(
|
|
59
|
+
response: EmbeddingResponse,
|
|
60
|
+
_input: EmbeddingInput
|
|
61
|
+
): number[] {
|
|
62
|
+
const embedding = this.extractEmbedding(response);
|
|
63
|
+
return this.normalizeL2(embedding);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract embedding from API response
|
|
68
|
+
*/
|
|
69
|
+
private extractEmbedding(response: EmbeddingResponse): number[] {
|
|
70
|
+
const embedding = response.data?.[0]?.embedding;
|
|
71
|
+
|
|
72
|
+
if (!embedding || !Array.isArray(embedding)) {
|
|
73
|
+
throw new Error("No embedding data received from DeepInfra API");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (embedding.length === 0) {
|
|
77
|
+
throw new Error("Empty embedding received from DeepInfra API");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return embedding;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* L2 normalization for optimal vector search performance
|
|
85
|
+
*/
|
|
86
|
+
private normalizeL2(embedding: number[]): number[] {
|
|
87
|
+
const norm = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
|
|
88
|
+
|
|
89
|
+
if (norm === 0) {
|
|
90
|
+
logger.warn("Zero norm embedding detected, returning original");
|
|
91
|
+
return [...embedding];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return embedding.map((val) => val / norm);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern Service Factory - Cloudflare Worker Native
|
|
3
|
+
* Creates and configures all services with optimal performance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AuthMiddleware } from "../auth/auth-middleware.js";
|
|
7
|
+
import type { AppConfig, Services, WorkerEnv } from "../types/index.js";
|
|
8
|
+
import { RAGService } from "./rag.js";
|
|
9
|
+
import { RateLimitService } from "./rate-limit.js";
|
|
10
|
+
import { ToolCallLogger } from "./tool-call-logger.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create all services from Worker environment with validation
|
|
14
|
+
*/
|
|
15
|
+
export async function createServices(env: WorkerEnv): Promise<Services> {
|
|
16
|
+
try {
|
|
17
|
+
// Convert Worker env to app config
|
|
18
|
+
const config = createAppConfig(env);
|
|
19
|
+
|
|
20
|
+
// Initialize services
|
|
21
|
+
const auth = new AuthMiddleware(env.DB);
|
|
22
|
+
const rag = new RAGService(config, env);
|
|
23
|
+
const rateLimit = new RateLimitService(env.DB);
|
|
24
|
+
const logger = new ToolCallLogger(env.DB);
|
|
25
|
+
|
|
26
|
+
// Initialize async services
|
|
27
|
+
await rag.initialize();
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
rag,
|
|
31
|
+
auth,
|
|
32
|
+
database: rag.database,
|
|
33
|
+
embedding: rag.embedding,
|
|
34
|
+
rateLimit,
|
|
35
|
+
logger,
|
|
36
|
+
};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Import logger here to avoid circular dependency
|
|
39
|
+
const { logger } = await import("../utils/logger.js");
|
|
40
|
+
logger.error(
|
|
41
|
+
`Service initialization failed: ${error instanceof Error ? error.message : String(error)}`
|
|
42
|
+
);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert Worker environment to app configuration
|
|
49
|
+
*/
|
|
50
|
+
function createAppConfig(env: WorkerEnv): AppConfig {
|
|
51
|
+
return {
|
|
52
|
+
RAG_DB_HOST: env.RAG_DB_HOST,
|
|
53
|
+
RAG_DB_PORT: parseInt(env.RAG_DB_PORT, 10),
|
|
54
|
+
RAG_DB_DATABASE: env.RAG_DB_DATABASE,
|
|
55
|
+
RAG_DB_USER: env.RAG_DB_USER,
|
|
56
|
+
RAG_DB_PASSWORD: env.RAG_DB_PASSWORD,
|
|
57
|
+
RAG_DB_SSLMODE: env.RAG_DB_SSLMODE,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IP Authentication Service with D1 timeout protection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { UserTokenData } from "../auth/token-validator.js";
|
|
6
|
+
import { backgroundD1Write, withD1Timeout } from "../utils/d1-utils.js";
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
|
|
9
|
+
interface UserRecord {
|
|
10
|
+
user_id: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class IPAuthenticationService {
|
|
16
|
+
constructor(private d1: D1Database) {}
|
|
17
|
+
|
|
18
|
+
async checkIPAuthentication(clientIP: string): Promise<UserTokenData | null> {
|
|
19
|
+
const user = await withD1Timeout(
|
|
20
|
+
() => this.queryIP(clientIP),
|
|
21
|
+
null,
|
|
22
|
+
"ip_auth"
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (!user) return null;
|
|
26
|
+
|
|
27
|
+
backgroundD1Write(
|
|
28
|
+
logger.getContext(),
|
|
29
|
+
() => this.updateLastUsed(clientIP, user.user_id),
|
|
30
|
+
"ip_last_used"
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
userId: user.user_id,
|
|
35
|
+
email: user.email || "ip-authenticated",
|
|
36
|
+
name: user.name || "IP User",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async queryIP(clientIP: string): Promise<UserRecord | null> {
|
|
41
|
+
const result = await this.d1
|
|
42
|
+
.prepare(
|
|
43
|
+
`SELECT uai.user_id, u.email, u.name
|
|
44
|
+
FROM user_authorized_ips uai
|
|
45
|
+
JOIN users u ON uai.user_id = u.id
|
|
46
|
+
WHERE uai.ip_address = ?`
|
|
47
|
+
)
|
|
48
|
+
.bind(clientIP)
|
|
49
|
+
.all();
|
|
50
|
+
|
|
51
|
+
return (result.results?.[0] as unknown as UserRecord | undefined) ?? null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async updateLastUsed(ipAddress: string, userId: string): Promise<void> {
|
|
55
|
+
await this.d1
|
|
56
|
+
.prepare(
|
|
57
|
+
"UPDATE user_authorized_ips SET last_used_at = ? WHERE ip_address = ? AND user_id = ?"
|
|
58
|
+
)
|
|
59
|
+
.bind(new Date().toISOString(), ipAddress, userId)
|
|
60
|
+
.run();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern RAG Service - Cloudflare Worker Native
|
|
3
|
+
* Optimized for edge computing with zero-dependency architecture
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
AppConfig,
|
|
8
|
+
RAGQuery,
|
|
9
|
+
RAGResult,
|
|
10
|
+
SearchResult,
|
|
11
|
+
WorkerEnv,
|
|
12
|
+
} from "../types/index.js";
|
|
13
|
+
import { logger } from "../utils/logger.js";
|
|
14
|
+
import { DatabaseService } from "./database.js";
|
|
15
|
+
import { EmbeddingService } from "./embedding.js";
|
|
16
|
+
import { RerankerService } from "./reranker.js";
|
|
17
|
+
import { type RankedSearchResult, SearchEngine } from "./search-engine.js";
|
|
18
|
+
|
|
19
|
+
export class RAGService {
|
|
20
|
+
readonly database: DatabaseService;
|
|
21
|
+
readonly embedding: EmbeddingService;
|
|
22
|
+
private readonly reranker: RerankerService;
|
|
23
|
+
private readonly searchEngine: SearchEngine;
|
|
24
|
+
|
|
25
|
+
constructor(config: AppConfig, env: WorkerEnv) {
|
|
26
|
+
this.database = new DatabaseService(config);
|
|
27
|
+
this.embedding = new EmbeddingService(env.DEEPINFRA_API_KEY);
|
|
28
|
+
this.reranker = new RerankerService(env.DEEPINFRA_API_KEY);
|
|
29
|
+
this.searchEngine = new SearchEngine(
|
|
30
|
+
this.database,
|
|
31
|
+
this.embedding,
|
|
32
|
+
this.reranker
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Initialize - no-op since database initialization is removed
|
|
38
|
+
*/
|
|
39
|
+
async initialize(): Promise<void> {
|
|
40
|
+
// No initialization needed - database trusted ready
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Perform RAG query with intelligent processing and detailed timing
|
|
45
|
+
*/
|
|
46
|
+
async query(request: RAGQuery): Promise<RAGResult> {
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
const { query, result_count = 4 } = request;
|
|
49
|
+
|
|
50
|
+
// No started log - only completion with timing
|
|
51
|
+
|
|
52
|
+
// Input validation
|
|
53
|
+
if (!query?.trim()) {
|
|
54
|
+
return this.createErrorResponse(
|
|
55
|
+
query,
|
|
56
|
+
"Query cannot be empty. Please provide a search query to find relevant Apple Developer Documentation.",
|
|
57
|
+
"Try searching for topics like 'SwiftUI navigation', 'iOS app development', or 'API documentation'.",
|
|
58
|
+
startTime
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const trimmedQuery = query.trim();
|
|
63
|
+
if (trimmedQuery.length > 10000) {
|
|
64
|
+
return this.createErrorResponse(
|
|
65
|
+
query,
|
|
66
|
+
"Query is too long. Please limit your query to 10000 characters or less.",
|
|
67
|
+
"Try to make your query more concise and specific.",
|
|
68
|
+
startTime
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Initialize services (if not already initialized)
|
|
74
|
+
await this.initialize();
|
|
75
|
+
|
|
76
|
+
// Execute search
|
|
77
|
+
const resultCount = Math.min(Math.max(result_count, 1), 20);
|
|
78
|
+
|
|
79
|
+
const searchResult = await this.searchEngine.search(trimmedQuery, {
|
|
80
|
+
resultCount,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Format results
|
|
84
|
+
const formattedResults = this.formatResults(searchResult.results);
|
|
85
|
+
const totalTime = Date.now() - startTime;
|
|
86
|
+
|
|
87
|
+
// Log completion with timing
|
|
88
|
+
logger.info(
|
|
89
|
+
`RAG query completed (${(totalTime / 1000).toFixed(1)}s) - results: ${formattedResults.length}, query: ${query.substring(0, 50)}`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
query: trimmedQuery,
|
|
95
|
+
results: formattedResults,
|
|
96
|
+
additionalUrls: searchResult.additionalUrls,
|
|
97
|
+
count: formattedResults.length,
|
|
98
|
+
processing_time_ms: totalTime,
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.error(
|
|
102
|
+
`RAG query failed for query "${trimmedQuery.substring(0, 50)}": ${error instanceof Error ? error.message : "Unknown error"}`
|
|
103
|
+
);
|
|
104
|
+
return this.createErrorResponse(
|
|
105
|
+
trimmedQuery,
|
|
106
|
+
`Search failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
107
|
+
"Please try again with a different query or check your connection.",
|
|
108
|
+
startTime
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Format search results for MCP response
|
|
115
|
+
*/
|
|
116
|
+
private formatResults(
|
|
117
|
+
results: readonly RankedSearchResult[]
|
|
118
|
+
): SearchResult[] {
|
|
119
|
+
return results.map((result) => ({
|
|
120
|
+
id: result.id,
|
|
121
|
+
url: result.url,
|
|
122
|
+
title: result.title,
|
|
123
|
+
content: result.content,
|
|
124
|
+
contentLength: result.content.length,
|
|
125
|
+
chunk_index: result.chunk_index,
|
|
126
|
+
total_chunks: result.total_chunks,
|
|
127
|
+
mergedChunkIndices: result.mergedChunkIndices,
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create standardized error response
|
|
133
|
+
*/
|
|
134
|
+
private createErrorResponse(
|
|
135
|
+
query: string,
|
|
136
|
+
_error: string,
|
|
137
|
+
_suggestion: string,
|
|
138
|
+
startTime: number
|
|
139
|
+
): RAGResult {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
query,
|
|
143
|
+
results: [],
|
|
144
|
+
additionalUrls: [],
|
|
145
|
+
count: 0,
|
|
146
|
+
processing_time_ms: Date.now() - startTime,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Clean up resources
|
|
152
|
+
*/
|
|
153
|
+
async close(): Promise<void> {
|
|
154
|
+
if (this.database) {
|
|
155
|
+
await this.database.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|