Add DuckLM service scripts and utility model benchmark
This commit is contained in:
+9
-2
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -16,6 +16,100 @@ function escapeText(value) {
|
||||
return String(value ?? "");
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user