fix: improve llmwiki search integration
This commit is contained in:
1 parent
4809ee7db3
commit
56044d9cb7
20 files changed
+504
-66
No files matched your search
Vendored
+9
-8
@@ -11,8 +11,7 @@
|
|||||||
"--host",
|
"--host",
|
||||||
"0.0.0.0",
|
"0.0.0.0",
|
||||||
"--port",
|
"--port",
|
||||||
"8001",
|
"8001"
|
||||||
"--reload"
|
|
||||||
],
|
],
|
||||||
"cwd": "${workspaceFolder}/offline-backend-20260512/backend",
|
"cwd": "${workspaceFolder}/offline-backend-20260512/backend",
|
||||||
"python": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
|
"python": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
|
||||||
@@ -24,6 +23,7 @@
|
|||||||
"WEKNORA_ADMIN_PASSWORD": "1qaz@WSX"
|
"WEKNORA_ADMIN_PASSWORD": "1qaz@WSX"
|
||||||
},
|
},
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
"redirectOutput": true,
|
||||||
"justMyCode": false,
|
"justMyCode": false,
|
||||||
"subProcess": true
|
"subProcess": true
|
||||||
},
|
},
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"name": "DeerFlow: Frontend Vite (5174)",
|
"name": "DeerFlow: Frontend Vite (5174)",
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "npm run dev -- --host 0.0.0.0 --port 5174",
|
"command": "npm.cmd run dev -- --host 0.0.0.0 --port 5174",
|
||||||
"cwd": "${workspaceFolder}/frontend-web",
|
"cwd": "${workspaceFolder}/frontend-web",
|
||||||
"env": {
|
"env": {
|
||||||
"VITE_BACKEND_BASE_URL": "http://127.0.0.1:8001",
|
"VITE_BACKEND_BASE_URL": "http://127.0.0.1:8001",
|
||||||
@@ -40,21 +40,22 @@
|
|||||||
"serverReadyAction": {
|
"serverReadyAction": {
|
||||||
"pattern": "Local:\\s+(https?://[^\\s]+)",
|
"pattern": "Local:\\s+(https?://[^\\s]+)",
|
||||||
"uriFormat": "%s",
|
"uriFormat": "%s",
|
||||||
"action": "debugWithChrome",
|
"action": "openExternally"
|
||||||
"webRoot": "${workspaceFolder}/frontend-web"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "DeerFlow: Open Frontend in Chrome",
|
"name": "DeerFlow: Open Frontend in Chrome",
|
||||||
"type": "pwa-chrome",
|
"type": "node-terminal",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"url": "http://127.0.0.1:5174/",
|
"preLaunchTask": "DeerFlow: Start Full Stack and Wait",
|
||||||
"webRoot": "${workspaceFolder}/frontend-web"
|
"command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"Start-Process 'http://127.0.0.1:5174/'\"",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
"name": "DeerFlow: Full Stack (Backend + Frontend)",
|
"name": "DeerFlow: Full Stack (Backend + Frontend)",
|
||||||
|
"preLaunchTask": "DeerFlow: Stop Full Stack",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
"DeerFlow: Backend FastAPI (8001)",
|
"DeerFlow: Backend FastAPI (8001)",
|
||||||
"DeerFlow: Frontend Vite (5174)"
|
"DeerFlow: Frontend Vite (5174)"
|
||||||
|
|||||||
Vendored
+52
@@ -1,6 +1,58 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "DeerFlow: Start Full Stack and Wait",
|
||||||
|
"type": "process",
|
||||||
|
"command": "powershell.exe",
|
||||||
|
"args": [
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-File",
|
||||||
|
"${workspaceFolder}/scripts/start-all-and-wait-frontend.ps1"
|
||||||
|
],
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "DeerFlow: Start Full Stack",
|
||||||
|
"type": "process",
|
||||||
|
"command": "powershell.exe",
|
||||||
|
"args": [
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-File",
|
||||||
|
"${workspaceFolder}/scripts/start-all.ps1"
|
||||||
|
],
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "DeerFlow: Stop Full Stack",
|
||||||
|
"type": "process",
|
||||||
|
"command": "powershell.exe",
|
||||||
|
"args": [
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-File",
|
||||||
|
"${workspaceFolder}/scripts/stop-all.ps1"
|
||||||
|
],
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "DeerFlow: Status",
|
||||||
|
"type": "process",
|
||||||
|
"command": "powershell.exe",
|
||||||
|
"args": [
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-File",
|
||||||
|
"${workspaceFolder}/scripts/status.ps1"
|
||||||
|
],
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "DeerFlow: Check Local Dev Environment",
|
"label": "DeerFlow: Check Local Dev Environment",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
|||||||
@@ -295,17 +295,19 @@ function ToolCall({
|
|||||||
</ChainOfThoughtStep>
|
</ChainOfThoughtStep>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (name === "web_search" || searchItems.length > 0) {
|
if (name === "web_search" || searchItems.length > 0) {
|
||||||
const query = resolveSearchQuery(args, result);
|
const query = resolveSearchQuery(args, result);
|
||||||
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
|
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
|
||||||
if (query) {
|
if (query) {
|
||||||
label =
|
label =
|
||||||
name === "web_search"
|
name === "web_search"
|
||||||
? t.toolCalls.searchOnWebFor(query)
|
? `知识库检索 "${query}"`
|
||||||
: t.toolCalls.searchFor(query);
|
: t.toolCalls.searchFor(query);
|
||||||
}
|
} else if (name === "web_search") {
|
||||||
return (
|
label = "知识库检索";
|
||||||
<ChainOfThoughtStep key={id} label={label} icon={SearchIcon}>
|
}
|
||||||
|
return (
|
||||||
|
<ChainOfThoughtStep key={id} label={label} icon={SearchIcon}>
|
||||||
{searchItems.length > 0 && (
|
{searchItems.length > 0 && (
|
||||||
<WebSearchResultsList results={searchItems} />
|
<WebSearchResultsList results={searchItems} />
|
||||||
)}
|
)}
|
||||||
@@ -520,12 +522,12 @@ function ToolCall({
|
|||||||
icon={WrenchIcon}
|
icon={WrenchIcon}
|
||||||
></ChainOfThoughtStep>
|
></ChainOfThoughtStep>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GenericCoTStep<T extends string = string> {
|
interface GenericCoTStep<T extends string = string> {
|
||||||
id?: string;
|
id?: string;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
type: T;
|
type: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,9 +101,9 @@ def _agent_bound_ids(body: Any) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def resolve_requested_llmwiki_knowledge_base_ids(body: Any) -> list[str]:
|
def resolve_requested_llmwiki_knowledge_base_ids(body: Any) -> list[str]:
|
||||||
"""Return selected ids plus custom-agent defaults, de-duplicated in order."""
|
"""Return knowledge bases explicitly selected for the current conversation."""
|
||||||
|
|
||||||
return list(dict.fromkeys([*_agent_bound_ids(body), *_body_selected_ids(body)]))
|
return _body_selected_ids(body)
|
||||||
|
|
||||||
|
|
||||||
def llmwiki_selection_requested(body: Any) -> bool:
|
def llmwiki_selection_requested(body: Any) -> bool:
|
||||||
|
|||||||
@@ -1045,6 +1045,10 @@ async def start_run(
|
|||||||
body_context["rag_mode_enabled"] = True
|
body_context["rag_mode_enabled"] = True
|
||||||
if "llmwiki_knowledge_base_ids" not in body_context and llmwiki_rag.knowledge_base_ids:
|
if "llmwiki_knowledge_base_ids" not in body_context and llmwiki_rag.knowledge_base_ids:
|
||||||
body_context["llmwiki_knowledge_base_ids"] = llmwiki_rag.knowledge_base_ids
|
body_context["llmwiki_knowledge_base_ids"] = llmwiki_rag.knowledge_base_ids
|
||||||
|
if not llmwiki_rag.knowledge_base_ids:
|
||||||
|
existing_excluded = list(body_context.get("excluded_tools") or getattr(body, "excluded_tools", None) or [])
|
||||||
|
if "llmwiki_search" not in existing_excluded:
|
||||||
|
body_context["excluded_tools"] = [*existing_excluded, "llmwiki_search"]
|
||||||
# Promote whitelisted top-level body fields into the context map so
|
# Promote whitelisted top-level body fields into the context map so
|
||||||
# callers can pass them either nested under ``context`` or as flat
|
# callers can pass them either nested under ``context`` or as flat
|
||||||
# top-level fields (``excluded_tools``, ``skill_stop_names``, …).
|
# top-level fields (``excluded_tools``, ``skill_stop_names``, …).
|
||||||
|
|||||||
+22
-8
@@ -2,11 +2,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from deerflow.config.agents_config import load_agent_soul
|
from deerflow.config.agents_config import load_agent_soul
|
||||||
from deerflow.skills.storage import get_or_new_skill_storage
|
from deerflow.skills.storage import get_or_new_skill_storage
|
||||||
@@ -22,7 +23,20 @@ logger = logging.getLogger(__name__)
|
|||||||
# Truthy strings for boolean env vars. Mirrors the convention used by
|
# Truthy strings for boolean env vars. Mirrors the convention used by
|
||||||
# DEER_FLOW_AUTH_DISABLED so operators don't have to learn yet another
|
# DEER_FLOW_AUTH_DISABLED so operators don't have to learn yet another
|
||||||
# spelling for "yes, turn this on".
|
# spelling for "yes, turn this on".
|
||||||
_TRUTHY = frozenset({"1", "true", "yes", "on", "y"})
|
_TRUTHY = frozenset({"1", "true", "yes", "on", "y"})
|
||||||
|
_BEIJING_TZ = ZoneInfo("Asia/Shanghai")
|
||||||
|
|
||||||
|
|
||||||
|
def _current_time_context() -> str:
|
||||||
|
now = datetime.now(_BEIJING_TZ)
|
||||||
|
return (
|
||||||
|
f"\n<current_datetime timezone=\"Asia/Shanghai\">{now.strftime('%Y-%m-%d %H:%M:%S, %A')}</current_datetime>"
|
||||||
|
"\n<time_grounding>"
|
||||||
|
"When interpreting relative dates such as 今天, 昨天, 近几天, 最近, this week, or last month, "
|
||||||
|
"use current_datetime as the only reference point. Before calling web_search, rewrite relative "
|
||||||
|
"time expressions into concrete dates or date ranges in the query; never infer a stale year."
|
||||||
|
"</time_grounding>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _is_offline_mode() -> bool:
|
def _is_offline_mode() -> bool:
|
||||||
@@ -761,7 +775,7 @@ RAG/reference display mode is enabled.
|
|||||||
acp_section=acp_and_mounts_section,
|
acp_section=acp_and_mounts_section,
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = prompt + rag_mode_section + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>"
|
prompt = prompt + rag_mode_section + _current_time_context()
|
||||||
return prompt
|
return prompt
|
||||||
|
|
||||||
|
|
||||||
@@ -1359,7 +1373,7 @@ When a search, knowledge, skill, or MCP tool returns a result list:
|
|||||||
acp_section=acp_and_mounts_section,
|
acp_section=acp_and_mounts_section,
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = prompt + rag_mode_section + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>"
|
prompt = prompt + rag_mode_section + _current_time_context()
|
||||||
|
|
||||||
if is_notebook_mode:
|
if is_notebook_mode:
|
||||||
source_ids = notebook_source_ids or []
|
source_ids = notebook_source_ids or []
|
||||||
|
|||||||
+64
-14
@@ -6,8 +6,10 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from langchain.tools import tool
|
from langchain.tools import tool
|
||||||
@@ -16,6 +18,12 @@ from deerflow.config import get_app_config
|
|||||||
from deerflow.config.tool_config import ToolConfig
|
from deerflow.config.tool_config import ToolConfig
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
_BEIJING_TZ = ZoneInfo("Asia/Shanghai")
|
||||||
|
|
||||||
|
|
||||||
|
def _current_time_hint() -> str:
|
||||||
|
now = datetime.now(_BEIJING_TZ)
|
||||||
|
return now.strftime("%Y-%m-%d %H:%M:%S, %A, Asia/Shanghai")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -87,6 +95,23 @@ def _text(value: Any) -> str:
|
|||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata(item: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
value = item.get("metadata")
|
||||||
|
return value if isinstance(value, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _first_text(*values: Any) -> str:
|
||||||
|
for value in values:
|
||||||
|
text = _text(value).strip()
|
||||||
|
if text:
|
||||||
|
return text
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _looks_like_url(value: str) -> bool:
|
||||||
|
return value.startswith(("http://", "https://"))
|
||||||
|
|
||||||
|
|
||||||
def _error_result(query: str, message: str) -> str:
|
def _error_result(query: str, message: str) -> str:
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
@@ -100,10 +125,13 @@ def _error_result(query: str, message: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _normalize_results(data: Any, settings: ConfigurableSearchSettings) -> list[dict[str, str]]:
|
def _normalize_results(data: Any, settings: ConfigurableSearchSettings) -> list[dict[str, str]]:
|
||||||
if not isinstance(data, dict):
|
if isinstance(data, list):
|
||||||
raise ValueError("search API response must be a JSON object")
|
raw_results = data
|
||||||
|
elif isinstance(data, dict):
|
||||||
|
raw_results = data.get("results")
|
||||||
|
else:
|
||||||
|
raise ValueError("search API response must be a JSON object or array")
|
||||||
|
|
||||||
raw_results = data.get("results")
|
|
||||||
if not isinstance(raw_results, list):
|
if not isinstance(raw_results, list):
|
||||||
raise ValueError("search API response field 'results' must be an array")
|
raise ValueError("search API response field 'results' must be an array")
|
||||||
|
|
||||||
@@ -113,8 +141,21 @@ def _normalize_results(data: Any, settings: ConfigurableSearchSettings) -> list[
|
|||||||
logger.warning("Ignoring non-object search result: %r", item)
|
logger.warning("Ignoring non-object search result: %r", item)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
rec_uuid = _text(item.get("recUuid")).strip()
|
metadata = _metadata(item)
|
||||||
original_url = _text(item.get("url")).strip()
|
rec_uuid = _first_text(item.get("recUuid"), item.get("rec_uuid"), metadata.get("recUuid"), metadata.get("rec_uuid"))
|
||||||
|
source = _first_text(item.get("source"), item.get("source1"), metadata.get("source"), metadata.get("source1"))
|
||||||
|
original_url = _first_text(
|
||||||
|
item.get("url"),
|
||||||
|
item.get("link"),
|
||||||
|
item.get("href"),
|
||||||
|
item.get("source_url"),
|
||||||
|
metadata.get("url"),
|
||||||
|
metadata.get("link"),
|
||||||
|
metadata.get("href"),
|
||||||
|
metadata.get("source_url"),
|
||||||
|
)
|
||||||
|
if not original_url and _looks_like_url(source):
|
||||||
|
original_url = source
|
||||||
result_url = original_url
|
result_url = original_url
|
||||||
if rec_uuid and settings.result_url_template:
|
if rec_uuid and settings.result_url_template:
|
||||||
result_url = settings.result_url_template.replace(
|
result_url = settings.result_url_template.replace(
|
||||||
@@ -122,14 +163,19 @@ def _normalize_results(data: Any, settings: ConfigurableSearchSettings) -> list[
|
|||||||
quote(rec_uuid, safe=""),
|
quote(rec_uuid, safe=""),
|
||||||
)
|
)
|
||||||
|
|
||||||
normalized.append(
|
normalized_item = {
|
||||||
{
|
"recUuid": rec_uuid,
|
||||||
"recUuid": rec_uuid,
|
"content": _first_text(item.get("content"), item.get("page_content"), metadata.get("content")),
|
||||||
"content": _text(item.get("content")),
|
"title": _first_text(item.get("title"), item.get("m_title"), metadata.get("m_title")),
|
||||||
"title": _text(item.get("title")),
|
"url": result_url,
|
||||||
"url": result_url,
|
}
|
||||||
}
|
if source:
|
||||||
)
|
normalized_item["source"] = source
|
||||||
|
publish_time = _first_text(item.get("time"), item.get("publish_time"), item.get("m_publish"), metadata.get("m_publish"))
|
||||||
|
if publish_time:
|
||||||
|
normalized_item["time"] = publish_time
|
||||||
|
normalized_item["publish_time"] = publish_time
|
||||||
|
normalized.append(normalized_item)
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
@@ -193,10 +239,14 @@ async def web_search_tool(query: str) -> str:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: Focused search keywords or a natural-language retrieval question.
|
query: Focused search keywords or a natural-language retrieval question.
|
||||||
|
Interpret relative time expressions against the current time in
|
||||||
|
Asia/Shanghai, and include concrete dates/date ranges in the query
|
||||||
|
when the user asks about recent periods.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
settings = ConfigurableSearchSettings.from_tool_config(get_app_config().get_tool_config("web_search"))
|
settings = ConfigurableSearchSettings.from_tool_config(get_app_config().get_tool_config("web_search"))
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
return _error_result(query, str(exc))
|
return _error_result(query, str(exc))
|
||||||
return await execute_search(query, settings)
|
grounded_query = f"{query.strip()}\n当前时间: {_current_time_hint()}" if query.strip() else query
|
||||||
|
return await execute_search(grounded_query, settings)
|
||||||
+14
-17
@@ -71,8 +71,8 @@ async def llmwiki_search_tool(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Search the LLMWiki knowledge selected for this conversation.
|
"""Search the LLMWiki knowledge selected for this conversation.
|
||||||
|
|
||||||
Use this tool when an answer can benefit from the user's managed knowledge
|
Use this tool only when the current conversation explicitly selected one
|
||||||
bases or the shared public LLMWiki. It only retrieves passages; it cannot
|
or more LLMWiki knowledge bases. It only retrieves passages; it cannot
|
||||||
upload, edit, delete, publish or change permissions.
|
upload, edit, delete, publish or change permissions.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -90,26 +90,23 @@ async def llmwiki_search_tool(
|
|||||||
selected_ids = selected_knowledge_base_ids(context)
|
selected_ids = selected_knowledge_base_ids(context)
|
||||||
allowed_ids = _agent_allowed_ids(context)
|
allowed_ids = _agent_allowed_ids(context)
|
||||||
if selected_ids is None:
|
if selected_ids is None:
|
||||||
selected_ids = allowed_ids
|
return "No LLMWiki knowledge base is selected for this conversation."
|
||||||
elif allowed_ids is not None and not set(selected_ids).issubset(set(allowed_ids)):
|
elif allowed_ids is not None and not set(selected_ids).issubset(set(allowed_ids)):
|
||||||
return "The conversation requested knowledge outside this agent's configured LLMWiki scope."
|
return "The conversation requested knowledge outside this agent's configured LLMWiki scope."
|
||||||
|
|
||||||
if selected_ids == []:
|
if selected_ids == []:
|
||||||
return "No LLMWiki knowledge base is selected for this conversation."
|
return "No LLMWiki knowledge base is selected for this conversation."
|
||||||
if selected_ids is not None:
|
rows = []
|
||||||
rows = []
|
for mapping_id in selected_ids:
|
||||||
for mapping_id in selected_ids:
|
row = await store.get_authorized(
|
||||||
row = await store.get_authorized(
|
mapping_id,
|
||||||
mapping_id,
|
user_id,
|
||||||
user_id,
|
write=False,
|
||||||
write=False,
|
is_admin=is_admin,
|
||||||
is_admin=is_admin,
|
)
|
||||||
)
|
if row is None:
|
||||||
if row is None:
|
return f"Knowledge base {mapping_id!r} is not available to the current user."
|
||||||
return f"Knowledge base {mapping_id!r} is not available to the current user."
|
rows.append(row)
|
||||||
rows.append(row)
|
|
||||||
else:
|
|
||||||
rows = await store.list_visible(user_id, scope="all", is_admin=False)
|
|
||||||
if not rows:
|
if not rows:
|
||||||
return "No authorized LLMWiki knowledge bases are available."
|
return "No authorized LLMWiki knowledge bases are available."
|
||||||
|
|
||||||
|
|||||||
+76
@@ -104,6 +104,82 @@ async def test_configurable_search_handles_empty_results() -> None:
|
|||||||
assert result == {"query": "nothing", "total_results": 0, "results": []}
|
assert result == {"query": "nothing", "total_results": 0, "results": []}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_configurable_search_accepts_root_array_results() -> None:
|
||||||
|
async def handler(_: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json=[
|
||||||
|
{
|
||||||
|
"recUuid": "root-array-record",
|
||||||
|
"content": "direct array content",
|
||||||
|
"title": "direct array title",
|
||||||
|
"url": "https://original.example.test/root-array",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = json.loads(
|
||||||
|
await execute_search(
|
||||||
|
"array response",
|
||||||
|
_settings(result_url_template=""),
|
||||||
|
transport=httpx.MockTransport(handler),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"query": "array response",
|
||||||
|
"total_results": 1,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"recUuid": "root-array-record",
|
||||||
|
"content": "direct array content",
|
||||||
|
"title": "direct array title",
|
||||||
|
"url": "https://original.example.test/root-array",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_configurable_search_normalizes_metadata_wrapped_results() -> None:
|
||||||
|
async def handler(_: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json=[
|
||||||
|
{
|
||||||
|
"page_content": "outer content",
|
||||||
|
"metadata": {
|
||||||
|
"m_title": "metadata title",
|
||||||
|
"content": "metadata content",
|
||||||
|
"source1": "https://source.example.test/article/1",
|
||||||
|
"m_publish": "2026-08-13",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = json.loads(
|
||||||
|
await execute_search(
|
||||||
|
"metadata response",
|
||||||
|
_settings(result_url_template=""),
|
||||||
|
transport=httpx.MockTransport(handler),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["results"] == [
|
||||||
|
{
|
||||||
|
"recUuid": "",
|
||||||
|
"content": "outer content",
|
||||||
|
"title": "metadata title",
|
||||||
|
"source": "https://source.example.test/article/1",
|
||||||
|
"time": "2026-08-13",
|
||||||
|
"publish_time": "2026-08-13",
|
||||||
|
"url": "https://source.example.test/article/1",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("response_factory", "expected_error"),
|
("response_factory", "expected_error"),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
@@ -128,6 +129,18 @@ def test_conversation_selection_is_deduplicated_and_absence_is_distinct() -> Non
|
|||||||
assert selected_knowledge_base_ids({"llmwiki_knowledge_base_ids": ["kb-one", "kb-one", "", "kb-two"]}) == ["kb-one", "kb-two"]
|
assert selected_knowledge_base_ids({"llmwiki_knowledge_base_ids": ["kb-one", "kb-one", "", "kb-two"]}) == ["kb-one", "kb-two"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_llmwiki_rag_only_uses_explicit_conversation_selection() -> None:
|
||||||
|
from app.gateway.llmwiki_rag import resolve_requested_llmwiki_knowledge_base_ids
|
||||||
|
|
||||||
|
body = SimpleNamespace(
|
||||||
|
assistant_id="agent-with-bound-kb",
|
||||||
|
context={},
|
||||||
|
metadata={"llmwiki_knowledge_base_ids": ["kb-one", "kb-one", "", "kb-two"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resolve_requested_llmwiki_knowledge_base_ids(body) == ["kb-one", "kb-two"]
|
||||||
|
|
||||||
|
|
||||||
def test_weknora_frame_bootstrap_accepts_query_token_only_for_frame_route() -> None:
|
def test_weknora_frame_bootstrap_accepts_query_token_only_for_frame_route() -> None:
|
||||||
from app.gateway.deps import get_request_access_token
|
from app.gateway.deps import get_request_access_token
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$RootDir = Resolve-Path (Join-Path $ScriptDir "..")
|
||||||
|
|
||||||
|
$BackendDir = Join-Path $RootDir "offline-backend-20260512\backend"
|
||||||
|
$FrontendDir = Join-Path $RootDir "frontend-web"
|
||||||
|
$RuntimeDir = Join-Path $RootDir ".runtime"
|
||||||
|
$LogDir = Join-Path $RuntimeDir "logs"
|
||||||
|
$PidDir = Join-Path $RuntimeDir "pids"
|
||||||
|
|
||||||
|
$BackendHost = if ($env:BACKEND_HOST) { $env:BACKEND_HOST } else { "0.0.0.0" }
|
||||||
|
$BackendPort = if ($env:BACKEND_PORT) { $env:BACKEND_PORT } else { "8001" }
|
||||||
|
$FrontendHost = if ($env:FRONTEND_HOST) { $env:FRONTEND_HOST } else { "0.0.0.0" }
|
||||||
|
$FrontendPort = if ($env:FRONTEND_PORT) { $env:FRONTEND_PORT } else { "5174" }
|
||||||
|
|
||||||
|
$HostIp = if ($env:HOST_IP) { $env:HOST_IP } else { "127.0.0.1" }
|
||||||
|
$BackendPublicBaseUrl = if ($env:BACKEND_PUBLIC_BASE_URL) { $env:BACKEND_PUBLIC_BASE_URL } else { "http://${HostIp}:${BackendPort}" }
|
||||||
|
$FrontendPublicBaseUrl = if ($env:FRONTEND_PUBLIC_BASE_URL) { $env:FRONTEND_PUBLIC_BASE_URL } else { "http://${HostIp}:${FrontendPort}" }
|
||||||
|
$GatewayCorsOrigins = if ($env:GATEWAY_CORS_ORIGINS) {
|
||||||
|
$env:GATEWAY_CORS_ORIGINS
|
||||||
|
} else {
|
||||||
|
"http://localhost:${FrontendPort},http://127.0.0.1:${FrontendPort},http://${HostIp}:${FrontendPort},http://localhost:8080,http://127.0.0.1:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
$BackendPython = Join-Path $BackendDir ".venv\Scripts\python.exe"
|
||||||
|
$BackendVendorDir = if ($env:BACKEND_VENDOR_DIR) { $env:BACKEND_VENDOR_DIR } else { Join-Path $BackendDir ".deer-flow\python-vendor" }
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $LogDir, $PidDir | Out-Null
|
||||||
|
|
||||||
|
function Test-PidRunning {
|
||||||
|
param([string]$PidFile)
|
||||||
|
if (-not (Test-Path -LiteralPath $PidFile)) { return $false }
|
||||||
|
$rawPid = (Get-Content -LiteralPath $PidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||||
|
$processId = 0
|
||||||
|
if (-not [int]::TryParse([string]$rawPid, [ref]$processId)) { return $false }
|
||||||
|
return [bool](Get-Process -Id $processId -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Stop-ProcessTree {
|
||||||
|
param([int]$ProcessId)
|
||||||
|
$children = Get-CimInstance Win32_Process -Filter "ParentProcessId=$ProcessId" -ErrorAction SilentlyContinue
|
||||||
|
foreach ($child in $children) {
|
||||||
|
Stop-ProcessTree -ProcessId ([int]$child.ProcessId)
|
||||||
|
}
|
||||||
|
$proc = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
|
||||||
|
if ($proc) {
|
||||||
|
Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-PSLiteral {
|
||||||
|
param([string]$Value)
|
||||||
|
return "'" + ($Value -replace "'", "''") + "'"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
. "$PSScriptRoot\env.ps1"
|
||||||
|
|
||||||
|
& "$PSScriptRoot\stop-all.ps1"
|
||||||
|
& "$PSScriptRoot\start-all.ps1"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
. "$PSScriptRoot\env.ps1"
|
||||||
|
|
||||||
|
& "$PSScriptRoot\start-all.ps1"
|
||||||
|
|
||||||
|
$deadline = (Get-Date).AddSeconds(60)
|
||||||
|
$url = "$FrontendPublicBaseUrl/"
|
||||||
|
Write-Host "Waiting for frontend: $url"
|
||||||
|
|
||||||
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
try {
|
||||||
|
$response = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 2
|
||||||
|
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
|
||||||
|
Write-Host "Frontend is ready: $url"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Timed out waiting for frontend: $url"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
. "$PSScriptRoot\env.ps1"
|
||||||
|
|
||||||
|
& "$PSScriptRoot\start-backend.ps1"
|
||||||
|
& "$PSScriptRoot\start-frontend.ps1"
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
. "$PSScriptRoot\env.ps1"
|
||||||
|
|
||||||
|
$PidFile = Join-Path $PidDir "backend.pid"
|
||||||
|
$LogFile = Join-Path $LogDir "backend.log"
|
||||||
|
|
||||||
|
if (Test-PidRunning $PidFile) {
|
||||||
|
$existingPid = Get-Content -LiteralPath $PidFile | Select-Object -First 1
|
||||||
|
Write-Host "Backend is already running (pid $existingPid)."
|
||||||
|
Write-Host "URL: $BackendPublicBaseUrl"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $BackendPython)) {
|
||||||
|
throw "Backend Python not found: $BackendPython. Run uv sync in offline-backend-20260512/backend first."
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
|
||||||
|
New-Item -ItemType Directory -Force -Path $BackendVendorDir | Out-Null
|
||||||
|
|
||||||
|
Write-Host "Starting backend..."
|
||||||
|
Write-Host "Log: $LogFile"
|
||||||
|
|
||||||
|
$RunnerFile = Join-Path $RuntimeDir "run-backend.ps1"
|
||||||
|
$runner = @"
|
||||||
|
`$ErrorActionPreference = "Continue"
|
||||||
|
Set-Location -LiteralPath $(ConvertTo-PSLiteral $BackendDir)
|
||||||
|
`$env:GATEWAY_CORS_ORIGINS = $(ConvertTo-PSLiteral $GatewayCorsOrigins)
|
||||||
|
`$env:PYTHONPATH = $(ConvertTo-PSLiteral ".;$BackendVendorDir")
|
||||||
|
if (-not `$env:WEKNORA_ADMIN_PASSWORD) { `$env:WEKNORA_ADMIN_PASSWORD = "1qaz@WSX" }
|
||||||
|
cmd.exe /c "`"$(($BackendPython -replace '"', '\"'))`" -m uvicorn app.gateway.app:app --host $(($BackendHost -replace '"', '\"')) --port $(($BackendPort -replace '"', '\"')) --reload > `"$(($LogFile -replace '"', '\"'))`" 2>&1"
|
||||||
|
"@
|
||||||
|
Set-Content -LiteralPath $RunnerFile -Value $runner -Encoding UTF8
|
||||||
|
|
||||||
|
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy", "Bypass",
|
||||||
|
"-File", $RunnerFile
|
||||||
|
) -WindowStyle Hidden -PassThru
|
||||||
|
|
||||||
|
Set-Content -LiteralPath $PidFile -Value $proc.Id
|
||||||
|
Write-Host "Backend started (pid $($proc.Id))."
|
||||||
|
Write-Host "URL: $BackendPublicBaseUrl"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
. "$PSScriptRoot\env.ps1"
|
||||||
|
|
||||||
|
$PidFile = Join-Path $PidDir "frontend.pid"
|
||||||
|
$LogFile = Join-Path $LogDir "frontend.log"
|
||||||
|
|
||||||
|
if (Test-PidRunning $PidFile) {
|
||||||
|
$existingPid = Get-Content -LiteralPath $PidFile | Select-Object -First 1
|
||||||
|
Write-Host "Frontend is already running (pid $existingPid)."
|
||||||
|
Write-Host "URL: $FrontendPublicBaseUrl"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$NpmCmd = (Get-Command npm.cmd -ErrorAction SilentlyContinue).Source
|
||||||
|
if (-not $NpmCmd) {
|
||||||
|
throw "npm.cmd was not found in PATH. Install Node.js or add npm.cmd to PATH."
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Write-Host "Starting frontend..."
|
||||||
|
Write-Host "Log: $LogFile"
|
||||||
|
|
||||||
|
$RunnerFile = Join-Path $RuntimeDir "run-frontend.ps1"
|
||||||
|
$runner = @"
|
||||||
|
`$ErrorActionPreference = "Continue"
|
||||||
|
Set-Location -LiteralPath $(ConvertTo-PSLiteral $FrontendDir)
|
||||||
|
`$env:VITE_BACKEND_BASE_URL = $(ConvertTo-PSLiteral $BackendPublicBaseUrl)
|
||||||
|
`$env:VITE_LANGGRAPH_BASE_URL = $(ConvertTo-PSLiteral "$BackendPublicBaseUrl/api")
|
||||||
|
& $(ConvertTo-PSLiteral $NpmCmd) run dev -- --host $(ConvertTo-PSLiteral $FrontendHost) --port $(ConvertTo-PSLiteral $FrontendPort) *> $(ConvertTo-PSLiteral $LogFile)
|
||||||
|
"@
|
||||||
|
Set-Content -LiteralPath $RunnerFile -Value $runner -Encoding UTF8
|
||||||
|
|
||||||
|
$proc = Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy", "Bypass",
|
||||||
|
"-File", $RunnerFile
|
||||||
|
) -WindowStyle Hidden -PassThru
|
||||||
|
|
||||||
|
Set-Content -LiteralPath $PidFile -Value $proc.Id
|
||||||
|
Write-Host "Frontend started (pid $($proc.Id))."
|
||||||
|
Write-Host "URL: $FrontendPublicBaseUrl"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
. "$PSScriptRoot\env.ps1"
|
||||||
|
|
||||||
|
$backendPidFile = Join-Path $PidDir "backend.pid"
|
||||||
|
$frontendPidFile = Join-Path $PidDir "frontend.pid"
|
||||||
|
|
||||||
|
if (Test-PidRunning $backendPidFile) {
|
||||||
|
$pidValue = Get-Content -LiteralPath $backendPidFile | Select-Object -First 1
|
||||||
|
Write-Host "Backend: running (pid $pidValue) $BackendPublicBaseUrl"
|
||||||
|
} else {
|
||||||
|
Write-Host "Backend: stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-PidRunning $frontendPidFile) {
|
||||||
|
$pidValue = Get-Content -LiteralPath $frontendPidFile | Select-Object -First 1
|
||||||
|
Write-Host "Frontend: running (pid $pidValue) $FrontendPublicBaseUrl"
|
||||||
|
} else {
|
||||||
|
Write-Host "Frontend: stopped"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
. "$PSScriptRoot\env.ps1"
|
||||||
|
|
||||||
|
& "$PSScriptRoot\stop-frontend.ps1"
|
||||||
|
& "$PSScriptRoot\stop-backend.ps1"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
. "$PSScriptRoot\env.ps1"
|
||||||
|
|
||||||
|
$PidFile = Join-Path $PidDir "backend.pid"
|
||||||
|
if (-not (Test-Path -LiteralPath $PidFile)) {
|
||||||
|
Write-Host "Backend is not running (no pid file)."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawPid = Get-Content -LiteralPath $PidFile | Select-Object -First 1
|
||||||
|
$processId = 0
|
||||||
|
if (-not [int]::TryParse([string]$rawPid, [ref]$processId) -or -not (Get-Process -Id $processId -ErrorAction SilentlyContinue)) {
|
||||||
|
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Backend is not running (stale pid file removed)."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Stopping backend (pid $processId)..."
|
||||||
|
Stop-ProcessTree -ProcessId $processId
|
||||||
|
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Backend stopped."
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
. "$PSScriptRoot\env.ps1"
|
||||||
|
|
||||||
|
$PidFile = Join-Path $PidDir "frontend.pid"
|
||||||
|
if (-not (Test-Path -LiteralPath $PidFile)) {
|
||||||
|
Write-Host "Frontend is not running (no pid file)."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawPid = Get-Content -LiteralPath $PidFile | Select-Object -First 1
|
||||||
|
$processId = 0
|
||||||
|
if (-not [int]::TryParse([string]$rawPid, [ref]$processId) -or -not (Get-Process -Id $processId -ErrorAction SilentlyContinue)) {
|
||||||
|
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Frontend is not running (stale pid file removed)."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Stopping frontend (pid $processId)..."
|
||||||
|
Stop-ProcessTree -ProcessId $processId
|
||||||
|
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Frontend stopped."
|
||||||
Reference in new issue
Block a user