1 line
10 KiB
JSON
1 line
10 KiB
JSON
{"repo": "ahmadhuss/burmese-proofreader", "files": {"apps/api/src/services/deepseek.service.js": {"020d3322528fea1f9873b7b8a69aeaeb3b2166e9": {"t": 1785746677.0625544, "n": 4846, "h": "c1ce62355a74c165", "c": "require(\"dotenv\").config();\nconst OpenAI = require(\"openai\");\nconst { buildEditingPrompt, buildStrictEditingPrompt } = require(\"prompts\");\nconst logger = require(\"../utils/logger\");\n\nfunction resolveBaseUrl() {\n const baseUrl = (process.env.DEEPSEEK_BASE_URL || \"https://api.deepseek.com\").replace(/\\/$/, \"\");\n return baseUrl.endsWith(\"/beta\") ? baseUrl : `${baseUrl}/beta`;\n}\n\nconst client = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: resolveBaseUrl()\n});\n\nconst DEFAULT_MODEL = process.env.DEEPSEEK_MODEL || \"deepseek-v4-flash\";\nconst DEFAULT_MAX_OUTPUT_TOKENS = parseInt(process.env.DEEPSEEK_MAX_OUTPUT_TOKENS || \"64000\", 10);\n\nconst SUSPICIOUS_PHRASES = [\"the rest continues\", \"chapters continue\", \"same level of correction applied\", \"full edited text has been prepared\", \"same correction applied\"];\n\n// Small helper for strict DeepSeek tool schemas. Every field is required by design.\nfunction objectSchema(properties) {\n return {\n type: \"object\",\n properties,\n required: Object.keys(properties),\n additionalProperties: false\n };\n}\n\n// DeepSeek strict mode expects a function-style tool with a JSON schema.\nfunction toolDefinition(name, description, parameters) {\n return {\n type: \"function\",\n function: {\n name,\n strict: true,\n description,\n parameters\n }\n };\n}\n\n// The model must call this tool instead of replying with loose text.\nconst CORRECT_TEXT_TOOL = toolDefinition(\n \"submit_corrected_text\",\n \"Submit the complete corrected Burmese text.\",\n objectSchema({\n corrected_text: {\n type: \"string\",\n description: \"The full corrected text, preserving meaning, paragraph order, and chapter headings.\"\n }\n })\n);\n\n// Sends one prompt to DeepSeek and returns the structured tool result.\nasync function callDeepSeekTool(prompt, tool, model, opts = {}) {\n const { thinkingEnabled = false, reasoningEffort = \"high\", maxTokens } = opts;\n const toolName = tool.function.name;\n\n const params = {\n model: model || DEFAULT_MODEL,\n messages: [\n { role: \"system\", content: \"You are a professional Burmese book editor. Use the required tool call to return the requested structured result.\" },\n { role: \"user\", content: prompt }\n ],\n tools: [tool],\n tool_choice: { type: \"function\", function: { name: toolName } },\n max_tokens: maxTokens || DEFAULT_MAX_OUTPUT_TOKENS,\n stream: false\n };\n\n if (thinkingEnabled) {\n params.thinking = { type: \"enabled\" };\n params.reasoning_effort = reasoningEffort;\n params.temperature = 1;\n } else {\n params.thinking = { type: \"disabled\" };\n params.temperature = 0.2;\n }\n\n const response = await client.chat.completions.create(params);\n const choice = response.choices[0];\n const msg = choice.message;\n\n if (msg.reasoning_content) {\n logger.debug(`Thinking tokens: ~${Math.round(msg.reasoning_content.length / 4)}`);\n }\n\n const call = msg.tool_calls?.find(toolCall => toolCall.function?.name === toolName);\n if (!call) {\n throw new Error(`DeepSeek did not return required tool call \"${toolName}\" (finish_reason: ${choice.finish_reason})`);\n }\n\n try {\n return JSON.parse(call.function.arguments);\n } catch (err) {\n throw new Error(`DeepSeek returned invalid tool arguments for \"${toolName}\" (finish_reason: ${choice.finish_reason}, argument_length: ${call.function.arguments?.length || 0}): ${err.message}`);\n }\n}\n\n// The schema makes the response parseable; these checks make sure the book was not shortened.\nfunction validateOutput(original, corrected) {\n if (!corrected || corrected.trim().length === 0) {\n return { valid: false, reason: \"Output is empty\" };\n }\n if (corrected.length < original.length * 0.4) {\n return { valid: false, reason: \"Output is too short compared to input\" };\n }\n const lower = corrected.toLowerCase();\n for (const phrase of SUSPICIOUS_PHRASES) {\n if (lower.includes(phrase.toLowerCase())) {\n return { valid: false, reason: `Suspicious phrase found: \"${phrase}\"` };\n }\n }\n return { valid: true };\n}\n\n// Corrects one book piece. Retries use the stricter prompt from the shared prompts package.\nasync function correctChunk(chunk, retryCount = 0, model, opts = {}) {\n const prompt = retryCount === 0 ? buildEditingPrompt(chunk.originalText) : buildStrictEditingPrompt(chunk.originalText);\n const maxTokens = Math.min(DEFAULT_MAX_OUTPUT_TOKENS, Math.max(8192, Math.ceil(chunk.originalText.length * 1.5) + 4000));\n\n const result = await callDeepSeekTool(prompt, CORRECT_TEXT_TOOL, model, { ...opts, maxTokens });\n const corrected = result.corrected_text.trim();\n\n const { valid, reason } = validateOutput(chunk.originalText, corrected);\n if (!valid) throw new Error(`Validation failed: ${reason}`);\n\n return corrected;\n}\n\nmodule.exports = { correctChunk, callDeepSeekTool, objectSchema, toolDefinition };\n"}, "43870465a2b05499dac24c3c4fb21174ff9ad9c7": {"t": 1785766757.0903513, "n": 4846, "h": "c1ce62355a74c165", "c": "require(\"dotenv\").config();\nconst OpenAI = require(\"openai\");\nconst { buildEditingPrompt, buildStrictEditingPrompt } = require(\"prompts\");\nconst logger = require(\"../utils/logger\");\n\nfunction resolveBaseUrl() {\n const baseUrl = (process.env.DEEPSEEK_BASE_URL || \"https://api.deepseek.com\").replace(/\\/$/, \"\");\n return baseUrl.endsWith(\"/beta\") ? baseUrl : `${baseUrl}/beta`;\n}\n\nconst client = new OpenAI({\n apiKey: process.env.DEEPSEEK_API_KEY,\n baseURL: resolveBaseUrl()\n});\n\nconst DEFAULT_MODEL = process.env.DEEPSEEK_MODEL || \"deepseek-v4-flash\";\nconst DEFAULT_MAX_OUTPUT_TOKENS = parseInt(process.env.DEEPSEEK_MAX_OUTPUT_TOKENS || \"64000\", 10);\n\nconst SUSPICIOUS_PHRASES = [\"the rest continues\", \"chapters continue\", \"same level of correction applied\", \"full edited text has been prepared\", \"same correction applied\"];\n\n// Small helper for strict DeepSeek tool schemas. Every field is required by design.\nfunction objectSchema(properties) {\n return {\n type: \"object\",\n properties,\n required: Object.keys(properties),\n additionalProperties: false\n };\n}\n\n// DeepSeek strict mode expects a function-style tool with a JSON schema.\nfunction toolDefinition(name, description, parameters) {\n return {\n type: \"function\",\n function: {\n name,\n strict: true,\n description,\n parameters\n }\n };\n}\n\n// The model must call this tool instead of replying with loose text.\nconst CORRECT_TEXT_TOOL = toolDefinition(\n \"submit_corrected_text\",\n \"Submit the complete corrected Burmese text.\",\n objectSchema({\n corrected_text: {\n type: \"string\",\n description: \"The full corrected text, preserving meaning, paragraph order, and chapter headings.\"\n }\n })\n);\n\n// Sends one prompt to DeepSeek and returns the structured tool result.\nasync function callDeepSeekTool(prompt, tool, model, opts = {}) {\n const { thinkingEnabled = false, reasoningEffort = \"high\", maxTokens } = opts;\n const toolName = tool.function.name;\n\n const params = {\n model: model || DEFAULT_MODEL,\n messages: [\n { role: \"system\", content: \"You are a professional Burmese book editor. Use the required tool call to return the requested structured result.\" },\n { role: \"user\", content: prompt }\n ],\n tools: [tool],\n tool_choice: { type: \"function\", function: { name: toolName } },\n max_tokens: maxTokens || DEFAULT_MAX_OUTPUT_TOKENS,\n stream: false\n };\n\n if (thinkingEnabled) {\n params.thinking = { type: \"enabled\" };\n params.reasoning_effort = reasoningEffort;\n params.temperature = 1;\n } else {\n params.thinking = { type: \"disabled\" };\n params.temperature = 0.2;\n }\n\n const response = await client.chat.completions.create(params);\n const choice = response.choices[0];\n const msg = choice.message;\n\n if (msg.reasoning_content) {\n logger.debug(`Thinking tokens: ~${Math.round(msg.reasoning_content.length / 4)}`);\n }\n\n const call = msg.tool_calls?.find(toolCall => toolCall.function?.name === toolName);\n if (!call) {\n throw new Error(`DeepSeek did not return required tool call \"${toolName}\" (finish_reason: ${choice.finish_reason})`);\n }\n\n try {\n return JSON.parse(call.function.arguments);\n } catch (err) {\n throw new Error(`DeepSeek returned invalid tool arguments for \"${toolName}\" (finish_reason: ${choice.finish_reason}, argument_length: ${call.function.arguments?.length || 0}): ${err.message}`);\n }\n}\n\n// The schema makes the response parseable; these checks make sure the book was not shortened.\nfunction validateOutput(original, corrected) {\n if (!corrected || corrected.trim().length === 0) {\n return { valid: false, reason: \"Output is empty\" };\n }\n if (corrected.length < original.length * 0.4) {\n return { valid: false, reason: \"Output is too short compared to input\" };\n }\n const lower = corrected.toLowerCase();\n for (const phrase of SUSPICIOUS_PHRASES) {\n if (lower.includes(phrase.toLowerCase())) {\n return { valid: false, reason: `Suspicious phrase found: \"${phrase}\"` };\n }\n }\n return { valid: true };\n}\n\n// Corrects one book piece. Retries use the stricter prompt from the shared prompts package.\nasync function correctChunk(chunk, retryCount = 0, model, opts = {}) {\n const prompt = retryCount === 0 ? buildEditingPrompt(chunk.originalText) : buildStrictEditingPrompt(chunk.originalText);\n const maxTokens = Math.min(DEFAULT_MAX_OUTPUT_TOKENS, Math.max(8192, Math.ceil(chunk.originalText.length * 1.5) + 4000));\n\n const result = await callDeepSeekTool(prompt, CORRECT_TEXT_TOOL, model, { ...opts, maxTokens });\n const corrected = result.corrected_text.trim();\n\n const { valid, reason } = validateOutput(chunk.originalText, corrected);\n if (!valid) throw new Error(`Validation failed: ${reason}`);\n\n return corrected;\n}\n\nmodule.exports = { correctChunk, callDeepSeekTool, objectSchema, toolDefinition };\n"}}}} |