Add DuckLM service scripts and utility model benchmark

This commit is contained in:
2026-05-22 07:27:59 +08:00
parent e6b82f0376
commit ff98224eb6
24 changed files with 2492 additions and 74 deletions
+9 -2
View File
@@ -3,7 +3,7 @@ import json
import logging
import time
from pathlib import Path
from typing import Any
from typing import Any, Literal
import uvicorn
from fastapi import FastAPI, HTTPException, Request
@@ -60,6 +60,7 @@ class ChatRequest(BaseModel):
conversation_id: str | None = None
workspace: str | None = None
debug: bool = False
reasoning: Literal["auto", "on", "off"] = "auto"
class ConversationRequest(BaseModel):
@@ -208,6 +209,7 @@ def create_app() -> FastAPI:
history_messages=history,
memory_records=memory_records,
skill_summary=await selected_skill_summary(body.message),
reasoning=body.reasoning,
)
await conversations.add_message(
conversation.conversation_id,
@@ -411,7 +413,12 @@ def create_app() -> FastAPI:
"Формирую ответ...",
)
await event_store.append(task.task_id, "model_call_started", {"role": "thinker"})
async for chunk in model_client.stream_chat("thinker", messages):
stream = (
model_client.stream_chat("thinker", messages, reasoning=body.reasoning)
if body.reasoning in {"on", "off"}
else model_client.stream_chat("thinker", messages)
)
async for chunk in stream:
delta = str(chunk.get("delta") or "")
if chunk.get("type") == "reasoning_delta":
generation_stats.record(delta)
+21 -1
View File
@@ -3,13 +3,15 @@ import logging
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from typing import Any, Literal
import httpx
import yaml
logger = logging.getLogger(__name__)
ReasoningMode = Literal["auto", "on", "off"]
@dataclass(frozen=True)
class RoleConfig:
@@ -92,6 +94,20 @@ class ModelClient:
}
return {"type": "json_object"}
def _reasoning_options(self, reasoning: ReasoningMode | None) -> dict[str, Any]:
if reasoning == "on":
return {
"reasoning_format": "deepseek",
"chat_template_kwargs": {"enable_thinking": True},
}
if reasoning == "off":
return {
"reasoning_format": "deepseek",
"chat_template_kwargs": {"enable_thinking": False},
"thinking_budget_tokens": 0,
}
return {}
async def chat(
self,
role: str,
@@ -99,6 +115,7 @@ class ModelClient:
temperature: float | None = None,
max_output_tokens: int | None = None,
response_format: dict[str, Any] | None = None,
reasoning: ReasoningMode | None = None,
) -> ModelResponse:
cfg = self.get_role_config(role)
outbound = list(messages)
@@ -115,6 +132,7 @@ class ModelClient:
fmt = self._response_format(cfg, response_format)
if fmt is not None:
payload["response_format"] = fmt
payload.update(self._reasoning_options(reasoning))
start = time.perf_counter()
try:
@@ -150,6 +168,7 @@ class ModelClient:
temperature: float | None = None,
max_output_tokens: int | None = None,
response_format: dict[str, Any] | None = None,
reasoning: ReasoningMode | None = None,
):
cfg = self.get_role_config(role)
outbound = list(messages)
@@ -167,6 +186,7 @@ class ModelClient:
fmt = self._response_format(cfg, response_format)
if fmt is not None:
payload["response_format"] = fmt
payload.update(self._reasoning_options(reasoning))
try:
async with httpx.AsyncClient(timeout=self.timeout, trust_env=False) as client:
+6 -2
View File
@@ -10,7 +10,7 @@ from duck_core.experience.recorder import ExperienceRecorder
from duck_core.memory.policy import MemoryPolicy
from duck_core.memory.store import MemoryStore
from duck_core.memory.vector_memory import VectorMemory
from duck_core.model_client import ModelClient
from duck_core.model_client import ModelClient, ReasoningMode
from duck_core.reflection import Reflection
from duck_core.tasks.store import TaskStore
from duck_core.tools.base import ToolResult
@@ -63,6 +63,7 @@ class RuntimeLoop:
memory_records: list[dict[str, str]] | None = None,
skill_summary: str | None = None,
reflect: bool = True,
reasoning: ReasoningMode | None = None,
) -> ChatResult:
task = await self.task_store.create_task(message, workspace, debug)
await self.event_store.append(
@@ -100,7 +101,10 @@ class RuntimeLoop:
await self.event_store.append(
task.task_id, "model_call_started", {"role": "thinker"}
)
response = await self.model_client.chat("thinker", messages)
if reasoning in {"on", "off"}:
response = await self.model_client.chat("thinker", messages, reasoning=reasoning)
else:
response = await self.model_client.chat("thinker", messages)
await self.event_store.append(
task.task_id,
"cognition_response",
+118 -9
View File
@@ -16,6 +16,100 @@ function escapeText(value) {
return String(value ?? "");
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function renderInlineMarkdown(text) {
let html = escapeHtml(text);
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
html = html.replace(
/(https?:\/\/[^\s<]+)/g,
'<a href="$1" target="_blank" rel="noreferrer">$1</a>',
);
return html;
}
function renderMarkdown(markdown) {
const blocks = [];
const lines = String(markdown ?? "").replace(/\r\n/g, "\n").split("\n");
let paragraph = [];
let listItems = [];
let inFence = false;
let fenceLines = [];
const flushParagraph = () => {
if (!paragraph.length) return;
blocks.push(`<p>${renderInlineMarkdown(paragraph.join(" "))}</p>`);
paragraph = [];
};
const flushList = () => {
if (!listItems.length) return;
blocks.push(`<ul>${listItems.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</ul>`);
listItems = [];
};
for (const line of lines) {
if (line.startsWith("```")) {
if (inFence) {
blocks.push(`<pre><code>${escapeHtml(fenceLines.join("\n"))}</code></pre>`);
fenceLines = [];
inFence = false;
} else {
flushParagraph();
flushList();
inFence = true;
}
continue;
}
if (inFence) {
fenceLines.push(line);
continue;
}
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
if (heading) {
flushParagraph();
flushList();
const level = heading[1].length + 2;
blocks.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
continue;
}
const listItem = /^\s*[-*]\s+(.+)$/.exec(line);
if (listItem) {
flushParagraph();
listItems.push(listItem[1]);
continue;
}
if (!line.trim()) {
flushParagraph();
flushList();
continue;
}
flushList();
paragraph.push(line.trim());
}
if (inFence) blocks.push(`<pre><code>${escapeHtml(fenceLines.join("\n"))}</code></pre>`);
flushParagraph();
flushList();
return blocks.join("");
}
function setMarkdownContent(node, content) {
if (!node) return;
node.dataset.markdown = String(content ?? "");
node.innerHTML = renderMarkdown(node.dataset.markdown);
}
function setStatus(id, text, tone = "neutral") {
const node = document.querySelector(id);
if (!node) return;
@@ -67,7 +161,12 @@ function addMessage(role, content, meta = "", options = {}) {
messageMeta.innerHTML = `<strong>${role === "user" ? "You" : "DuckLM"}</strong><span>${escapeText(meta)}</span>`;
const text = document.createElement("p");
text.textContent = content;
text.className = role === "assistant" ? "message-body markdown-body" : "message-body";
if (role === "assistant") {
setMarkdownContent(text, content);
} else {
text.textContent = content;
}
bubble.append(messageMeta);
if (role === "assistant" && options.reasoning) {
@@ -204,11 +303,10 @@ function updateToolTerminal(article, eventPayload) {
terminal.classList.remove("is-waiting");
status.textContent = result.ok ? "ok" : "error";
const title = terminal.querySelector(".tool-terminal-title")?.textContent || body.textContent.trim();
const parts = [title];
if (result.output) parts.push("\nstdout\n" + result.output.trimEnd());
if (result.error) parts.push("\nstderr\n" + result.error.trimEnd());
body.textContent = parts.join("\n");
const parts = [];
if (result.output) parts.push("stdout\n" + result.output.trimEnd());
if (result.error) parts.push("stderr\n" + result.error.trimEnd());
body.textContent = parts.join("\n\n") || "completed with no output";
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
}
@@ -359,13 +457,22 @@ function humanApprovalDecision(action) {
function setMessagePending(article, text) {
const paragraph = article?.querySelector("p");
if (paragraph) paragraph.textContent = text;
if (!paragraph) return;
if (paragraph.classList.contains("markdown-body")) {
setMarkdownContent(paragraph, text);
} else {
paragraph.textContent = text;
}
}
function appendMessageText(article, delta) {
const paragraph = article?.querySelector("p");
if (!paragraph) return;
paragraph.textContent += delta;
if (paragraph.classList.contains("markdown-body")) {
setMarkdownContent(paragraph, `${paragraph.dataset.markdown || ""}${delta}`);
} else {
paragraph.textContent += delta;
}
document.querySelector("#messages").scrollTop = document.querySelector("#messages").scrollHeight;
}
@@ -758,7 +865,8 @@ async function sendMessage() {
setStatus("#task-status", "running", "warn");
addMessage("user", message, "submitted");
input.value = "";
const pending = addMessage("assistant", "", "thinking", {reasoning: true});
const reasoningEnabled = document.querySelector("#reasoning")?.checked || false;
const pending = addMessage("assistant", "", "thinking", {reasoning: reasoningEnabled});
const context = {taskId: "", contentStarted: false};
try {
@@ -767,6 +875,7 @@ async function sendMessage() {
conversation_id: state.currentConversationId || null,
workspace: document.querySelector("#workspace").value,
debug: document.querySelector("#debug").checked,
reasoning: reasoningEnabled ? "on" : "off",
}, async ({name, data}) => {
await handleAssistantStreamEvent(pending, name, data, context);
});
+54
View File
@@ -500,6 +500,60 @@ dd {
line-height: 1.5;
}
.bubble .markdown-body {
white-space: normal;
}
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body p,
.markdown-body ul,
.markdown-body pre {
margin: 8px 0 0;
}
.markdown-body h3 {
font-size: 17px;
}
.markdown-body h4,
.markdown-body h5 {
font-size: 15px;
}
.markdown-body ul {
padding-left: 22px;
}
.markdown-body code {
padding: 1px 4px;
border-radius: 5px;
background: #e2e8f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.92em;
}
.markdown-body pre {
max-width: 100%;
overflow: auto;
padding: 10px 12px;
border-radius: 8px;
background: #0f172a;
color: #d1fae5;
white-space: pre;
}
.markdown-body pre code {
padding: 0;
background: transparent;
color: inherit;
}
.markdown-body a {
color: var(--accent);
}
.message-reasoning {
display: grid;
gap: 8px;
+4
View File
@@ -46,6 +46,10 @@
<input id="debug" type="checkbox" checked>
<span>Debug mode</span>
</label>
<label class="toggle-row">
<input id="reasoning" type="checkbox">
<span>Reasoning</span>
</label>
</section>
<section class="status-panel" aria-labelledby="status-title">