@tungpastry/pupperfish-framework 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 +9 -0
- package/dist/answer.d.ts +15 -0
- package/dist/answer.js +86 -0
- package/dist/contracts.d.ts +116 -0
- package/dist/contracts.js +1 -0
- package/dist/embeddings.d.ts +5 -0
- package/dist/embeddings.js +60 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +13 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/normalize.d.ts +7 -0
- package/dist/normalize.js +73 -0
- package/dist/planner.d.ts +4 -0
- package/dist/planner.js +23 -0
- package/dist/runtime.d.ts +38 -0
- package/dist/runtime.js +344 -0
- package/dist/types.d.ts +201 -0
- package/dist/types.js +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZenLog Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/answer.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PupperfishAiProvider, PupperfishRuntimeConfig } from "./contracts.js";
|
|
2
|
+
import type { PupperfishEvidenceItem, PupperfishImageEvidence, PupperfishMemoryEvidence, PupperfishPlannerMode } from "./types.js";
|
|
3
|
+
export declare function composePupperfishAnswer(params: {
|
|
4
|
+
aiProvider: PupperfishAiProvider;
|
|
5
|
+
config: PupperfishRuntimeConfig;
|
|
6
|
+
query: string;
|
|
7
|
+
mode: PupperfishPlannerMode;
|
|
8
|
+
evidence: PupperfishEvidenceItem[];
|
|
9
|
+
memories: PupperfishMemoryEvidence[];
|
|
10
|
+
charts: PupperfishImageEvidence[];
|
|
11
|
+
}): Promise<{
|
|
12
|
+
answer: string;
|
|
13
|
+
assumptions: string[];
|
|
14
|
+
confidence: number;
|
|
15
|
+
}>;
|
package/dist/answer.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
function evidenceLine(item) {
|
|
2
|
+
if (item.kind === "log") {
|
|
3
|
+
return `- [log:${item.entryUid}] ${item.dateText} ${item.timeText} | ${item.activity} | Outcome: ${item.outcome} | Next: ${item.nextAction ?? "(trống)"}`;
|
|
4
|
+
}
|
|
5
|
+
if (item.kind === "summary") {
|
|
6
|
+
return `- [summary:${item.summaryUid}] ${item.summaryDate} ${item.scope} | ${item.summaryText}`;
|
|
7
|
+
}
|
|
8
|
+
if (item.kind === "memory") {
|
|
9
|
+
return `- [memory:${item.memoryUid}] ${item.memoryType} | ${item.memoryText}`;
|
|
10
|
+
}
|
|
11
|
+
return `- [image:${item.imageUid}] ${item.chartLabel} ${item.symbol ?? ""} ${item.timeframe ?? ""}`.trim();
|
|
12
|
+
}
|
|
13
|
+
export async function composePupperfishAnswer(params) {
|
|
14
|
+
const assumptions = [];
|
|
15
|
+
const assistantName = params.config.branding.assistantName || "Pupperfish";
|
|
16
|
+
const productName = params.config.branding.productName || "host app";
|
|
17
|
+
const language = params.config.answerPolicy?.language ?? "tiếng Việt";
|
|
18
|
+
if (params.evidence.length < 1) {
|
|
19
|
+
assumptions.push("Không có bằng chứng khớp mạnh; trả lời theo dữ liệu hạn chế.");
|
|
20
|
+
return {
|
|
21
|
+
answer: `Chưa tìm thấy bằng chứng đủ mạnh trong ${productName}. Bạn có thể ghi thêm dữ liệu hoặc cung cấp phạm vi thời gian cụ thể để truy vấn chính xác hơn.`,
|
|
22
|
+
assumptions,
|
|
23
|
+
confidence: 0.28,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const prompt = [
|
|
27
|
+
`Bạn là ${assistantName} assistant cho ${productName}.`,
|
|
28
|
+
`Mode: ${params.mode}`,
|
|
29
|
+
`User query: ${params.query}`,
|
|
30
|
+
"Yêu cầu:",
|
|
31
|
+
`1) Trả lời ngắn gọn bằng ${language}.`,
|
|
32
|
+
"2) Chỉ dùng dữ kiện từ Evidence.",
|
|
33
|
+
"3) Nếu chưa chắc, nói rõ giả định.",
|
|
34
|
+
"4) Cuối câu trả lời thêm mục Nguồn: [kind:uid] cách nhau bằng dấu phẩy.",
|
|
35
|
+
"Evidence:",
|
|
36
|
+
...params.evidence.map(evidenceLine),
|
|
37
|
+
].join("\n");
|
|
38
|
+
try {
|
|
39
|
+
const generated = await params.aiProvider.generateAnswer(prompt);
|
|
40
|
+
const topScore = params.evidence[0]?.score ?? 0;
|
|
41
|
+
const confidence = Math.max(0.25, Math.min(0.95, topScore));
|
|
42
|
+
return {
|
|
43
|
+
answer: generated.text,
|
|
44
|
+
assumptions,
|
|
45
|
+
confidence,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
assumptions.push("Dùng fallback composer do generate service không phản hồi.");
|
|
50
|
+
const sourceList = params.evidence
|
|
51
|
+
.slice(0, 6)
|
|
52
|
+
.map((item) => {
|
|
53
|
+
if (item.kind === "log") {
|
|
54
|
+
return `[log:${item.entryUid}]`;
|
|
55
|
+
}
|
|
56
|
+
if (item.kind === "summary") {
|
|
57
|
+
return `[summary:${item.summaryUid}]`;
|
|
58
|
+
}
|
|
59
|
+
if (item.kind === "memory") {
|
|
60
|
+
return `[memory:${item.memoryUid}]`;
|
|
61
|
+
}
|
|
62
|
+
return `[image:${item.imageUid}]`;
|
|
63
|
+
})
|
|
64
|
+
.join(", ");
|
|
65
|
+
const keyPoints = params.evidence
|
|
66
|
+
.slice(0, 3)
|
|
67
|
+
.map((item) => {
|
|
68
|
+
if (item.kind === "log") {
|
|
69
|
+
return `- ${item.dateText} ${item.timeText}: ${item.activity} -> ${item.outcome}`;
|
|
70
|
+
}
|
|
71
|
+
if (item.kind === "summary") {
|
|
72
|
+
return `- Summary ${item.summaryDate}/${item.scope}: ${item.summaryText}`;
|
|
73
|
+
}
|
|
74
|
+
if (item.kind === "memory") {
|
|
75
|
+
return `- Memory ${item.memoryType}: ${item.memoryText}`;
|
|
76
|
+
}
|
|
77
|
+
return `- Image ${item.chartLabel} (${item.symbol ?? "n/a"} ${item.timeframe ?? ""})`;
|
|
78
|
+
})
|
|
79
|
+
.join("\n");
|
|
80
|
+
return {
|
|
81
|
+
answer: `Mình đã tổng hợp từ dữ liệu ${productName}:\n${keyPoints}\nNguồn: ${sourceList || "(không có)"}`,
|
|
82
|
+
assumptions,
|
|
83
|
+
confidence: Math.max(0.2, Math.min(0.9, params.evidence[0]?.score ?? 0.3)),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { PupperfishConversationTurn, PupperfishCreateImageInput, PupperfishDeleteImageResult, PupperfishImageEvidence, PupperfishLogDetail, PupperfishLogEvidence, PupperfishLogImageTarget, PupperfishMemoryEvidence, PupperfishPersistedImage, PupperfishPlannerMode, PupperfishStoredImage, PupperfishSummaryEvidence, PupperfishTradeImageItem, PupperfishUpdateTradeImagePayload, PupperfishWorkerCycleResult, QueryLogFilters, QueryMemoryFilters, QuerySummaryFilters, WorkerJobPayload, WorkerJobType } from "./types.js";
|
|
2
|
+
export type PupperfishUploadFile = {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
size: number;
|
|
6
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
7
|
+
};
|
|
8
|
+
export type PupperfishBrandingConfig = {
|
|
9
|
+
assistantName: string;
|
|
10
|
+
productName: string;
|
|
11
|
+
};
|
|
12
|
+
export type PupperfishPlannerKeyword = {
|
|
13
|
+
mode: PupperfishPlannerMode;
|
|
14
|
+
pattern: RegExp;
|
|
15
|
+
};
|
|
16
|
+
export type PupperfishRuntimeConfig = {
|
|
17
|
+
branding: PupperfishBrandingConfig;
|
|
18
|
+
locale?: string;
|
|
19
|
+
plannerKeywords?: PupperfishPlannerKeyword[];
|
|
20
|
+
answerPolicy?: {
|
|
21
|
+
language?: string;
|
|
22
|
+
concise?: boolean;
|
|
23
|
+
};
|
|
24
|
+
limits?: {
|
|
25
|
+
topKDefault?: number;
|
|
26
|
+
topKMax?: number;
|
|
27
|
+
uploadMaxBytes?: number;
|
|
28
|
+
workerBatchLimit?: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export interface PupperfishRepositories {
|
|
32
|
+
searchLogs(query: string, filters: QueryLogFilters, topK: number): Promise<PupperfishLogEvidence[]>;
|
|
33
|
+
searchSummaries(query: string, filters: QuerySummaryFilters, topK: number): Promise<PupperfishSummaryEvidence[]>;
|
|
34
|
+
searchMemories(query: string, filters: QueryMemoryFilters, topK: number): Promise<PupperfishMemoryEvidence[]>;
|
|
35
|
+
searchImages(query: string, topK: number): Promise<PupperfishImageEvidence[]>;
|
|
36
|
+
getLog(entryUid: string): Promise<PupperfishLogDetail | null>;
|
|
37
|
+
getSummary(summaryUid: string): Promise<Record<string, unknown> | null>;
|
|
38
|
+
getMemory(memoryUid: string): Promise<Record<string, unknown> | null>;
|
|
39
|
+
getImage(imageUid: string): Promise<PupperfishStoredImage | null>;
|
|
40
|
+
getSimilarImages(imageUid: string, topK: number): Promise<PupperfishImageEvidence[]>;
|
|
41
|
+
listLogImages(entryUid: string): Promise<PupperfishTradeImageItem[] | null>;
|
|
42
|
+
getLogImageTarget(entryUid: string): Promise<PupperfishLogImageTarget | null>;
|
|
43
|
+
createImageForLog(input: PupperfishCreateImageInput): Promise<PupperfishStoredImage>;
|
|
44
|
+
updateImage(imageUid: string, payload: PupperfishUpdateTradeImagePayload): Promise<PupperfishStoredImage>;
|
|
45
|
+
deleteImage(imageUid: string): Promise<PupperfishDeleteImageResult>;
|
|
46
|
+
recordConversation?(params: {
|
|
47
|
+
convoUid: string;
|
|
48
|
+
userId: string;
|
|
49
|
+
requestUid: string;
|
|
50
|
+
turns: PupperfishConversationTurn[];
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
export interface PupperfishAiProvider {
|
|
54
|
+
embedText?(text: string): Promise<{
|
|
55
|
+
embedding: number[];
|
|
56
|
+
model: string;
|
|
57
|
+
source: string;
|
|
58
|
+
}>;
|
|
59
|
+
embedImage?(bytes: Uint8Array): Promise<{
|
|
60
|
+
embedding: number[];
|
|
61
|
+
model: string;
|
|
62
|
+
source: string;
|
|
63
|
+
}>;
|
|
64
|
+
generateAnswer(prompt: string): Promise<{
|
|
65
|
+
text: string;
|
|
66
|
+
model: string;
|
|
67
|
+
source: string;
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
export interface PupperfishStorageProvider {
|
|
71
|
+
persistImage(file: PupperfishUploadFile): Promise<PupperfishPersistedImage>;
|
|
72
|
+
deletePersistedImage(persisted: PupperfishPersistedImage): Promise<void>;
|
|
73
|
+
deleteStoredImage(pointer: PupperfishStoredImage): Promise<void>;
|
|
74
|
+
resolveStoredImagePath(pointer: PupperfishStoredImage): Promise<string>;
|
|
75
|
+
buildImageDownloadUrl(imageUid: string): string;
|
|
76
|
+
}
|
|
77
|
+
export interface PupperfishJobQueue {
|
|
78
|
+
enqueue(jobType: WorkerJobType, payload: WorkerJobPayload, priority?: number): Promise<void>;
|
|
79
|
+
enqueueMany(jobs: Array<{
|
|
80
|
+
jobType: WorkerJobType;
|
|
81
|
+
payload: WorkerJobPayload;
|
|
82
|
+
priority?: number;
|
|
83
|
+
}>): Promise<void>;
|
|
84
|
+
runWorkerCycle(limit: number): Promise<PupperfishWorkerCycleResult>;
|
|
85
|
+
onLogsChanged(logs: Array<{
|
|
86
|
+
id: string;
|
|
87
|
+
}>): Promise<void>;
|
|
88
|
+
onImageChanged(imageId: string): Promise<void>;
|
|
89
|
+
onSummaryChanged(summaryId: string, sourceLogIds: string[]): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
export interface PupperfishAuditLogger {
|
|
92
|
+
logRetrieveSuccess(params: {
|
|
93
|
+
requestUid: string;
|
|
94
|
+
userId: string;
|
|
95
|
+
queryText: string;
|
|
96
|
+
queryMode: PupperfishPlannerMode;
|
|
97
|
+
entryUid: string | null;
|
|
98
|
+
imageUid: string | null;
|
|
99
|
+
summaryScope: string | null;
|
|
100
|
+
filters: Record<string, unknown>;
|
|
101
|
+
retrievedCount: number;
|
|
102
|
+
latencyMs: number;
|
|
103
|
+
responseJson: Record<string, unknown>;
|
|
104
|
+
requestJson: Record<string, unknown>;
|
|
105
|
+
}): Promise<void>;
|
|
106
|
+
logRetrieveError(params: {
|
|
107
|
+
requestUid: string;
|
|
108
|
+
userId: string;
|
|
109
|
+
queryText: string;
|
|
110
|
+
queryMode: PupperfishPlannerMode;
|
|
111
|
+
filters: Record<string, unknown>;
|
|
112
|
+
errorCode: string;
|
|
113
|
+
requestJson: Record<string, unknown>;
|
|
114
|
+
responseJson: Record<string, unknown>;
|
|
115
|
+
}): Promise<void>;
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function cosineSimilarity(a: number[], b: number[]): number;
|
|
2
|
+
export declare function deterministicEmbeddingFromText(text: string, dimensions?: number): number[];
|
|
3
|
+
export declare function deterministicEmbeddingFromBuffer(buffer: Buffer, dimensions?: number): number[];
|
|
4
|
+
export declare function normalizeVector(vector: number[]): number[];
|
|
5
|
+
export declare function coerceNumberArray(input: unknown): number[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
export function cosineSimilarity(a, b) {
|
|
3
|
+
if (!a.length || !b.length || a.length !== b.length) {
|
|
4
|
+
return 0;
|
|
5
|
+
}
|
|
6
|
+
let dot = 0;
|
|
7
|
+
let normA = 0;
|
|
8
|
+
let normB = 0;
|
|
9
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
10
|
+
dot += a[index] * b[index];
|
|
11
|
+
normA += a[index] * a[index];
|
|
12
|
+
normB += b[index] * b[index];
|
|
13
|
+
}
|
|
14
|
+
if (normA === 0 || normB === 0) {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
18
|
+
}
|
|
19
|
+
export function deterministicEmbeddingFromText(text, dimensions = 64) {
|
|
20
|
+
const out = new Array(dimensions).fill(0);
|
|
21
|
+
if (!text.trim()) {
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
const hash = createHash("sha256").update(text).digest();
|
|
25
|
+
for (let index = 0; index < dimensions; index += 1) {
|
|
26
|
+
const base = hash[index % hash.length] ?? 0;
|
|
27
|
+
out[index] = (base / 255) * 2 - 1;
|
|
28
|
+
}
|
|
29
|
+
return normalizeVector(out);
|
|
30
|
+
}
|
|
31
|
+
export function deterministicEmbeddingFromBuffer(buffer, dimensions = 64) {
|
|
32
|
+
const out = new Array(dimensions).fill(0);
|
|
33
|
+
if (buffer.length < 1) {
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
for (let index = 0; index < dimensions; index += 1) {
|
|
37
|
+
const value = buffer[index % buffer.length] ?? 0;
|
|
38
|
+
out[index] = (value / 255) * 2 - 1;
|
|
39
|
+
}
|
|
40
|
+
return normalizeVector(out);
|
|
41
|
+
}
|
|
42
|
+
export function normalizeVector(vector) {
|
|
43
|
+
let norm = 0;
|
|
44
|
+
for (const value of vector) {
|
|
45
|
+
norm += value * value;
|
|
46
|
+
}
|
|
47
|
+
if (norm === 0) {
|
|
48
|
+
return vector;
|
|
49
|
+
}
|
|
50
|
+
const sqrt = Math.sqrt(norm);
|
|
51
|
+
return vector.map((value) => value / sqrt);
|
|
52
|
+
}
|
|
53
|
+
export function coerceNumberArray(input) {
|
|
54
|
+
if (!Array.isArray(input)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
return input
|
|
58
|
+
.map((value) => (typeof value === "number" && Number.isFinite(value) ? value : null))
|
|
59
|
+
.filter((value) => value !== null);
|
|
60
|
+
}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class PupperfishError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
status;
|
|
4
|
+
constructor(code, message, status = 400) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "PupperfishError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function isPupperfishError(error) {
|
|
12
|
+
return error instanceof PupperfishError;
|
|
13
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function normalizeText(input: string): string;
|
|
2
|
+
export declare function tokenizeText(input: string): string[];
|
|
3
|
+
export declare function buildOutcomeCode(outcome: string): string | null;
|
|
4
|
+
export declare function buildActionCode(nextAction: string | null | undefined): string | null;
|
|
5
|
+
export declare function buildContextStruct(context: string | null | undefined): Record<string, unknown>;
|
|
6
|
+
export declare function createDeterministicUid(prefix: string, payload: string): string;
|
|
7
|
+
export declare function sanitizeList(input: string[]): string[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
export function normalizeText(input) {
|
|
3
|
+
return input
|
|
4
|
+
.normalize("NFKD")
|
|
5
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
6
|
+
.replace(/[^\p{L}\p{N}\s_:/.-]+/gu, " ")
|
|
7
|
+
.replace(/\s+/g, " ")
|
|
8
|
+
.trim()
|
|
9
|
+
.toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
export function tokenizeText(input) {
|
|
12
|
+
const normalized = normalizeText(input);
|
|
13
|
+
if (!normalized) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const tokens = normalized
|
|
17
|
+
.split(/\s+/)
|
|
18
|
+
.filter((token) => token.length >= 2)
|
|
19
|
+
.slice(0, 64);
|
|
20
|
+
return Array.from(new Set(tokens));
|
|
21
|
+
}
|
|
22
|
+
export function buildOutcomeCode(outcome) {
|
|
23
|
+
const normalized = normalizeText(outcome);
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (/(done|xong|hoan thanh|completed|success|ok)/.test(normalized)) {
|
|
28
|
+
return "done";
|
|
29
|
+
}
|
|
30
|
+
if (/(watch|theo doi|monitor|waiting|dang theo doi)/.test(normalized)) {
|
|
31
|
+
return "watching";
|
|
32
|
+
}
|
|
33
|
+
if (/(fail|that bai|loi|error|miss)/.test(normalized)) {
|
|
34
|
+
return "failed";
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
export function buildActionCode(nextAction) {
|
|
39
|
+
const normalized = normalizeText(nextAction ?? "");
|
|
40
|
+
if (!normalized) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (/(alert|canh bao|set alert)/.test(normalized)) {
|
|
44
|
+
return "set_alert";
|
|
45
|
+
}
|
|
46
|
+
if (/(review|xem lai|kiem tra)/.test(normalized)) {
|
|
47
|
+
return "review";
|
|
48
|
+
}
|
|
49
|
+
if (/(execute|vao lenh|entry|mo lenh)/.test(normalized)) {
|
|
50
|
+
return "execute";
|
|
51
|
+
}
|
|
52
|
+
return normalized.slice(0, 40);
|
|
53
|
+
}
|
|
54
|
+
export function buildContextStruct(context) {
|
|
55
|
+
const source = context ?? "";
|
|
56
|
+
const normalized = normalizeText(source);
|
|
57
|
+
const symbolMatch = source.match(/\b([A-Z]{3,6}\/?[A-Z]{0,6})\b/);
|
|
58
|
+
const timeframeMatch = source.match(/\b(M1|M5|M15|M30|H1|H4|D1|W1)\b/i);
|
|
59
|
+
return {
|
|
60
|
+
hasSma: /\bsma\d+/i.test(source),
|
|
61
|
+
hasAlert: /(alert|canh bao)/i.test(source),
|
|
62
|
+
symbol: symbolMatch?.[1] ?? null,
|
|
63
|
+
timeframe: timeframeMatch?.[1]?.toUpperCase() ?? null,
|
|
64
|
+
tokens: tokenizeText(normalized),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function createDeterministicUid(prefix, payload) {
|
|
68
|
+
const digest = createHash("sha1").update(payload).digest("hex");
|
|
69
|
+
return `${prefix}_${digest.slice(0, 20)}`;
|
|
70
|
+
}
|
|
71
|
+
export function sanitizeList(input) {
|
|
72
|
+
return input.map((item) => item.trim()).filter(Boolean);
|
|
73
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PupperfishPlannerKeyword } from "./contracts.js";
|
|
2
|
+
import type { PupperfishPlannerMode } from "./types.js";
|
|
3
|
+
export declare function getDefaultPlannerKeywords(): PupperfishPlannerKeyword[];
|
|
4
|
+
export declare function resolvePlannerMode(query: string, forcedMode?: string, keywords?: PupperfishPlannerKeyword[]): PupperfishPlannerMode;
|
package/dist/planner.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { normalizeText } from "./normalize.js";
|
|
2
|
+
const DEFAULT_KEYWORDS = [
|
|
3
|
+
{ mode: "image", pattern: /\b(image|chart|anh|hinh|screenshot|candlestick|biểu đồ|bieu do)\b/i },
|
|
4
|
+
{ mode: "summary", pattern: /\b(summary|tong hop|tổng hợp|session|tokyo|london|newyork|today)\b/i },
|
|
5
|
+
{ mode: "memory", pattern: /\b(memory|ghi nho|nhớ|rule|quy tac|thoi quen|bai hoc)\b/i },
|
|
6
|
+
{ mode: "sql", pattern: /\b(log|entry|nhat ky|nhật ký|lenh|lệnh|trade)\b/i },
|
|
7
|
+
];
|
|
8
|
+
export function getDefaultPlannerKeywords() {
|
|
9
|
+
return DEFAULT_KEYWORDS;
|
|
10
|
+
}
|
|
11
|
+
export function resolvePlannerMode(query, forcedMode, keywords = DEFAULT_KEYWORDS) {
|
|
12
|
+
const forced = (forcedMode ?? "").trim().toLowerCase();
|
|
13
|
+
if (forced === "sql" || forced === "summary" || forced === "memory" || forced === "image" || forced === "hybrid") {
|
|
14
|
+
return forced;
|
|
15
|
+
}
|
|
16
|
+
const normalized = normalizeText(query);
|
|
17
|
+
for (const item of keywords) {
|
|
18
|
+
if (item.pattern.test(normalized)) {
|
|
19
|
+
return item.mode;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return "hybrid";
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PupperfishAiProvider, PupperfishAuditLogger, PupperfishJobQueue, PupperfishRepositories, PupperfishRuntimeConfig, PupperfishStorageProvider, PupperfishUploadFile } from "./contracts.js";
|
|
2
|
+
import type { PupperfishRetrieveRequest, PupperfishRetrieveResult, PupperfishTradeImageItem, PupperfishUpdateTradeImagePayload, QueryLogFilters, QueryMemoryFilters, QuerySummaryFilters } from "./types.js";
|
|
3
|
+
export declare function createPupperfishRuntime(params: {
|
|
4
|
+
repositories: PupperfishRepositories;
|
|
5
|
+
aiProvider: PupperfishAiProvider;
|
|
6
|
+
storageProvider: PupperfishStorageProvider;
|
|
7
|
+
jobQueue: PupperfishJobQueue;
|
|
8
|
+
auditLogger: PupperfishAuditLogger;
|
|
9
|
+
config: PupperfishRuntimeConfig;
|
|
10
|
+
}): {
|
|
11
|
+
retrieve: (input: PupperfishRetrieveRequest, userId?: string) => Promise<PupperfishRetrieveResult>;
|
|
12
|
+
searchLogs: (query: string, filters: QueryLogFilters, topK: number) => Promise<import("./types.js").PupperfishLogEvidence[]>;
|
|
13
|
+
searchSummaries: (query: string, filters: QuerySummaryFilters, topK: number) => Promise<import("./types.js").PupperfishSummaryEvidence[]>;
|
|
14
|
+
searchMemories: (query: string, filters: QueryMemoryFilters, topK: number) => Promise<import("./types.js").PupperfishMemoryEvidence[]>;
|
|
15
|
+
searchImages: (query: string, topK: number) => Promise<import("./types.js").PupperfishImageEvidence[]>;
|
|
16
|
+
getLog: (entryUid: string) => Promise<import("./types.js").PupperfishLogDetail | null>;
|
|
17
|
+
getSummary: (summaryUid: string) => Promise<Record<string, unknown> | null>;
|
|
18
|
+
getMemory: (memoryUid: string) => Promise<Record<string, unknown> | null>;
|
|
19
|
+
getImage: (imageUid: string) => Promise<import("./types.js").PupperfishStoredImage | null>;
|
|
20
|
+
getSimilarImages: (imageUid: string, topK: number) => Promise<import("./types.js").PupperfishImageEvidence[]>;
|
|
21
|
+
listLogImages: (entryUid: string) => Promise<PupperfishTradeImageItem[] | null>;
|
|
22
|
+
uploadImage: (entryUid: string, payload: {
|
|
23
|
+
file: PupperfishUploadFile;
|
|
24
|
+
chartLabel?: string | null;
|
|
25
|
+
symbol?: string | null;
|
|
26
|
+
timeframe?: string | null;
|
|
27
|
+
note?: string | null;
|
|
28
|
+
imageSlot?: number | null;
|
|
29
|
+
}, userId?: string) => Promise<PupperfishTradeImageItem>;
|
|
30
|
+
updateImage: (imageUid: string, payload: PupperfishUpdateTradeImagePayload) => Promise<PupperfishTradeImageItem>;
|
|
31
|
+
deleteImage: (imageUid: string) => Promise<import("./types.js").PupperfishDeleteImageResult>;
|
|
32
|
+
runWorkerCycle(limit?: number): Promise<import("./types.js").PupperfishWorkerCycleResult>;
|
|
33
|
+
onLogChanged(logs: Array<{
|
|
34
|
+
id: string;
|
|
35
|
+
}>): Promise<void>;
|
|
36
|
+
onImageChanged(imageId: string): Promise<void>;
|
|
37
|
+
onSummaryChanged(summaryId: string, sourceLogIds: string[]): Promise<void>;
|
|
38
|
+
};
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { PupperfishError } from "./errors.js";
|
|
3
|
+
import { composePupperfishAnswer } from "./answer.js";
|
|
4
|
+
import { getDefaultPlannerKeywords, resolvePlannerMode } from "./planner.js";
|
|
5
|
+
function trimQuery(query) {
|
|
6
|
+
return typeof query === "string" ? query.trim() : "";
|
|
7
|
+
}
|
|
8
|
+
function readFilterString(filters, key) {
|
|
9
|
+
const value = filters[key];
|
|
10
|
+
if (typeof value !== "string") {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const normalized = value.trim();
|
|
14
|
+
return normalized || undefined;
|
|
15
|
+
}
|
|
16
|
+
function parseFilters(input) {
|
|
17
|
+
const raw = input.filters && typeof input.filters === "object" ? input.filters : {};
|
|
18
|
+
return {
|
|
19
|
+
logFilters: {
|
|
20
|
+
fromDate: readFilterString(raw, "fromDate"),
|
|
21
|
+
toDate: readFilterString(raw, "toDate"),
|
|
22
|
+
tag: readFilterString(raw, "tag"),
|
|
23
|
+
activity: readFilterString(raw, "activity"),
|
|
24
|
+
context: readFilterString(raw, "context"),
|
|
25
|
+
outcome: readFilterString(raw, "outcome"),
|
|
26
|
+
timeFrom: readFilterString(raw, "timeFrom"),
|
|
27
|
+
timeTo: readFilterString(raw, "timeTo"),
|
|
28
|
+
},
|
|
29
|
+
summaryFilters: {
|
|
30
|
+
scope: readFilterString(raw, "scope"),
|
|
31
|
+
source: readFilterString(raw, "source"),
|
|
32
|
+
status: readFilterString(raw, "status"),
|
|
33
|
+
fromDate: readFilterString(raw, "fromDate"),
|
|
34
|
+
toDate: readFilterString(raw, "toDate"),
|
|
35
|
+
},
|
|
36
|
+
memoryFilters: {
|
|
37
|
+
memoryType: readFilterString(raw, "memoryType"),
|
|
38
|
+
sourceType: readFilterString(raw, "sourceType"),
|
|
39
|
+
status: readFilterString(raw, "status"),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function clampTopK(value, config) {
|
|
44
|
+
const fallback = config.limits?.topKDefault ?? 8;
|
|
45
|
+
const max = config.limits?.topKMax ?? 20;
|
|
46
|
+
const numeric = typeof value === "number" ? value : Number(value ?? fallback);
|
|
47
|
+
if (!Number.isFinite(numeric)) {
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
return Math.min(Math.max(Math.trunc(numeric), 1), max);
|
|
51
|
+
}
|
|
52
|
+
function evidenceKey(item) {
|
|
53
|
+
if (item.kind === "log") {
|
|
54
|
+
return `log:${item.entryUid}`;
|
|
55
|
+
}
|
|
56
|
+
if (item.kind === "summary") {
|
|
57
|
+
return `summary:${item.summaryUid}`;
|
|
58
|
+
}
|
|
59
|
+
if (item.kind === "memory") {
|
|
60
|
+
return `memory:${item.memoryUid}`;
|
|
61
|
+
}
|
|
62
|
+
return `image:${item.imageUid}`;
|
|
63
|
+
}
|
|
64
|
+
function rankEvidence(items, topK) {
|
|
65
|
+
const bestByKey = new Map();
|
|
66
|
+
for (const item of items) {
|
|
67
|
+
const key = evidenceKey(item);
|
|
68
|
+
const previous = bestByKey.get(key);
|
|
69
|
+
if (!previous || item.score > previous.score) {
|
|
70
|
+
bestByKey.set(key, item);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return [...bestByKey.values()]
|
|
74
|
+
.sort((left, right) => right.score - left.score)
|
|
75
|
+
.slice(0, topK);
|
|
76
|
+
}
|
|
77
|
+
function modeUsesLogs(mode) {
|
|
78
|
+
return mode === "sql" || mode === "hybrid";
|
|
79
|
+
}
|
|
80
|
+
function modeUsesSummaries(mode) {
|
|
81
|
+
return mode === "summary" || mode === "hybrid";
|
|
82
|
+
}
|
|
83
|
+
function modeUsesMemories(mode) {
|
|
84
|
+
return mode === "memory" || mode === "hybrid";
|
|
85
|
+
}
|
|
86
|
+
function modeUsesImages(mode) {
|
|
87
|
+
return mode === "image" || mode === "hybrid";
|
|
88
|
+
}
|
|
89
|
+
function buildSources(evidence) {
|
|
90
|
+
return evidence.map((item) => {
|
|
91
|
+
if (item.kind === "log") {
|
|
92
|
+
return { kind: "log", uid: item.entryUid };
|
|
93
|
+
}
|
|
94
|
+
if (item.kind === "summary") {
|
|
95
|
+
return { kind: "summary", uid: item.summaryUid };
|
|
96
|
+
}
|
|
97
|
+
if (item.kind === "memory") {
|
|
98
|
+
return { kind: "memory", uid: item.memoryUid };
|
|
99
|
+
}
|
|
100
|
+
return { kind: "image", uid: item.imageUid };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function normalizeUploadText(value) {
|
|
104
|
+
const normalized = (value ?? "").trim();
|
|
105
|
+
return normalized || null;
|
|
106
|
+
}
|
|
107
|
+
function parseImageSlot(raw) {
|
|
108
|
+
if (raw === null || raw === undefined || raw === "") {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const parsed = typeof raw === "number" ? raw : Number(raw);
|
|
112
|
+
if (!Number.isFinite(parsed)) {
|
|
113
|
+
throw new PupperfishError("PUPPERFISH_IMAGE_SLOT_INVALID", "imageSlot phải là số nguyên dương.", 400);
|
|
114
|
+
}
|
|
115
|
+
return Math.max(1, Math.trunc(parsed));
|
|
116
|
+
}
|
|
117
|
+
export function createPupperfishRuntime(params) {
|
|
118
|
+
const { repositories, aiProvider, storageProvider, jobQueue, auditLogger, config } = params;
|
|
119
|
+
const plannerKeywords = config.plannerKeywords ?? getDefaultPlannerKeywords();
|
|
120
|
+
const uploadMaxBytes = config.limits?.uploadMaxBytes ?? 10 * 1024 * 1024;
|
|
121
|
+
async function retrieve(input, userId = "admin") {
|
|
122
|
+
const query = trimQuery(input.query);
|
|
123
|
+
if (!query) {
|
|
124
|
+
throw new PupperfishError("PUPPERFISH_QUERY_REQUIRED", "query không được để trống.", 400);
|
|
125
|
+
}
|
|
126
|
+
const requestUid = randomUUID();
|
|
127
|
+
const convoUid = typeof input.convoUid === "string" && input.convoUid.trim() ? input.convoUid.trim() : randomUUID();
|
|
128
|
+
const topK = clampTopK(input.topK, config);
|
|
129
|
+
const mode = resolvePlannerMode(query, input.mode, plannerKeywords);
|
|
130
|
+
const { logFilters, summaryFilters, memoryFilters } = parseFilters(input);
|
|
131
|
+
const startedAt = Date.now();
|
|
132
|
+
try {
|
|
133
|
+
const logsPromise = modeUsesLogs(mode) ? repositories.searchLogs(query, logFilters, Math.max(6, topK)) : Promise.resolve([]);
|
|
134
|
+
const summariesPromise = modeUsesSummaries(mode)
|
|
135
|
+
? repositories.searchSummaries(query, summaryFilters, Math.max(4, Math.floor(topK * 0.75)))
|
|
136
|
+
: Promise.resolve([]);
|
|
137
|
+
const memoriesPromise = modeUsesMemories(mode)
|
|
138
|
+
? repositories.searchMemories(query, memoryFilters, Math.max(4, Math.floor(topK * 0.75)))
|
|
139
|
+
: Promise.resolve([]);
|
|
140
|
+
const imagesPromise = modeUsesImages(mode) ? repositories.searchImages(query, Math.max(4, topK)) : Promise.resolve([]);
|
|
141
|
+
const [logs, summaries, memories, images] = await Promise.all([logsPromise, summariesPromise, memoriesPromise, imagesPromise]);
|
|
142
|
+
let evidence = rankEvidence([...logs, ...summaries, ...memories, ...images], topK);
|
|
143
|
+
const needsFallbackMerge = evidence.length < 1 || (mode === "hybrid" && evidence.length < Math.min(2, topK));
|
|
144
|
+
let fallbackEvidenceUsed = false;
|
|
145
|
+
if (needsFallbackMerge) {
|
|
146
|
+
const [fallbackLogs, fallbackSummaries, fallbackMemories, fallbackImages] = await Promise.all([
|
|
147
|
+
modeUsesLogs(mode) ? repositories.searchLogs("", logFilters, Math.max(4, topK)) : Promise.resolve([]),
|
|
148
|
+
modeUsesSummaries(mode)
|
|
149
|
+
? repositories.searchSummaries("", summaryFilters, Math.max(3, Math.floor(topK * 0.75)))
|
|
150
|
+
: Promise.resolve([]),
|
|
151
|
+
modeUsesMemories(mode)
|
|
152
|
+
? repositories.searchMemories("", memoryFilters, Math.max(3, Math.floor(topK * 0.75)))
|
|
153
|
+
: Promise.resolve([]),
|
|
154
|
+
modeUsesImages(mode) ? repositories.searchImages("", Math.max(3, topK)) : Promise.resolve([]),
|
|
155
|
+
]);
|
|
156
|
+
const merged = rankEvidence([...evidence, ...fallbackLogs, ...fallbackSummaries, ...fallbackMemories, ...fallbackImages], topK);
|
|
157
|
+
fallbackEvidenceUsed = merged.length > evidence.length;
|
|
158
|
+
evidence = merged;
|
|
159
|
+
}
|
|
160
|
+
const composed = await composePupperfishAnswer({
|
|
161
|
+
aiProvider,
|
|
162
|
+
config,
|
|
163
|
+
query,
|
|
164
|
+
mode,
|
|
165
|
+
evidence,
|
|
166
|
+
memories,
|
|
167
|
+
charts: images,
|
|
168
|
+
});
|
|
169
|
+
const sources = buildSources(evidence);
|
|
170
|
+
const latencyMs = Date.now() - startedAt;
|
|
171
|
+
if (repositories.recordConversation) {
|
|
172
|
+
await repositories.recordConversation({
|
|
173
|
+
convoUid,
|
|
174
|
+
userId,
|
|
175
|
+
requestUid,
|
|
176
|
+
turns: [
|
|
177
|
+
{
|
|
178
|
+
role: "user",
|
|
179
|
+
content: query,
|
|
180
|
+
metadata: { source: "pupperfish", requestUid },
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
role: "assistant",
|
|
184
|
+
content: composed.answer,
|
|
185
|
+
metadata: { source: "pupperfish", requestUid, mode, fallbackEvidenceUsed },
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
await auditLogger.logRetrieveSuccess({
|
|
191
|
+
requestUid,
|
|
192
|
+
userId,
|
|
193
|
+
queryText: query,
|
|
194
|
+
queryMode: mode,
|
|
195
|
+
entryUid: evidence.find((item) => item.kind === "log")?.entryUid ?? null,
|
|
196
|
+
imageUid: evidence.find((item) => item.kind === "image")?.imageUid ?? null,
|
|
197
|
+
summaryScope: evidence.find((item) => item.kind === "summary")?.scope ?? null,
|
|
198
|
+
filters: input.filters ?? {},
|
|
199
|
+
retrievedCount: evidence.length,
|
|
200
|
+
latencyMs,
|
|
201
|
+
requestJson: input,
|
|
202
|
+
responseJson: {
|
|
203
|
+
mode,
|
|
204
|
+
sources,
|
|
205
|
+
confidence: composed.confidence,
|
|
206
|
+
fallbackEvidenceUsed,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
return {
|
|
210
|
+
requestUid,
|
|
211
|
+
convoUid,
|
|
212
|
+
mode,
|
|
213
|
+
answer: composed.answer,
|
|
214
|
+
confidence: composed.confidence,
|
|
215
|
+
assumptions: composed.assumptions,
|
|
216
|
+
evidence,
|
|
217
|
+
charts: images,
|
|
218
|
+
memories,
|
|
219
|
+
sources,
|
|
220
|
+
latencyMs,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const code = error instanceof PupperfishError ? error.code : "PUPPERFISH_RETRIEVE_FAILED";
|
|
225
|
+
await auditLogger.logRetrieveError({
|
|
226
|
+
requestUid,
|
|
227
|
+
userId,
|
|
228
|
+
queryText: query,
|
|
229
|
+
queryMode: mode,
|
|
230
|
+
filters: input.filters ?? {},
|
|
231
|
+
errorCode: code,
|
|
232
|
+
requestJson: input,
|
|
233
|
+
responseJson: {
|
|
234
|
+
error: error instanceof Error ? error.message : String(error),
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function uploadImage(entryUid, payload, userId = "admin") {
|
|
241
|
+
const normalizedEntryUid = entryUid.trim();
|
|
242
|
+
if (!normalizedEntryUid) {
|
|
243
|
+
throw new PupperfishError("PUPPERFISH_ENTRY_UID_INVALID", "entryUid không hợp lệ.", 400);
|
|
244
|
+
}
|
|
245
|
+
if (!payload.file) {
|
|
246
|
+
throw new PupperfishError("PUPPERFISH_IMAGE_FILE_REQUIRED", "Thiếu file upload.", 400);
|
|
247
|
+
}
|
|
248
|
+
if (payload.file.size < 1) {
|
|
249
|
+
throw new PupperfishError("PUPPERFISH_IMAGE_EMPTY", "File ảnh rỗng.", 400);
|
|
250
|
+
}
|
|
251
|
+
if (!payload.file.type || !payload.file.type.toLowerCase().startsWith("image/")) {
|
|
252
|
+
throw new PupperfishError("PUPPERFISH_IMAGE_FILE_INVALID_TYPE", "Chỉ chấp nhận file ảnh (image/*).", 400);
|
|
253
|
+
}
|
|
254
|
+
if (payload.file.size > uploadMaxBytes) {
|
|
255
|
+
throw new PupperfishError("PUPPERFISH_IMAGE_FILE_TOO_LARGE", `Dung lượng ảnh tối đa là ${Math.trunc(uploadMaxBytes / (1024 * 1024))}MB.`, 400);
|
|
256
|
+
}
|
|
257
|
+
const target = await repositories.getLogImageTarget(normalizedEntryUid);
|
|
258
|
+
if (!target) {
|
|
259
|
+
throw new PupperfishError("PUPPERFISH_LOG_NOT_FOUND", "Không tìm thấy log theo entryUid.", 404);
|
|
260
|
+
}
|
|
261
|
+
const requestedSlot = parseImageSlot(payload.imageSlot);
|
|
262
|
+
const occupied = new Set(target.occupiedSlots);
|
|
263
|
+
if (requestedSlot && occupied.has(requestedSlot)) {
|
|
264
|
+
throw new PupperfishError("PUPPERFISH_IMAGE_SLOT_CONFLICT", "imageSlot đã tồn tại cho log này.", 409);
|
|
265
|
+
}
|
|
266
|
+
const imageSlot = requestedSlot ?? (target.occupiedSlots.length > 0 ? Math.max(...target.occupiedSlots) + 1 : 1);
|
|
267
|
+
const persisted = await storageProvider.persistImage(payload.file);
|
|
268
|
+
try {
|
|
269
|
+
const created = await repositories.createImageForLog({
|
|
270
|
+
imageUid: persisted.imageUid,
|
|
271
|
+
entryId: target.entryId,
|
|
272
|
+
entryUid: target.entryUid,
|
|
273
|
+
imageSlot,
|
|
274
|
+
chartLabel: normalizeUploadText(payload.chartLabel)?.slice(0, 120) ?? "Chart",
|
|
275
|
+
symbol: normalizeUploadText(payload.symbol),
|
|
276
|
+
timeframe: normalizeUploadText(payload.timeframe)?.toUpperCase() ?? null,
|
|
277
|
+
note: normalizeUploadText(payload.note),
|
|
278
|
+
filePath: persisted.absolutePath,
|
|
279
|
+
fileUrl: storageProvider.buildImageDownloadUrl(persisted.imageUid),
|
|
280
|
+
fileName: persisted.fileName,
|
|
281
|
+
mimeType: persisted.mimeType,
|
|
282
|
+
fileSizeBytes: persisted.fileSizeBytes,
|
|
283
|
+
sha256: persisted.sha256,
|
|
284
|
+
uploadedBy: userId,
|
|
285
|
+
relativePath: persisted.relativePath,
|
|
286
|
+
});
|
|
287
|
+
await jobQueue.onImageChanged(created.id);
|
|
288
|
+
return created;
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
await storageProvider.deletePersistedImage(persisted);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function updateImage(imageUid, payload) {
|
|
296
|
+
const normalizedUid = imageUid.trim();
|
|
297
|
+
if (!normalizedUid) {
|
|
298
|
+
throw new PupperfishError("PUPPERFISH_IMAGE_UID_INVALID", "imageUid không hợp lệ.", 400);
|
|
299
|
+
}
|
|
300
|
+
const updated = await repositories.updateImage(normalizedUid, payload);
|
|
301
|
+
await jobQueue.onImageChanged(updated.id);
|
|
302
|
+
return updated;
|
|
303
|
+
}
|
|
304
|
+
async function deleteImage(imageUid) {
|
|
305
|
+
const normalizedUid = imageUid.trim();
|
|
306
|
+
if (!normalizedUid) {
|
|
307
|
+
throw new PupperfishError("PUPPERFISH_IMAGE_UID_INVALID", "imageUid không hợp lệ.", 400);
|
|
308
|
+
}
|
|
309
|
+
const image = await repositories.getImage(normalizedUid);
|
|
310
|
+
if (!image) {
|
|
311
|
+
throw new PupperfishError("PUPPERFISH_IMAGE_NOT_FOUND", "Không tìm thấy ảnh theo imageUid.", 404);
|
|
312
|
+
}
|
|
313
|
+
await storageProvider.deleteStoredImage(image);
|
|
314
|
+
return repositories.deleteImage(normalizedUid);
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
retrieve,
|
|
318
|
+
searchLogs: repositories.searchLogs,
|
|
319
|
+
searchSummaries: repositories.searchSummaries,
|
|
320
|
+
searchMemories: repositories.searchMemories,
|
|
321
|
+
searchImages: repositories.searchImages,
|
|
322
|
+
getLog: repositories.getLog,
|
|
323
|
+
getSummary: repositories.getSummary,
|
|
324
|
+
getMemory: repositories.getMemory,
|
|
325
|
+
getImage: repositories.getImage,
|
|
326
|
+
getSimilarImages: repositories.getSimilarImages,
|
|
327
|
+
listLogImages: repositories.listLogImages,
|
|
328
|
+
uploadImage,
|
|
329
|
+
updateImage,
|
|
330
|
+
deleteImage,
|
|
331
|
+
runWorkerCycle(limit) {
|
|
332
|
+
return jobQueue.runWorkerCycle(limit ?? config.limits?.workerBatchLimit ?? 8);
|
|
333
|
+
},
|
|
334
|
+
onLogChanged(logs) {
|
|
335
|
+
return jobQueue.onLogsChanged(logs);
|
|
336
|
+
},
|
|
337
|
+
onImageChanged(imageId) {
|
|
338
|
+
return jobQueue.onImageChanged(imageId);
|
|
339
|
+
},
|
|
340
|
+
onSummaryChanged(summaryId, sourceLogIds) {
|
|
341
|
+
return jobQueue.onSummaryChanged(summaryId, sourceLogIds);
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
export type PupperfishPlannerMode = "sql" | "summary" | "memory" | "image" | "hybrid";
|
|
2
|
+
export type PupperfishEvidenceKind = "log" | "summary" | "memory" | "image";
|
|
3
|
+
export type PupperfishLogEvidence = {
|
|
4
|
+
kind: "log";
|
|
5
|
+
id: string;
|
|
6
|
+
entryUid: string;
|
|
7
|
+
dateText: string;
|
|
8
|
+
timeText: string;
|
|
9
|
+
activity: string;
|
|
10
|
+
context: string | null;
|
|
11
|
+
outcome: string;
|
|
12
|
+
nextAction: string | null;
|
|
13
|
+
moodEnergy: string | null;
|
|
14
|
+
tags: string[];
|
|
15
|
+
score: number;
|
|
16
|
+
};
|
|
17
|
+
export type PupperfishSummaryEvidence = {
|
|
18
|
+
kind: "summary";
|
|
19
|
+
id: string;
|
|
20
|
+
summaryUid: string;
|
|
21
|
+
summaryDate: string;
|
|
22
|
+
scope: string;
|
|
23
|
+
source: string;
|
|
24
|
+
status: string;
|
|
25
|
+
summaryText: string;
|
|
26
|
+
nextAction: string | null;
|
|
27
|
+
score: number;
|
|
28
|
+
};
|
|
29
|
+
export type PupperfishMemoryEvidence = {
|
|
30
|
+
kind: "memory";
|
|
31
|
+
id: string;
|
|
32
|
+
memoryUid: string;
|
|
33
|
+
memoryType: string;
|
|
34
|
+
title: string | null;
|
|
35
|
+
memoryText: string;
|
|
36
|
+
importanceScore: number;
|
|
37
|
+
confidenceScore: number;
|
|
38
|
+
score: number;
|
|
39
|
+
};
|
|
40
|
+
export type PupperfishImageEvidence = {
|
|
41
|
+
kind: "image";
|
|
42
|
+
id: string;
|
|
43
|
+
imageUid: string;
|
|
44
|
+
entryUid: string;
|
|
45
|
+
chartLabel: string;
|
|
46
|
+
symbol: string | null;
|
|
47
|
+
timeframe: string | null;
|
|
48
|
+
fileName: string;
|
|
49
|
+
fileUrl: string | null;
|
|
50
|
+
score: number;
|
|
51
|
+
};
|
|
52
|
+
export type PupperfishEvidenceItem = PupperfishLogEvidence | PupperfishSummaryEvidence | PupperfishMemoryEvidence | PupperfishImageEvidence;
|
|
53
|
+
export type PupperfishRetrieveRequest = {
|
|
54
|
+
query: string;
|
|
55
|
+
mode?: PupperfishPlannerMode;
|
|
56
|
+
topK?: number;
|
|
57
|
+
convoUid?: string;
|
|
58
|
+
filters?: Record<string, unknown>;
|
|
59
|
+
};
|
|
60
|
+
export type PupperfishRetrieveSource = {
|
|
61
|
+
kind: PupperfishEvidenceKind;
|
|
62
|
+
uid: string;
|
|
63
|
+
};
|
|
64
|
+
export type PupperfishRetrieveResult = {
|
|
65
|
+
requestUid: string;
|
|
66
|
+
convoUid: string;
|
|
67
|
+
mode: PupperfishPlannerMode;
|
|
68
|
+
answer: string;
|
|
69
|
+
confidence: number;
|
|
70
|
+
assumptions: string[];
|
|
71
|
+
evidence: PupperfishEvidenceItem[];
|
|
72
|
+
charts: PupperfishImageEvidence[];
|
|
73
|
+
memories: PupperfishMemoryEvidence[];
|
|
74
|
+
sources: PupperfishRetrieveSource[];
|
|
75
|
+
latencyMs: number;
|
|
76
|
+
};
|
|
77
|
+
export type WorkerJobType = "text_embedding_job" | "summary_embedding_job" | "image_embedding_job" | "memory_extraction_job" | "summary_linking_job" | "summary_memory_job" | "reembed_backfill_job";
|
|
78
|
+
export type WorkerJobPayload = Record<string, unknown>;
|
|
79
|
+
export type WorkerJob = {
|
|
80
|
+
id: string;
|
|
81
|
+
jobType: WorkerJobType;
|
|
82
|
+
payload: WorkerJobPayload;
|
|
83
|
+
retryCount: number;
|
|
84
|
+
maxRetries: number;
|
|
85
|
+
};
|
|
86
|
+
export type QueryLogFilters = {
|
|
87
|
+
fromDate?: string;
|
|
88
|
+
toDate?: string;
|
|
89
|
+
tag?: string;
|
|
90
|
+
activity?: string;
|
|
91
|
+
context?: string;
|
|
92
|
+
outcome?: string;
|
|
93
|
+
timeFrom?: string;
|
|
94
|
+
timeTo?: string;
|
|
95
|
+
};
|
|
96
|
+
export type QuerySummaryFilters = {
|
|
97
|
+
scope?: string;
|
|
98
|
+
source?: string;
|
|
99
|
+
status?: string;
|
|
100
|
+
fromDate?: string;
|
|
101
|
+
toDate?: string;
|
|
102
|
+
};
|
|
103
|
+
export type QueryMemoryFilters = {
|
|
104
|
+
memoryType?: string;
|
|
105
|
+
sourceType?: string;
|
|
106
|
+
status?: string;
|
|
107
|
+
};
|
|
108
|
+
export type PupperfishLogDetail = {
|
|
109
|
+
entryUid: string;
|
|
110
|
+
dateText: string;
|
|
111
|
+
timeText: string;
|
|
112
|
+
activity: string;
|
|
113
|
+
outcome: string;
|
|
114
|
+
nextAction: string | null;
|
|
115
|
+
tags: string[];
|
|
116
|
+
};
|
|
117
|
+
export type PupperfishTradeImageItem = {
|
|
118
|
+
id: string;
|
|
119
|
+
imageUid: string;
|
|
120
|
+
entryUid: string;
|
|
121
|
+
imageSlot: number;
|
|
122
|
+
chartLabel: string;
|
|
123
|
+
symbol: string | null;
|
|
124
|
+
timeframe: string | null;
|
|
125
|
+
note: string | null;
|
|
126
|
+
fileName: string;
|
|
127
|
+
fileUrl: string | null;
|
|
128
|
+
mimeType: string;
|
|
129
|
+
fileSizeBytes: string | null;
|
|
130
|
+
widthPx: number | null;
|
|
131
|
+
heightPx: number | null;
|
|
132
|
+
createdAt: string;
|
|
133
|
+
updatedAt: string;
|
|
134
|
+
};
|
|
135
|
+
export type PupperfishStoredImagePointer = {
|
|
136
|
+
filePath: string | null;
|
|
137
|
+
meta?: unknown;
|
|
138
|
+
};
|
|
139
|
+
export type PupperfishStoredImage = PupperfishTradeImageItem & PupperfishStoredImagePointer & {
|
|
140
|
+
entry: {
|
|
141
|
+
id: string;
|
|
142
|
+
entryUid: string;
|
|
143
|
+
dateText: string;
|
|
144
|
+
timeText: string;
|
|
145
|
+
activity: string;
|
|
146
|
+
outcome: string;
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
export type PupperfishUpdateTradeImagePayload = {
|
|
150
|
+
chartLabel?: string | null;
|
|
151
|
+
symbol?: string | null;
|
|
152
|
+
timeframe?: string | null;
|
|
153
|
+
note?: string | null;
|
|
154
|
+
imageSlot?: number | null;
|
|
155
|
+
};
|
|
156
|
+
export type PupperfishDeleteImageResult = {
|
|
157
|
+
imageUid: string;
|
|
158
|
+
deleted: boolean;
|
|
159
|
+
};
|
|
160
|
+
export type PupperfishLogImageTarget = {
|
|
161
|
+
entryId: string;
|
|
162
|
+
entryUid: string;
|
|
163
|
+
occupiedSlots: number[];
|
|
164
|
+
};
|
|
165
|
+
export type PupperfishPersistedImage = {
|
|
166
|
+
imageUid: string;
|
|
167
|
+
absolutePath: string;
|
|
168
|
+
relativePath: string;
|
|
169
|
+
fileName: string;
|
|
170
|
+
fileSizeBytes: bigint | string;
|
|
171
|
+
mimeType: string;
|
|
172
|
+
sha256: string;
|
|
173
|
+
};
|
|
174
|
+
export type PupperfishCreateImageInput = {
|
|
175
|
+
imageUid: string;
|
|
176
|
+
entryId: string;
|
|
177
|
+
entryUid: string;
|
|
178
|
+
imageSlot: number;
|
|
179
|
+
chartLabel: string;
|
|
180
|
+
symbol: string | null;
|
|
181
|
+
timeframe: string | null;
|
|
182
|
+
note: string | null;
|
|
183
|
+
filePath: string;
|
|
184
|
+
fileUrl: string | null;
|
|
185
|
+
fileName: string;
|
|
186
|
+
mimeType: string;
|
|
187
|
+
fileSizeBytes: bigint | string | null;
|
|
188
|
+
sha256: string | null;
|
|
189
|
+
uploadedBy: string;
|
|
190
|
+
relativePath: string | null;
|
|
191
|
+
};
|
|
192
|
+
export type PupperfishConversationTurn = {
|
|
193
|
+
role: "user" | "assistant";
|
|
194
|
+
content: string;
|
|
195
|
+
metadata?: Record<string, unknown>;
|
|
196
|
+
};
|
|
197
|
+
export type PupperfishWorkerCycleResult = {
|
|
198
|
+
claimed: number;
|
|
199
|
+
done: number;
|
|
200
|
+
failed: number;
|
|
201
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tungpastry/pupperfish-framework",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless Pupperfish runtime and contracts for host apps.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"package.json"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
22
|
+
"build": "npm run clean && tsc -p tsconfig.build.json"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./types": {
|
|
30
|
+
"types": "./dist/types.d.ts",
|
|
31
|
+
"import": "./dist/types.js"
|
|
32
|
+
},
|
|
33
|
+
"./contracts": {
|
|
34
|
+
"types": "./dist/contracts.d.ts",
|
|
35
|
+
"import": "./dist/contracts.js"
|
|
36
|
+
},
|
|
37
|
+
"./normalize": {
|
|
38
|
+
"types": "./dist/normalize.d.ts",
|
|
39
|
+
"import": "./dist/normalize.js"
|
|
40
|
+
},
|
|
41
|
+
"./planner": {
|
|
42
|
+
"types": "./dist/planner.d.ts",
|
|
43
|
+
"import": "./dist/planner.js"
|
|
44
|
+
},
|
|
45
|
+
"./runtime": {
|
|
46
|
+
"types": "./dist/runtime.d.ts",
|
|
47
|
+
"import": "./dist/runtime.js"
|
|
48
|
+
},
|
|
49
|
+
"./errors": {
|
|
50
|
+
"types": "./dist/errors.d.ts",
|
|
51
|
+
"import": "./dist/errors.js"
|
|
52
|
+
},
|
|
53
|
+
"./embeddings": {
|
|
54
|
+
"types": "./dist/embeddings.d.ts",
|
|
55
|
+
"import": "./dist/embeddings.js"
|
|
56
|
+
},
|
|
57
|
+
"./answer": {
|
|
58
|
+
"types": "./dist/answer.d.ts",
|
|
59
|
+
"import": "./dist/answer.js"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|