Merge branch 'gzy_iframedf' into 'master'
feat: iframe传 文件进入 See merge request knowledge-base-bj/deerflow-server!93
This commit is contained in:
commit
9ebde3b315
3 files changed
+139
-2
No files matched your search
@@ -100,3 +100,37 @@ test("maps iframe model selection into stream context model_name", () => {
|
|||||||
model_name: "OpenAI3",
|
model_name: "OpenAI3",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("maps externally uploaded files into stream additional_kwargs.files", () => {
|
||||||
|
const result = parseBrainyChatSubmitMessage({
|
||||||
|
source: "brainy-extension",
|
||||||
|
type: "brainy:chat-submit",
|
||||||
|
requestId: "iframe-789",
|
||||||
|
payload: {
|
||||||
|
text: "网摘",
|
||||||
|
prompt: "网摘",
|
||||||
|
uploadedFiles: [
|
||||||
|
{
|
||||||
|
filename: "LLM Wiki 接口说明.md",
|
||||||
|
size: "7002",
|
||||||
|
path: "/mnt/user-data/uploads/LLM Wiki 接口说明.md",
|
||||||
|
status: "uploaded",
|
||||||
|
artifact_url:
|
||||||
|
"/api/threads/445ca7a7-9737-44f0-8eea-6887776f4f9c/artifacts/mnt/user-data/uploads/LLM%20Wiki%20%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E.md",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.submitText, "网摘");
|
||||||
|
assert.deepEqual(result.additionalKwargs?.files, [
|
||||||
|
{
|
||||||
|
filename: "LLM Wiki 接口说明.md",
|
||||||
|
size: "7002",
|
||||||
|
path: "/mnt/user-data/uploads/LLM Wiki 接口说明.md",
|
||||||
|
status: "uploaded",
|
||||||
|
artifact_url:
|
||||||
|
"/api/threads/445ca7a7-9737-44f0-8eea-6887776f4f9c/artifacts/mnt/user-data/uploads/LLM%20Wiki%20%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E.md",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -26,6 +26,14 @@ type BrainyModelSelection = {
|
|||||||
value?: string;
|
value?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BrainyUploadedFile = {
|
||||||
|
filename?: string;
|
||||||
|
size?: string | number;
|
||||||
|
path?: string;
|
||||||
|
status?: string;
|
||||||
|
artifact_url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type BrainyChatSubmitMessage = {
|
export type BrainyChatSubmitMessage = {
|
||||||
source?: string;
|
source?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
@@ -35,6 +43,7 @@ export type BrainyChatSubmitMessage = {
|
|||||||
pageContext?: BrainyPageContext;
|
pageContext?: BrainyPageContext;
|
||||||
screenshot?: BrainyScreenshot;
|
screenshot?: BrainyScreenshot;
|
||||||
model?: BrainyModelSelection;
|
model?: BrainyModelSelection;
|
||||||
|
uploadedFiles?: BrainyUploadedFile[];
|
||||||
};
|
};
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
};
|
};
|
||||||
@@ -253,6 +262,36 @@ function buildScreenshotFileParts(
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeUploadedFiles(
|
||||||
|
uploadedFiles: BrainyUploadedFile[] | undefined,
|
||||||
|
): Record<string, unknown>[] {
|
||||||
|
if (!Array.isArray(uploadedFiles)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadedFiles
|
||||||
|
.map((file) => {
|
||||||
|
const filename = trimToUndefined(file.filename);
|
||||||
|
if (!filename) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = String(file.size ?? "0");
|
||||||
|
const path = trimToUndefined(file.path);
|
||||||
|
const status = trimToUndefined(file.status);
|
||||||
|
const artifactUrl = trimToUndefined(file.artifact_url);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename,
|
||||||
|
size,
|
||||||
|
...(path ? { path } : {}),
|
||||||
|
...(status ? { status } : {}),
|
||||||
|
...(artifactUrl ? { artifact_url: artifactUrl } : {}),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((file): file is Record<string, unknown> => file !== null);
|
||||||
|
}
|
||||||
|
|
||||||
export function parseBrainyChatSubmitMessage(
|
export function parseBrainyChatSubmitMessage(
|
||||||
data: BrainyChatSubmitMessage,
|
data: BrainyChatSubmitMessage,
|
||||||
): ParsedBrainyChatSubmitMessage {
|
): ParsedBrainyChatSubmitMessage {
|
||||||
@@ -268,6 +307,7 @@ export function parseBrainyChatSubmitMessage(
|
|||||||
});
|
});
|
||||||
const requestId = trimToUndefined(data.requestId);
|
const requestId = trimToUndefined(data.requestId);
|
||||||
const modelName = trimToUndefined(data.payload?.model?.value);
|
const modelName = trimToUndefined(data.payload?.model?.value);
|
||||||
|
const uploadedFiles = normalizeUploadedFiles(data.payload?.uploadedFiles);
|
||||||
|
|
||||||
const iframeContext: Record<string, unknown> = {};
|
const iframeContext: Record<string, unknown> = {};
|
||||||
if (requestId) {
|
if (requestId) {
|
||||||
@@ -281,6 +321,13 @@ export function parseBrainyChatSubmitMessage(
|
|||||||
submitText,
|
submitText,
|
||||||
files,
|
files,
|
||||||
...(Object.keys(iframeContext).length > 0 ? { iframeContext } : {}),
|
...(Object.keys(iframeContext).length > 0 ? { iframeContext } : {}),
|
||||||
...(promptPrefix ? { additionalKwargs: { prompt_prefix: promptPrefix } } : {}),
|
...(promptPrefix || uploadedFiles.length > 0
|
||||||
|
? {
|
||||||
|
additionalKwargs: {
|
||||||
|
...(promptPrefix ? { prompt_prefix: promptPrefix } : {}),
|
||||||
|
...(uploadedFiles.length > 0 ? { files: uploadedFiles } : {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+57
-1
@@ -1,8 +1,10 @@
|
|||||||
"""Middleware to inject uploaded files information into agent context."""
|
"""Middleware to inject uploaded files information into agent context."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import NotRequired, override
|
from typing import NotRequired, override
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from langchain.agents import AgentState
|
from langchain.agents import AgentState
|
||||||
from langchain.agents.middleware import AgentMiddleware
|
from langchain.agents.middleware import AgentMiddleware
|
||||||
@@ -23,6 +25,7 @@ _OUTLINE_PREVIEW_LINES = 5
|
|||||||
# generic read_file/grep guidance made the agent try to parse image binaries
|
# generic read_file/grep guidance made the agent try to parse image binaries
|
||||||
# with bash/python, which fails and wastes turns.
|
# with bash/python, which fails and wastes turns.
|
||||||
_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp"}
|
_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp"}
|
||||||
|
_ARTIFACT_URL_MARKER = "/artifacts/"
|
||||||
|
|
||||||
|
|
||||||
def _extract_outline_for_file(file_path: Path) -> tuple[list[dict], list[str]]:
|
def _extract_outline_for_file(file_path: Path) -> tuple[list[dict], list[str]]:
|
||||||
@@ -162,6 +165,58 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
|
|||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _import_file_from_artifact_url(
|
||||||
|
self,
|
||||||
|
file_info: dict,
|
||||||
|
uploads_dir: Path | None,
|
||||||
|
) -> bool:
|
||||||
|
"""Copy a previously uploaded file into the current thread when possible.
|
||||||
|
|
||||||
|
iframe-style callers may pass file metadata that points at an upload
|
||||||
|
from another DeerFlow thread. The current run thread still needs a real
|
||||||
|
file under its own ``/mnt/user-data/uploads`` directory, otherwise this
|
||||||
|
middleware will filter the entry out during existence checks.
|
||||||
|
"""
|
||||||
|
filename = file_info.get("filename") or ""
|
||||||
|
artifact_url = str(file_info.get("artifact_url") or "").strip()
|
||||||
|
if not filename or uploads_dir is None or not artifact_url:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not artifact_url.startswith("/api/threads/") or _ARTIFACT_URL_MARKER not in artifact_url:
|
||||||
|
return False
|
||||||
|
|
||||||
|
prefix, encoded_virtual_path = artifact_url.split(_ARTIFACT_URL_MARKER, 1)
|
||||||
|
source_thread_id = prefix.removeprefix("/api/threads/").strip("/")
|
||||||
|
if not source_thread_id or not encoded_virtual_path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
virtual_path = "/" + unquote(encoded_virtual_path.lstrip("/"))
|
||||||
|
if Path(virtual_path).name != filename:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
source_path = self._paths.resolve_virtual_path(
|
||||||
|
source_thread_id,
|
||||||
|
virtual_path,
|
||||||
|
user_id=get_effective_user_id(),
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
logger.debug("Failed to resolve uploaded artifact path: %s", artifact_url, exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not source_path.is_file():
|
||||||
|
return False
|
||||||
|
|
||||||
|
uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
target_path = uploads_dir / filename
|
||||||
|
shutil.copy2(source_path, target_path)
|
||||||
|
|
||||||
|
source_markdown = source_path.with_suffix(".md")
|
||||||
|
if source_markdown.is_file() and source_markdown != source_path:
|
||||||
|
shutil.copy2(source_markdown, target_path.with_suffix(".md"))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def _files_from_kwargs(self, message: HumanMessage, uploads_dir: Path | None = None) -> list[dict] | None:
|
def _files_from_kwargs(self, message: HumanMessage, uploads_dir: Path | None = None) -> list[dict] | None:
|
||||||
"""Extract file info from message additional_kwargs.files.
|
"""Extract file info from message additional_kwargs.files.
|
||||||
|
|
||||||
@@ -189,7 +244,8 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
|
|||||||
if not filename or Path(filename).name != filename:
|
if not filename or Path(filename).name != filename:
|
||||||
continue
|
continue
|
||||||
if uploads_dir is not None and not (uploads_dir / filename).is_file():
|
if uploads_dir is not None and not (uploads_dir / filename).is_file():
|
||||||
continue
|
if not self._import_file_from_artifact_url(f, uploads_dir):
|
||||||
|
continue
|
||||||
files.append(
|
files.append(
|
||||||
{
|
{
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
|
|||||||
Reference in new issue
Block a user