已实现由外部创建会话
This commit is contained in:
parent
c669b3bb24
commit
4119fdcba7
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import APIRouter, File, HTTPException, UploadFile
|
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from src.agents.middlewares.thread_data_middleware import THREAD_DATA_BASE_DIR
|
from src.agents.middlewares.thread_data_middleware import THREAD_DATA_BASE_DIR
|
||||||
|
|
@ -14,6 +15,8 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/threads/{thread_id}/uploads", tags=["uploads"])
|
router = APIRouter(prefix="/api/threads/{thread_id}/uploads", tags=["uploads"])
|
||||||
|
|
||||||
|
SKILL_UPLOAD_TARGET = "skill"
|
||||||
|
|
||||||
# File extensions that should be converted to markdown
|
# File extensions that should be converted to markdown
|
||||||
CONVERTIBLE_EXTENSIONS = {
|
CONVERTIBLE_EXTENSIONS = {
|
||||||
".pdf",
|
".pdf",
|
||||||
|
|
@ -78,6 +81,7 @@ async def convert_file_to_markdown(file_path: Path) -> Path | None:
|
||||||
async def upload_files(
|
async def upload_files(
|
||||||
thread_id: str,
|
thread_id: str,
|
||||||
files: list[UploadFile] = File(...),
|
files: list[UploadFile] = File(...),
|
||||||
|
upload_target: str | None = Form(default=None),
|
||||||
) -> UploadResponse:
|
) -> UploadResponse:
|
||||||
"""Upload multiple files to a thread's uploads directory.
|
"""Upload multiple files to a thread's uploads directory.
|
||||||
|
|
||||||
|
|
@ -95,6 +99,19 @@ async def upload_files(
|
||||||
raise HTTPException(status_code=400, detail="No files provided")
|
raise HTTPException(status_code=400, detail="No files provided")
|
||||||
|
|
||||||
uploads_dir = get_uploads_dir(thread_id)
|
uploads_dir = get_uploads_dir(thread_id)
|
||||||
|
target_dir = uploads_dir
|
||||||
|
relative_upload_prefix = "uploads"
|
||||||
|
|
||||||
|
if upload_target == SKILL_UPLOAD_TARGET:
|
||||||
|
target_dir = uploads_dir / SKILL_UPLOAD_TARGET
|
||||||
|
|
||||||
|
# Clean existing uploads/skill directory to keep only the latest skill package
|
||||||
|
if target_dir.exists():
|
||||||
|
shutil.rmtree(target_dir)
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
relative_upload_prefix = f"uploads/{SKILL_UPLOAD_TARGET}"
|
||||||
|
|
||||||
uploaded_files = []
|
uploaded_files = []
|
||||||
|
|
||||||
sandbox_provider = get_sandbox_provider()
|
sandbox_provider = get_sandbox_provider()
|
||||||
|
|
@ -107,12 +124,12 @@ async def upload_files(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Save the original file
|
# Save the original file
|
||||||
file_path = uploads_dir / file.filename
|
file_path = target_dir / file.filename
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
|
|
||||||
# Build relative path from backend root
|
# Build relative path from backend root
|
||||||
relative_path = f".deer-flow/threads/{thread_id}/user-data/uploads/{file.filename}"
|
relative_path = f".deer-flow/threads/{thread_id}/user-data/{relative_upload_prefix}/{file.filename}"
|
||||||
virtual_path = f"/mnt/user-data/uploads/{file.filename}"
|
virtual_path = f"/mnt/user-data/{relative_upload_prefix}/{file.filename}"
|
||||||
sandbox.update_file(virtual_path, content)
|
sandbox.update_file(virtual_path, content)
|
||||||
|
|
||||||
file_info = {
|
file_info = {
|
||||||
|
|
@ -120,7 +137,7 @@ async def upload_files(
|
||||||
"size": str(len(content)),
|
"size": str(len(content)),
|
||||||
"path": relative_path, # Actual filesystem path (relative to backend/)
|
"path": relative_path, # Actual filesystem path (relative to backend/)
|
||||||
"virtual_path": virtual_path, # Path for Agent in sandbox
|
"virtual_path": virtual_path, # Path for Agent in sandbox
|
||||||
"artifact_url": f"/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/{file.filename}", # HTTP URL
|
"artifact_url": f"/api/threads/{thread_id}/artifacts/mnt/user-data/{relative_upload_prefix}/{file.filename}", # HTTP URL
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"Saved file: {file.filename} ({len(content)} bytes) to {relative_path}")
|
logger.info(f"Saved file: {file.filename} ({len(content)} bytes) to {relative_path}")
|
||||||
|
|
@ -130,11 +147,13 @@ async def upload_files(
|
||||||
if file_ext in CONVERTIBLE_EXTENSIONS:
|
if file_ext in CONVERTIBLE_EXTENSIONS:
|
||||||
md_path = await convert_file_to_markdown(file_path)
|
md_path = await convert_file_to_markdown(file_path)
|
||||||
if md_path:
|
if md_path:
|
||||||
md_relative_path = f".deer-flow/threads/{thread_id}/user-data/uploads/{md_path.name}"
|
md_relative_path = f".deer-flow/threads/{thread_id}/user-data/{relative_upload_prefix}/{md_path.name}"
|
||||||
file_info["markdown_file"] = md_path.name
|
file_info["markdown_file"] = md_path.name
|
||||||
file_info["markdown_path"] = md_relative_path
|
file_info["markdown_path"] = md_relative_path
|
||||||
file_info["markdown_virtual_path"] = f"/mnt/user-data/uploads/{md_path.name}"
|
file_info["markdown_virtual_path"] = f"/mnt/user-data/{relative_upload_prefix}/{md_path.name}"
|
||||||
file_info["markdown_artifact_url"] = f"/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/{md_path.name}"
|
file_info["markdown_artifact_url"] = (
|
||||||
|
f"/api/threads/{thread_id}/artifacts/mnt/user-data/{relative_upload_prefix}/{md_path.name}"
|
||||||
|
)
|
||||||
|
|
||||||
uploaded_files.append(file_info)
|
uploaded_files.append(file_info)
|
||||||
|
|
||||||
|
|
@ -165,17 +184,18 @@ async def list_uploaded_files(thread_id: str) -> dict:
|
||||||
return {"files": [], "count": 0}
|
return {"files": [], "count": 0}
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
for file_path in sorted(uploads_dir.iterdir()):
|
for file_path in sorted(uploads_dir.rglob("*")):
|
||||||
if file_path.is_file():
|
if file_path.is_file():
|
||||||
stat = file_path.stat()
|
stat = file_path.stat()
|
||||||
relative_path = f".deer-flow/threads/{thread_id}/user-data/uploads/{file_path.name}"
|
path_in_uploads = file_path.relative_to(uploads_dir).as_posix()
|
||||||
|
relative_path = f".deer-flow/threads/{thread_id}/user-data/uploads/{path_in_uploads}"
|
||||||
files.append(
|
files.append(
|
||||||
{
|
{
|
||||||
"filename": file_path.name,
|
"filename": path_in_uploads,
|
||||||
"size": stat.st_size,
|
"size": stat.st_size,
|
||||||
"path": relative_path, # Actual filesystem path (relative to backend/)
|
"path": relative_path, # Actual filesystem path (relative to backend/)
|
||||||
"virtual_path": f"/mnt/user-data/uploads/{file_path.name}", # Path for Agent in sandbox
|
"virtual_path": f"/mnt/user-data/uploads/{path_in_uploads}", # Path for Agent in sandbox
|
||||||
"artifact_url": f"/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/{file_path.name}", # HTTP URL
|
"artifact_url": f"/api/threads/{thread_id}/artifacts/mnt/user-data/uploads/{path_in_uploads}", # HTTP URL
|
||||||
"extension": file_path.suffix,
|
"extension": file_path.suffix,
|
||||||
"modified": stat.st_mtime,
|
"modified": stat.st_mtime,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,17 +81,34 @@ export default function ChatPage() {
|
||||||
}
|
}
|
||||||
}, [inputInitialValue]);
|
}, [inputInitialValue]);
|
||||||
const isNewThread = useMemo(
|
const isNewThread = useMemo(
|
||||||
() => threadIdFromPath === "new",
|
() => {
|
||||||
[threadIdFromPath],
|
if (threadIdFromPath !== "new") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryThreadId = searchParams.get("thread_id")?.trim();
|
||||||
|
const queryIsNew = searchParams.get("isnew")?.trim().toLowerCase();
|
||||||
|
const shouldReuseExisting = queryIsNew === "false" && !!queryThreadId;
|
||||||
|
|
||||||
|
return !shouldReuseExisting;
|
||||||
|
},
|
||||||
|
[threadIdFromPath, searchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const uploadTarget = useMemo(() => {
|
||||||
|
const target = searchParams.get("upload_target")?.trim().toLowerCase();
|
||||||
|
return target === "skill" ? "skill" : undefined;
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
const [threadId, setThreadId] = useState<string | null>(null);
|
const [threadId, setThreadId] = useState<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (threadIdFromPath !== "new") {
|
if (threadIdFromPath !== "new") {
|
||||||
setThreadId(threadIdFromPath);
|
setThreadId(threadIdFromPath);
|
||||||
} else {
|
} else {
|
||||||
setThreadId(uuid());
|
const queryThreadId = searchParams.get("thread_id")?.trim();
|
||||||
|
setThreadId(queryThreadId || uuid());
|
||||||
}
|
}
|
||||||
}, [threadIdFromPath]);
|
}, [threadIdFromPath, searchParams]);
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const [finalState, setFinalState] = useState<AgentThreadState | null>(null);
|
const [finalState, setFinalState] = useState<AgentThreadState | null>(null);
|
||||||
|
|
@ -185,6 +202,7 @@ export default function ChatPage() {
|
||||||
isNewThread,
|
isNewThread,
|
||||||
threadId,
|
threadId,
|
||||||
thread,
|
thread,
|
||||||
|
uploadTarget,
|
||||||
threadContext: {
|
threadContext: {
|
||||||
...settings.context,
|
...settings.context,
|
||||||
thinking_enabled: settings.context.mode !== "flash",
|
thinking_enabled: settings.context.mode !== "flash",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { getAPIClient } from "../api";
|
import { getAPIClient } from "../api";
|
||||||
import { useUpdateSubtask } from "../tasks/context";
|
import { useUpdateSubtask } from "../tasks/context";
|
||||||
import { uploadFiles } from "../uploads";
|
import { uploadFiles } from "../uploads";
|
||||||
|
import type { UploadTarget } from "../uploads/api";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AgentThread,
|
AgentThread,
|
||||||
|
|
@ -83,12 +84,14 @@ export function useSubmitThread({
|
||||||
thread,
|
thread,
|
||||||
threadContext,
|
threadContext,
|
||||||
isNewThread,
|
isNewThread,
|
||||||
|
uploadTarget,
|
||||||
afterSubmit,
|
afterSubmit,
|
||||||
}: {
|
}: {
|
||||||
isNewThread: boolean;
|
isNewThread: boolean;
|
||||||
threadId: string | null | undefined;
|
threadId: string | null | undefined;
|
||||||
thread: UseStream<AgentThreadState>;
|
thread: UseStream<AgentThreadState>;
|
||||||
threadContext: Omit<AgentThreadContext, "thread_id">;
|
threadContext: Omit<AgentThreadContext, "thread_id">;
|
||||||
|
uploadTarget?: UploadTarget;
|
||||||
afterSubmit?: () => void;
|
afterSubmit?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
@ -127,7 +130,7 @@ export function useSubmitThread({
|
||||||
);
|
);
|
||||||
|
|
||||||
if (files.length > 0 && threadId) {
|
if (files.length > 0 && threadId) {
|
||||||
await uploadFiles(threadId, files);
|
await uploadFiles(threadId, files, { target: uploadTarget });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to upload files:", error);
|
console.error("Failed to upload files:", error);
|
||||||
|
|
@ -167,7 +170,7 @@ export function useSubmitThread({
|
||||||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||||
afterSubmit?.();
|
afterSubmit?.();
|
||||||
},
|
},
|
||||||
[thread, isNewThread, threadId, threadContext, queryClient, afterSubmit],
|
[thread, isNewThread, threadId, threadContext, uploadTarget, queryClient, afterSubmit],
|
||||||
);
|
);
|
||||||
return callback;
|
return callback;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,15 @@ export interface ListFilesResponse {
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UploadTarget = "skill";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload files to a thread
|
* Upload files to a thread
|
||||||
*/
|
*/
|
||||||
export async function uploadFiles(
|
export async function uploadFiles(
|
||||||
threadId: string,
|
threadId: string,
|
||||||
files: File[],
|
files: File[],
|
||||||
|
options?: { target?: UploadTarget },
|
||||||
): Promise<UploadResponse> {
|
): Promise<UploadResponse> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
|
|
@ -42,6 +45,10 @@ export async function uploadFiles(
|
||||||
formData.append("files", file);
|
formData.append("files", file);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options?.target) {
|
||||||
|
formData.append("upload_target", options.target);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${getBackendBaseURL()}/api/threads/${threadId}/uploads`,
|
`${getBackendBaseURL()}/api/threads/${threadId}/uploads`,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue