Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
This commit is contained in:
commit
7afdf5b363
13 files changed
+176
-28
No files matched your search
@@ -10,6 +10,7 @@ import {
|
||||
LightbulbIcon,
|
||||
PaperclipIcon,
|
||||
PlusIcon,
|
||||
ShrinkIcon,
|
||||
SparklesIcon,
|
||||
RocketIcon,
|
||||
XIcon,
|
||||
@@ -329,6 +330,14 @@ export function InputBox({
|
||||
setLocalSettings("display", { ragModeEnabled: !ragModeEnabled });
|
||||
}, [ragModeEnabled, ragModeLocked, setLocalSettings]);
|
||||
|
||||
const contextCompressionEnabled =
|
||||
localSettings.display.contextCompressionEnabled ?? false;
|
||||
const handleContextCompressionToggle = useCallback(() => {
|
||||
setLocalSettings("display", {
|
||||
contextCompressionEnabled: !contextCompressionEnabled,
|
||||
});
|
||||
}, [contextCompressionEnabled, setLocalSettings]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (message: PromptInputMessage) => {
|
||||
if (status === "streaming") {
|
||||
@@ -735,6 +744,35 @@ export function InputBox({
|
||||
<span className="text-xs font-normal">参考文献</span>
|
||||
</PromptInputButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
contextCompressionEnabled
|
||||
? "上下文压缩已开启:超长对话会自动压缩历史以节省 token"
|
||||
: "上下文压缩已关闭:点击开启"
|
||||
}
|
||||
>
|
||||
<PromptInputButton
|
||||
type="button"
|
||||
aria-pressed={contextCompressionEnabled}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"gap-1.5! rounded-full px-3! shadow-none",
|
||||
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
||||
contextCompressionEnabled
|
||||
? "border border-violet-300 bg-violet-50 text-violet-700 hover:bg-violet-100 dark:border-violet-700 dark:bg-violet-950/40 dark:text-violet-200"
|
||||
: compact
|
||||
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
||||
)}
|
||||
onClick={handleContextCompressionToggle}
|
||||
>
|
||||
{contextCompressionEnabled && (
|
||||
<CheckIcon className="size-3 text-violet-700 dark:text-violet-200" />
|
||||
)}
|
||||
<ShrinkIcon className="size-3" />
|
||||
<span className="text-xs font-normal">上下文压缩</span>
|
||||
</PromptInputButton>
|
||||
</Tooltip>
|
||||
<PromptInputActionMenu>
|
||||
<ModeHoverGuide
|
||||
mode={
|
||||
|
||||
@@ -147,6 +147,13 @@ export interface BatchIngestPayload {
|
||||
include_sources?: boolean;
|
||||
skip_existing?: boolean;
|
||||
mode?: "summary" | "full";
|
||||
/**
|
||||
* When true (default), the backend runs the per-thread extraction loop in a
|
||||
* background task and returns immediately (`queued:true`) instead of blocking
|
||||
* the request until every thread is processed — which otherwise outlives the
|
||||
* nginx gateway timeout and surfaces as a generic "request failed".
|
||||
*/
|
||||
background?: boolean;
|
||||
/** Optional model override for knowledge extraction (null = configured default). */
|
||||
model_name?: string | null;
|
||||
/** Optional extraction-prompt template id (null = scope default). */
|
||||
@@ -157,6 +164,8 @@ export interface BatchIngestPayload {
|
||||
|
||||
export interface BatchIngestResult {
|
||||
ok: boolean;
|
||||
/** True when the backend queued the batch as a background task (see payload). */
|
||||
queued?: boolean;
|
||||
processed: number;
|
||||
created: number;
|
||||
skipped: number;
|
||||
|
||||
@@ -208,12 +208,12 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
userPath: "/page/strategy/user-leaderboard",
|
||||
},
|
||||
{ id: "mgmt-agents", label: "智能体管理", icon: Bot, path: "/page/strategy/agents" },
|
||||
{
|
||||
id: "mgmt-business-chain-mapping",
|
||||
label: "配置管理",
|
||||
icon: Network,
|
||||
path: "/page/strategy/business-chain-mapping",
|
||||
},
|
||||
// {
|
||||
// id: "mgmt-business-chain-mapping",
|
||||
// label: "配置管理",
|
||||
// icon: Network,
|
||||
// path: "/page/strategy/business-chain-mapping",
|
||||
// },
|
||||
{
|
||||
id: "mgmt-business-chains",
|
||||
label: "业务链条配置",
|
||||
|
||||
@@ -7,6 +7,7 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
|
||||
display: {
|
||||
streamSpeedMeterEnabled: true,
|
||||
ragModeEnabled: false,
|
||||
contextCompressionEnabled: false,
|
||||
citationCleanupWords: [],
|
||||
fontScale: 1,
|
||||
},
|
||||
@@ -43,6 +44,12 @@ export interface LocalSettings {
|
||||
* messages render tool results with the legacy generic search UI only.
|
||||
*/
|
||||
ragModeEnabled: boolean;
|
||||
/**
|
||||
* Per-user switch for context compression (上下文压缩). When enabled, long
|
||||
* conversations are summarized transiently before the model call to save
|
||||
* tokens. Default off; toggled from the chat input box (below 参考文献).
|
||||
*/
|
||||
contextCompressionEnabled: boolean;
|
||||
/**
|
||||
* Admin-maintained words or short phrases removed from reference snippets
|
||||
* and dialogs, one entry per line in the settings UI.
|
||||
|
||||
@@ -664,6 +664,9 @@ export function useThreadStream({
|
||||
lockThreadRagMode(threadId);
|
||||
}
|
||||
|
||||
const contextCompressionEnabled =
|
||||
localSettings.display.contextCompressionEnabled ?? false;
|
||||
|
||||
const mergedContext = {
|
||||
...context,
|
||||
...extraContext,
|
||||
@@ -715,6 +718,7 @@ export function useThreadStream({
|
||||
memory_injection_enabled:
|
||||
mergedContext.memory_injection_enabled ?? true,
|
||||
rag_mode_enabled: ragModeEnabled,
|
||||
summarization_enabled: contextCompressionEnabled,
|
||||
thread_id: threadId,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface AgentThreadContext extends Record<string, unknown> {
|
||||
writing_artifact_path?: string;
|
||||
memory_injection_enabled?: boolean;
|
||||
rag_mode_enabled?: boolean;
|
||||
/** Per-request switch for context compression (上下文压缩). Default off. */
|
||||
summarization_enabled?: boolean;
|
||||
/** Per-conversation switch for knowledge-base RAG injection (phase 2). */
|
||||
knowledge_rag_enabled?: boolean;
|
||||
agent_id?: string;
|
||||
|
||||
@@ -176,10 +176,21 @@ function BatchImportDialog({
|
||||
skip_existing: skipExisting,
|
||||
status: "approved",
|
||||
include_sources: true,
|
||||
// Run in the background so the request returns immediately — a large
|
||||
// batch otherwise blocks past the gateway timeout and shows "request
|
||||
// failed". Threads stream into the list as they finish.
|
||||
background: true,
|
||||
model_name: model,
|
||||
template_id: template,
|
||||
folder,
|
||||
});
|
||||
if (result.queued) {
|
||||
toast.success(
|
||||
`已转入后台导入 ${result.processed} 个线程,完成后会陆续出现在左侧列表(可稍后刷新查看)`,
|
||||
);
|
||||
onOpenChange(false);
|
||||
return;
|
||||
}
|
||||
const base = `批量导入完成:新增 ${result.created},跳过 ${result.skipped},失败 ${result.failed}`;
|
||||
if (result.llm_fallback && result.llm_fallback > 0) {
|
||||
// The model failed on some threads — they were saved via rule-based
|
||||
@@ -549,7 +560,7 @@ export default function KnowledgeBasePage() {
|
||||
keyword: debouncedKeyword || undefined,
|
||||
source_type: sourceType === "all" ? undefined : sourceType,
|
||||
status: status === "all" ? undefined : status,
|
||||
limit: 500,
|
||||
limit: 1000,
|
||||
}),
|
||||
[debouncedKeyword, sourceType, status],
|
||||
);
|
||||
|
||||
@@ -252,7 +252,7 @@ FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWA
|
||||
| **HTML Page Favorites** (`/api/html-page-favorites`) | Bookmarked generated HTML pages, owner-scoped. `GET /` - list; `POST /` `{page_id, title?, description?, tags?}` - bookmark a `scheduled_html_pages` row (extracts a reusable style/layout summary via GLM5 at favorite time); a page can only be favorited once per user (`DuplicateFavoriteError` ? HTTP 409). `GET/DELETE /{id}` - read/delete (the create-task reference picker deletes favorites here). A favorite can be selected as a style reference when creating a new `html_page` task. |
|
||||
| **Public Scheduled Tasks** (`/api/public/scheduled-tasks`) | **Unauthenticated** mirror for embedding. Adds `GET /runs/{rid}/html` and `GET /{task_id}/latest-html` for the `html_page` preview; the run result keeps `result.html` (only file paths are redacted). |
|
||||
| **Light Apps** (`/api/light-apps`) | Light-app registry (????? / ????), **global/shared** (no user scoping; `created_by`/`updated_by` audit-only). Two kinds: `window_open` (launched in a new tab from the ???? card grid) and `iframe` (embedded under a parent sidebar menu via `mount_location` and dynamically injected into the sidebar). Both carry `route_params` ? a list of `{key, paramType: login\|theme, source, customValue, themeValue}` descriptors the **frontend** resolves at launch time (login params read from `localStorage.userInfo`; theme params use a literal value) and appends to `url` as query string. `GET /` - list (filters `app_type`/`mount_location`/`enabled_only`/`search`); `GET /{app_id}` - read one (both open to any authenticated user); `POST /`, `PUT /{app_id}`, `DELETE /{app_id}` - **admin only**; iframe apps must carry a `mount_location` (400 otherwise); the unique `name` collides as HTTP 409. Backed by `deerflow.persistence.light_apps` (table `light_apps`, auto-created by `create_all`); store wired in `deps.py` as `app.state.light_app_store`. Tests: `tests/test_light_apps.py`. |
|
||||
| **Knowledge** (`/api/knowledge`) | Product knowledge base, **global/shared** (no user isolation; writes record `created_by`/`updated_by` for audit). `POST /ingest/thread/{thread_id}` - sediment one thread into a note (reads messages from the checkpointer, `threads:read` owner-checked); `POST /ingest/threads/batch` - batch-sediment the caller's recent threads (`skip_existing` via `.manifest.json` dedup). **Optional model selection**: both ingest endpoints accept `model_name` (overrides `knowledge.extract_model_name` for that capture; threaded through `KnowledgeService.capture_thread` ? `extract_thread_llm_multi`). **Model-error surfacing**: when the chosen model produces nothing usable (timeout / API error / non-JSON) capture still **falls back to rule-based extraction and lands a draft** (`llm_fallback:true`); the single-thread (non-background) response and the batch response (`llm_fallback` count) carry this so the UI tells the user "???????". The "??????" chat button stays background/fire-and-forget (passes `model_name`, no per-save error toast); the batch-import dialog is synchronous and shows the fallback count. `POST /ingest/file` - **import an uploaded file** (multipart: `file` + optional `title`/`tags`/`status`/`model_name`) into wiki note(s): the file is converted to Markdown (md/txt read directly, pdf/word/ppt/excel via `convert_file_to_markdown`), distilled by the same wiki-capture LLM pipeline as thread capture (`source_type="file"`), and falls back to a verbatim **draft** note when the model returns nothing usable (`llm_fallback:true`); `POST /ingest/search` - sediment search/tool results as a reference note; `POST /notes` - manual note; `GET /notes` (keyword/tag/source_type/status + pagination); `GET/PUT/DELETE /notes/{id}` (DELETE is soft = archive, `?hard=true` removes the vault file); `POST /search` - `mode=keyword\|vector\|hybrid` (vector?keyword fallback when no embedding endpoint); `GET /notes/{id}/versions` - edit history; `GET /duplicates` + `POST /merge` - dedup; `POST /reindex` - rebuild vector index; `GET /graph` - product graph data (notes + entities, graph-blacklist filtered); `GET/POST /graph/blacklist` + `DELETE /graph/blacklist/{id}` - graph node blacklist (labels = entity names / note titles, case-insensitive match; blacklisted nodes and their edges are dropped from `/graph` and the wiki-export artifacts; table `knowledge_graph_blacklist`, auto-created); `POST /export/graph` + `GET /export/files/{graph.json\|graph.html\|graph.graphml\|cypher.txt}` - obsidian-wiki `wiki-export` artifacts (self-contained `graph.html` for iframe); `POST /resolve` `{targets:[?]}` + `GET /notes/{id}/wikilinks` - resolve `[[wikilink]]` targets to a note id / vault page / missing (powers clickable wikilinks); `GET /vault-page?path=` - read a raw vault meta/hub Markdown page (no DB note). `GET/POST /folders` + `PUT/DELETE /folders/{id}` - user-managed directories (notes carry a `folder` path; `GET /notes?folder=` filters a dir + sub-dirs); `GET/POST /extract-templates` + `GET/PUT/DELETE /extract-templates/{id}` - editable extraction-prompt templates (thread/document ingest accept `template_id`; capture/PUT-note accept `folder`). Backed by the `deerflow.knowledge` module (see below). |
|
||||
| **Knowledge** (`/api/knowledge`) | Product knowledge base, **global/shared** (no user isolation; writes record `created_by`/`updated_by` for audit). `POST /ingest/thread/{thread_id}` - sediment one thread into a note (reads messages from the checkpointer, `threads:read` owner-checked); `POST /ingest/threads/batch` - batch-sediment the caller's recent threads (`skip_existing` via `.manifest.json` dedup). **Defaults to `background=true`**: the slow per-thread extraction loop is deferred to a FastAPI background task and the endpoint returns `{queued:true, processed:N}` immediately (each thread's transcript is truncated to `max_input_chars` before the LLM call). Running the loop inline for a large batch outlives the nginx gateway timeout (504), which the frontend's `parseResponse` surfaces as a generic "Request failed"; `background=false` keeps the blocking path (full per-thread `created/skipped/failed` tally) for small batches/scripts. Shared loop: `_run_batch_ingest`. **Optional model selection**: both ingest endpoints accept `model_name` (overrides `knowledge.extract_model_name` for that capture; threaded through `KnowledgeService.capture_thread` ? `extract_thread_llm_multi`). **Model-error surfacing**: when the chosen model produces nothing usable (timeout / API error / non-JSON) capture still **falls back to rule-based extraction and lands a draft** (`llm_fallback:true`); the single-thread (non-background) response and the batch response (`llm_fallback` count) carry this so the UI tells the user "???????". The "??????" chat button stays background/fire-and-forget (passes `model_name`, no per-save error toast); the batch-import dialog is synchronous and shows the fallback count. `POST /ingest/file` - **import an uploaded file** (multipart: `file` + optional `title`/`tags`/`status`/`model_name`) into wiki note(s): the file is converted to Markdown (md/txt read directly, pdf/word/ppt/excel via `convert_file_to_markdown`), distilled by the same wiki-capture LLM pipeline as thread capture (`source_type="file"`), and falls back to a verbatim **draft** note when the model returns nothing usable (`llm_fallback:true`); `POST /ingest/search` - sediment search/tool results as a reference note; `POST /notes` - manual note; `GET /notes` (keyword/tag/source_type/status + pagination); `GET/PUT/DELETE /notes/{id}` (DELETE is soft = archive, `?hard=true` removes the vault file); `POST /search` - `mode=keyword\|vector\|hybrid` (vector?keyword fallback when no embedding endpoint); `GET /notes/{id}/versions` - edit history; `GET /duplicates` + `POST /merge` - dedup; `POST /reindex` - rebuild vector index; `GET /graph` - product graph data (notes + entities, graph-blacklist filtered); `GET/POST /graph/blacklist` + `DELETE /graph/blacklist/{id}` - graph node blacklist (labels = entity names / note titles, case-insensitive match; blacklisted nodes and their edges are dropped from `/graph` and the wiki-export artifacts; table `knowledge_graph_blacklist`, auto-created); `POST /export/graph` + `GET /export/files/{graph.json\|graph.html\|graph.graphml\|cypher.txt}` - obsidian-wiki `wiki-export` artifacts (self-contained `graph.html` for iframe); `POST /resolve` `{targets:[?]}` + `GET /notes/{id}/wikilinks` - resolve `[[wikilink]]` targets to a note id / vault page / missing (powers clickable wikilinks); `GET /vault-page?path=` - read a raw vault meta/hub Markdown page (no DB note). `GET/POST /folders` + `PUT/DELETE /folders/{id}` - user-managed directories (notes carry a `folder` path; `GET /notes?folder=` filters a dir + sub-dirs); `GET/POST /extract-templates` + `GET/PUT/DELETE /extract-templates/{id}` - editable extraction-prompt templates (thread/document ingest accept `template_id`; capture/PUT-note accept `folder`). Backed by the `deerflow.knowledge` module (see below). |
|
||||
|
||||
Proxied through nginx: `/api/langgraph/*` ? LangGraph, all other `/api/*` ? Gateway.
|
||||
|
||||
@@ -846,10 +846,20 @@ See [docs/plan_mode_usage.md](docs/plan_mode_usage.md) for details.
|
||||
### Context Summarization
|
||||
|
||||
Automatic conversation summarization when approaching token limits:
|
||||
- Configured in `config.yaml` under `summarization` key
|
||||
- Trigger types: tokens, messages, or fraction of max input
|
||||
- Keeps recent messages while summarizing older ones
|
||||
- **On/off switch is per-request, not the config file.** The chat input box has a
|
||||
「上下文压缩」 toggle (below 参考文献, **default off**) → frontend sends
|
||||
`context.summarization_enabled` → whitelisted in `_CONTEXT_CONFIGURABLE_KEYS`
|
||||
(`app/gateway/services.py`) → `_build_middlewares` reads it from the run config
|
||||
and calls `_create_summarization_middleware(force_enabled=summarization_enabled)`.
|
||||
`force_enabled=True` mounts the middleware even when `config.summarization.enabled`
|
||||
is false; when the flag is absent (channels / scheduled runs) it falls back to
|
||||
the config flag. `config.summarization.enabled` now defaults to **false**.
|
||||
- `config.yaml` `summarization` still supplies the **tuning**: trigger types
|
||||
(tokens, messages, or fraction of max input) and the keep policy.
|
||||
- Keeps recent messages while summarizing older ones.
|
||||
|
||||
Tests: `tests/test_summarization_toggle.py` (gating + whitelist),
|
||||
`tests/test_summarization_middleware.py` (cache behavior).
|
||||
See [docs/summarization.md](docs/summarization.md) for details.
|
||||
|
||||
### Vision Support / Image Recognition (??)
|
||||
|
||||
@@ -65,6 +65,10 @@ class BatchIngestRequest(BaseModel):
|
||||
include_sources: bool = True
|
||||
skip_existing: bool = True
|
||||
mode: str = "summary"
|
||||
background: bool = Field(
|
||||
default=True,
|
||||
description="When true (default), the per-thread extraction loop runs in a background task and the endpoint returns immediately ({queued:true}). Batch capture of many threads is far too slow to finish inside one HTTP request — running it inline makes nginx time out (504), which the frontend surfaces as a generic 'request failed'.",
|
||||
)
|
||||
model_name: str | None = Field(default=None, description="可选:指定用于知识提炼的大模型名称;留空使用默认配置")
|
||||
template_id: str | None = Field(default=None, description="可选:抽取提示词模板 id;留空使用对话默认模板")
|
||||
folder: str | None = Field(default=None, description="可选:沉淀结果归入的目录(斜杠分隔路径)")
|
||||
@@ -285,15 +289,14 @@ async def ingest_thread(thread_id: str, body: IngestThreadRequest, request: Requ
|
||||
}
|
||||
|
||||
|
||||
@router.post("/ingest/threads/batch")
|
||||
@require_auth
|
||||
async def ingest_threads_batch(body: BatchIngestRequest, request: Request) -> dict[str, Any]:
|
||||
"""Batch-sediment the caller's recent threads into knowledge notes."""
|
||||
service = get_knowledge_service(request)
|
||||
checkpointer = get_checkpointer(request)
|
||||
thread_store = get_thread_store(request)
|
||||
async def _run_batch_ingest(service, checkpointer, threads: list[dict[str, Any]], body: BatchIngestRequest, created_by: str | None) -> dict[str, Any]:
|
||||
"""Run the per-thread sediment loop and tally the outcome.
|
||||
|
||||
threads = await thread_store.search(limit=body.limit, offset=body.offset)
|
||||
Shared by the inline and background paths; ``created_by`` is passed in
|
||||
explicitly because the user contextvar is gone once a background task runs.
|
||||
Each thread's transcript is truncated by the extractor (``max_input_chars``)
|
||||
before the LLM call, so a single huge conversation can't blow the context.
|
||||
"""
|
||||
created = skipped = failed = 0
|
||||
# Notes that landed via the rule-based fallback because the chosen model
|
||||
# produced nothing usable (timeout / API error / non-JSON). Surfaced so the
|
||||
@@ -319,7 +322,7 @@ async def ingest_threads_batch(body: BatchIngestRequest, request: Request) -> di
|
||||
status=body.status,
|
||||
include_sources=body.include_sources,
|
||||
reference_batches=batches,
|
||||
created_by=get_effective_user_id(),
|
||||
created_by=created_by,
|
||||
skip_existing=body.skip_existing,
|
||||
model_name=body.model_name,
|
||||
template_id=body.template_id,
|
||||
@@ -347,6 +350,49 @@ async def ingest_threads_batch(body: BatchIngestRequest, request: Request) -> di
|
||||
return {"ok": True, "processed": len(items), "created": created, "skipped": skipped, "failed": failed, "llm_fallback": llm_fallback, "items": items}
|
||||
|
||||
|
||||
async def _ingest_threads_batch_background(service, checkpointer, threads: list[dict[str, Any]], body: BatchIngestRequest, created_by: str | None) -> None:
|
||||
"""Background variant of the batch loop (failures swallowed/logged)."""
|
||||
try:
|
||||
result = await _run_batch_ingest(service, checkpointer, threads, body, created_by)
|
||||
logger.info(
|
||||
"Background batch ingest finished: processed=%d created=%d skipped=%d failed=%d llm_fallback=%d",
|
||||
result["processed"], result["created"], result["skipped"], result["failed"], result["llm_fallback"],
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Background batch ingest failed")
|
||||
|
||||
|
||||
@router.post("/ingest/threads/batch")
|
||||
@require_auth
|
||||
async def ingest_threads_batch(body: BatchIngestRequest, request: Request, background_tasks: BackgroundTasks) -> dict[str, Any]:
|
||||
"""Batch-sediment the caller's recent threads into knowledge notes.
|
||||
|
||||
Defaults to ``background=true``: the (slow) per-thread extraction runs in a
|
||||
background task and the endpoint returns ``{"queued": true, "processed": N}``
|
||||
immediately, so the request never outlives the nginx gateway timeout. Pass
|
||||
``background=false`` to block until every thread is processed (small batches /
|
||||
scripts that want the full per-thread tally).
|
||||
"""
|
||||
service = get_knowledge_service(request)
|
||||
checkpointer = get_checkpointer(request)
|
||||
thread_store = get_thread_store(request)
|
||||
|
||||
threads = await thread_store.search(limit=body.limit, offset=body.offset)
|
||||
if body.background:
|
||||
# Capture the user id now — the contextvar is gone once the task runs.
|
||||
background_tasks.add_task(
|
||||
_ingest_threads_batch_background,
|
||||
service,
|
||||
checkpointer,
|
||||
threads,
|
||||
body,
|
||||
get_effective_user_id(),
|
||||
)
|
||||
return {"ok": True, "queued": True, "processed": len(threads), "created": 0, "skipped": 0, "failed": 0, "llm_fallback": 0, "items": []}
|
||||
|
||||
return await _run_batch_ingest(service, checkpointer, threads, body, get_effective_user_id())
|
||||
|
||||
|
||||
async def _extract_upload_text(file: UploadFile) -> tuple[str, str]:
|
||||
"""Read an uploaded file and return ``(filename, markdown_text)``.
|
||||
|
||||
@@ -476,7 +522,7 @@ async def list_notes(
|
||||
offset: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
service = get_knowledge_service(request)
|
||||
limit = max(1, min(limit, 100))
|
||||
limit = max(1, min(limit, 1000))
|
||||
offset = max(0, offset)
|
||||
return await service.list_notes(keyword=keyword, tag=tag, entity=entity, source_type=source_type, status=status, folder=folder, limit=limit, offset=offset)
|
||||
|
||||
|
||||
@@ -243,6 +243,9 @@ _CONTEXT_CONFIGURABLE_KEYS: frozenset[str] = frozenset(
|
||||
# 关掉个人记忆召回 / builtin 记忆注入(见 multi_agent._run_payload)。
|
||||
"memory_recall_disabled",
|
||||
"memory_injection_enabled",
|
||||
# 上下文压缩开关:由聊天框下方的按钮控制(默认关闭),透传到 run config 后
|
||||
# 在 _build_middlewares 里决定是否挂载 SummarizationMiddleware。
|
||||
"summarization_enabled",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -200,7 +200,10 @@ title:
|
||||
# grep「上下文压缩」/「context compaction」确认生效;确认后再调回生产值
|
||||
# (trigger tokens 60000 / messages 80,keep tokens 16000)。
|
||||
summarization:
|
||||
enabled: true
|
||||
# 开关已移到聊天框下方(参考文献下面)的「上下文压缩」按钮,默认关闭。
|
||||
# 这里 enabled 仅作为非 Web 路径(渠道/定时任务)的后备开关;下面的 trigger/keep
|
||||
# 等仅是调参,Web 端是否压缩由前端按钮(summarization_enabled)决定。
|
||||
enabled: false
|
||||
# model_name: deepseek-chat # 留空则用主对话模型
|
||||
trigger:
|
||||
- type: tokens
|
||||
|
||||
+17
-5
@@ -54,12 +54,19 @@ def _resolve_model_name(requested_model_name: str | None = None, *, app_config:
|
||||
return default_model_name
|
||||
|
||||
|
||||
def _create_summarization_middleware(*, app_config: AppConfig | None = None) -> DeerFlowSummarizationMiddleware | None:
|
||||
"""Create and configure the summarization middleware from config."""
|
||||
def _create_summarization_middleware(*, app_config: AppConfig | None = None, force_enabled: bool = False) -> DeerFlowSummarizationMiddleware | None:
|
||||
"""Create and configure the summarization middleware from config.
|
||||
|
||||
Args:
|
||||
force_enabled: When True, build the middleware regardless of the config
|
||||
``summarization.enabled`` flag. The per-request 上下文压缩 toggle
|
||||
(chat input box, default off) drives this — the config file no longer
|
||||
owns the on/off switch, only the tuning (trigger/keep/...).
|
||||
"""
|
||||
resolved_app_config = app_config or get_app_config()
|
||||
config = resolved_app_config.summarization
|
||||
|
||||
if not config.enabled:
|
||||
if not (config.enabled or force_enabled):
|
||||
return None
|
||||
|
||||
# Prepare trigger parameter
|
||||
@@ -268,8 +275,13 @@ def _build_middlewares(
|
||||
if cfg.get("rag_mode_enabled", False):
|
||||
middlewares.append(RagCitationMiddleware())
|
||||
|
||||
# Add summarization middleware if enabled
|
||||
summarization_middleware = _create_summarization_middleware(app_config=resolved_app_config)
|
||||
# Add summarization middleware. The on/off switch now lives in the chat UI
|
||||
# (上下文压缩 toggle, default off) and arrives as ``summarization_enabled`` in
|
||||
# the runtime config; the config file only supplies tuning. force_enabled
|
||||
# lets the per-request toggle turn it on even when config.enabled is false,
|
||||
# while non-UI paths (channels/scheduled) still fall back to config.enabled.
|
||||
summarization_requested = cfg.get("summarization_enabled", False)
|
||||
summarization_middleware = _create_summarization_middleware(app_config=resolved_app_config, force_enabled=summarization_requested)
|
||||
if summarization_middleware is not None:
|
||||
middlewares.append(summarization_middleware)
|
||||
|
||||
|
||||
@@ -303,7 +303,10 @@ log_system:
|
||||
label: "日志系统"
|
||||
|
||||
summarization:
|
||||
enabled: true
|
||||
# 开关已移到聊天框下方(参考文献下面)的「上下文压缩」按钮,默认关闭。
|
||||
# 这里 enabled 仅作为非 Web 路径(渠道/定时任务)的后备开关;下面的 trigger/keep
|
||||
# 等仅是调参,Web 端是否压缩由前端按钮(summarization_enabled)决定。
|
||||
enabled: false
|
||||
# model_name: deepseek-chat
|
||||
trigger:
|
||||
- type: tokens
|
||||
|
||||
Reference in new issue
Block a user