refactor: remove unused text analysis module and integrate Qdrant enhancements
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 2m30s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 3m7s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m20s

- Deleted the text analysis prompt constants and helpers as they are no longer needed.
- Added batch search functionality for Qdrant to optimize vector searches.
- Implemented methods for deleting expired Qdrant points and invalidating cache based on content hash.
- Updated text batch processor to use new timeout configurations and modified content building for moderation prompts.
- Enhanced text cache store to support new Qdrant integration and improved cache invalidation logic.
- Introduced a new user reputation model with a more nuanced trust scoring system, including penalties and rewards for user behavior.
- Added unit tests for the new trust model to ensure correctness of penalty and trust gain calculations.
- Updated configuration schema to reflect new timeout settings and removed deprecated OpenAI moderation keys.
This commit is contained in:
Developer
2026-07-31 23:09:00 +07:00
parent fc475dfbb7
commit 6df4f306dd
16 changed files with 893 additions and 802 deletions
@@ -6,10 +6,10 @@
* defaults are maintained in one place.
*/
import { createChildLogger } from "@/shared/logger/index";
import { retryWithBackoff } from "@/shared/utils/index";
import OpenAI from "openai";
import pLimit from "p-limit";
import { createChildLogger } from "@/shared/logger/index";
import { retryWithBackoff } from "@/shared/utils/index";
import { config } from "../../shared/config/config.js";
const log = createChildLogger("llm-client");
@@ -245,39 +245,13 @@ export async function llmChat(
);
}
/**
* Convenience for the legacy text-only badword detection call in
* `indonesianTextNormalizer`. Returns parsed flags or [].
*/
export async function llmDetectBadwords(text: string): Promise<string[]> {
const completion = await llmChat({
messages: [
{
role: "user",
content:
"Deteksi kata kasar / pelanggaran ringan dari teks Indonesia berikut. " +
'Balas hanya JSON object dengan format {"flags":[...]} dan gunakan hanya flag valid ini: ' +
Array.from(VALID_PRIMARY_AI_FLAGS).join(", ") +
". Jika tidak ada pelanggaran, flags harus array kosong. Teks: " +
text,
},
],
max_tokens: 200,
temperature: 0.1,
top_p: 0.9,
jsonResponse: { type: "json_object" },
retries: 2,
});
if (!completion) return [];
const content = completion.choices[0]?.message?.content?.trim();
if (!content) return [];
return extractFlagsFromContent(content);
}
/**
* Convenience for vision (image/sticker/emoji) analysis.
* Returns the raw completion content (trimmed) or null.
*
* NOTE: retries are disabled here on purpose — visionAnalyzer.ts already
* wraps this call in its own 3-attempt loop with exponential backoff.
* A second retry layer would multiply worst-case API calls (3×3=9/image).
*/
export async function llmVision(
promptText: string,
@@ -297,91 +271,9 @@ export async function llmVision(
max_tokens: 500,
temperature: 0.1,
top_p: 0.9,
retries: 2,
retries: 0,
});
if (!completion) return null;
return completion.choices[0]?.message?.content?.trim() ?? null;
}
// ---------------------------------------------------------------------------
// Flag extraction (reused from indonesianTextNormalizer)
// ---------------------------------------------------------------------------
const VALID_PRIMARY_AI_FLAGS = new Set([
"spam",
"hate_speech",
"sara",
"hoaks",
"harassment",
"vulgar_language",
"sexual_content",
"sexual_deviation",
"violence",
"self_harm",
"doxxing",
"scam",
"misinformation",
"nsfw_image",
"gore_image",
"illegal_content",
"gambling",
"drugs",
"child_safety",
"financial_scam",
"religious_insult",
"self_promo",
"conflict_instigation",
"offensive_username",
"potential_evasion",
"unclear_context",
]);
function normalizeFlag(value: string): string | null {
const lower = value
.trim()
.toLowerCase()
.replace(/[\s-]+/g, "_");
if (!lower) return null;
if (VALID_PRIMARY_AI_FLAGS.has(lower)) return lower;
return null;
}
function extractFlagsFromContent(content: string): string[] {
const flags = new Set<string>();
let parsed: unknown;
try {
parsed = JSON.parse(content);
} catch {
parsed = null;
}
const addValue = (v: unknown) => {
if (typeof v !== "string") return;
const n = normalizeFlag(v);
if (n) flags.add(n);
};
if (Array.isArray(parsed)) {
for (const item of parsed) addValue(item);
} else if (parsed && typeof parsed === "object") {
const obj = parsed as Record<string, unknown>;
for (const key of ["flags", "categories", "badwords"]) {
const val = obj[key];
if (Array.isArray(val)) {
for (const item of val) addValue(item);
} else {
addValue(val);
}
}
}
if (flags.size > 0) return Array.from(flags);
const lower = content.toLowerCase();
for (const flag of VALID_PRIMARY_AI_FLAGS) {
if (lower.includes(flag)) flags.add(flag);
}
return Array.from(flags);
}