Skip to content

Commit 862866e

Browse files
authored
✨ Multi modal agent. Pass the URL of the multimodal file as the query to the agent.
2 parents 9147098 + 6c83812 commit 862866e

File tree

15 files changed

+64
-1306
lines changed

15 files changed

+64
-1306
lines changed

backend/agents/create_agent_info.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -314,13 +314,12 @@ async def join_minio_file_description_to_query(minio_files, query):
314314
if minio_files and isinstance(minio_files, list):
315315
file_descriptions = []
316316
for file in minio_files:
317-
if isinstance(file, dict) and "description" in file and file["description"]:
318-
file_descriptions.append(file["description"])
319-
317+
if isinstance(file, dict) and "url" in file and file["url"] and "name" in file and file["name"]:
318+
file_descriptions.append(f"File name: {file['name']}, S3 URL: s3:/{file['url']}")
320319
if file_descriptions:
321-
final_query = "User provided some reference files:\n"
320+
final_query = "User uploaded files. The file information is as follows:\n"
322321
final_query += "\n".join(file_descriptions) + "\n\n"
323-
final_query += f"User wants to answer questions based on the above information: {query}"
322+
final_query += f"User wants to answer questions based on the information in the above files: {query}"
324323
return final_query
325324

326325

backend/apps/file_management_app.py

Lines changed: 2 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import logging
2-
import os
32
from http import HTTPStatus
43
from typing import List, Optional
54

6-
from fastapi import APIRouter, Body, File, Form, Header, HTTPException, Path as PathParam, Query, Request, UploadFile
5+
from fastapi import APIRouter, Body, File, Form, Header, HTTPException, Path as PathParam, Query, UploadFile
76
from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse
87

98
from consts.model import ProcessParams
109
from services.file_management_service import upload_to_minio, upload_files_impl, \
11-
get_file_url_impl, get_file_stream_impl, delete_file_impl, list_files_impl, \
12-
preprocess_files_generator
13-
from utils.auth_utils import get_current_user_info
10+
get_file_url_impl, get_file_stream_impl, delete_file_impl, list_files_impl
1411
from utils.file_management_utils import trigger_data_process
1512

1613
logger = logging.getLogger("file_management_app")
@@ -271,61 +268,3 @@ async def get_storage_file_batch_urls(
271268
"failed_count": sum(1 for r in results if not r.get("success", False)),
272269
"results": results
273270
}
274-
275-
276-
@file_management_runtime_router.post("/preprocess")
277-
async def agent_preprocess_api(
278-
request: Request, query: str = Form(...),
279-
files: List[UploadFile] = File(...),
280-
authorization: Optional[str] = Header(None)
281-
):
282-
"""
283-
Preprocess uploaded files and return streaming response
284-
"""
285-
try:
286-
# Pre-read and cache all file contents
287-
user_id, tenant_id, language = get_current_user_info(
288-
authorization, request)
289-
file_cache = []
290-
for file in files:
291-
try:
292-
content = await file.read()
293-
file_cache.append({
294-
"filename": file.filename or "",
295-
"content": content,
296-
"ext": os.path.splitext(file.filename or "")[1].lower()
297-
})
298-
except Exception as e:
299-
file_cache.append({
300-
"filename": file.filename or "",
301-
"error": str(e)
302-
})
303-
304-
# Generate unique task ID for this preprocess operation
305-
import uuid
306-
task_id = str(uuid.uuid4())
307-
conversation_id = request.query_params.get("conversation_id")
308-
if conversation_id:
309-
conversation_id = int(conversation_id)
310-
else:
311-
conversation_id = -1 # Default for cases without conversation_id
312-
313-
# Call service layer to generate streaming response
314-
return StreamingResponse(
315-
preprocess_files_generator(
316-
query=query,
317-
file_cache=file_cache,
318-
tenant_id=tenant_id,
319-
language=language,
320-
task_id=task_id,
321-
conversation_id=conversation_id
322-
),
323-
media_type="text/event-stream",
324-
headers={
325-
"Cache-Control": "no-cache",
326-
"Connection": "keep-alive"
327-
}
328-
)
329-
except Exception as e:
330-
raise HTTPException(
331-
status_code=500, detail=f"File preprocessing error: {str(e)}")

backend/services/file_management_service.py

Lines changed: 2 additions & 229 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import asyncio
2-
import json
32
import logging
43
import os
54
from io import BytesIO
65
from pathlib import Path
7-
from typing import List, Optional, AsyncGenerator
6+
from typing import List, Optional
87

9-
import httpx
108
from fastapi import UploadFile
119

12-
from agents.preprocess_manager import preprocess_manager
13-
from consts.const import UPLOAD_FOLDER, MAX_CONCURRENT_UPLOADS, DATA_PROCESS_SERVICE, LANGUAGE, MODEL_CONFIG_MAPPING
10+
from consts.const import UPLOAD_FOLDER, MAX_CONCURRENT_UPLOADS, MODEL_CONFIG_MAPPING
1411
from database.attachment_db import (
1512
upload_fileobj,
1613
get_file_url,
@@ -20,9 +17,7 @@
2017
list_files
2118
)
2219
from services.vectordatabase_service import ElasticSearchService, get_vector_db_core
23-
from utils.attachment_utils import convert_image_to_text, convert_long_text_to_text
2420
from utils.config_utils import tenant_config_manager, get_model_name_from_config
25-
from utils.prompt_template_utils import get_file_processing_messages_template
2621
from utils.file_management_utils import save_upload_file
2722

2823
from nexent import MessageObserver
@@ -188,228 +183,6 @@ async def list_files_impl(prefix: str, limit: Optional[int] = None):
188183
return files
189184

190185

191-
def get_parsing_file_data(index: int, total_files: int, filename: str) -> dict:
192-
"""
193-
Get structured data for parsing file message
194-
195-
Args:
196-
index: Current file index (0-based)
197-
total_files: Total number of files
198-
filename: Name of the file being parsed
199-
200-
Returns:
201-
dict: Structured data with parameters for internationalization
202-
"""
203-
return {
204-
"params": {
205-
"index": index + 1,
206-
"total": total_files,
207-
"filename": filename
208-
}
209-
}
210-
211-
212-
def get_truncation_data(filename: str, truncation_percentage: int) -> dict:
213-
"""
214-
Get structured data for truncation message
215-
216-
Args:
217-
filename: Name of the file being truncated
218-
truncation_percentage: Percentage of content that was read
219-
220-
Returns:
221-
dict: Structured data with parameters for internationalization
222-
"""
223-
return {
224-
"params": {
225-
"filename": filename,
226-
"percentage": truncation_percentage
227-
}
228-
}
229-
230-
231-
async def preprocess_files_generator(
232-
query: str,
233-
file_cache: List[dict],
234-
tenant_id: str,
235-
language: str,
236-
task_id: str,
237-
conversation_id: int
238-
) -> AsyncGenerator[str, None]:
239-
"""
240-
Generate streaming response for file preprocessing
241-
242-
Args:
243-
query: User query string
244-
file_cache: List of cached file data
245-
tenant_id: Tenant ID
246-
language: Language preference
247-
task_id: Unique task ID
248-
conversation_id: Conversation ID
249-
250-
Yields:
251-
str: JSON formatted streaming messages
252-
"""
253-
file_descriptions = []
254-
total_files = len(file_cache)
255-
256-
# Create and register the preprocess task
257-
task = asyncio.current_task()
258-
if task:
259-
preprocess_manager.register_preprocess_task(
260-
task_id, conversation_id, task)
261-
262-
try:
263-
for index, file_data in enumerate(file_cache):
264-
if task and task.done():
265-
logger.info(f"Preprocess task {task_id} was cancelled")
266-
break
267-
268-
progress = int((index / total_files) * 100)
269-
progress_message = json.dumps({
270-
"type": "progress",
271-
"progress": progress,
272-
"message_data": get_parsing_file_data(index, total_files, file_data['filename'])
273-
}, ensure_ascii=False)
274-
yield f"data: {progress_message}\n\n"
275-
await asyncio.sleep(0.1)
276-
277-
try:
278-
# Check if file already has an error
279-
if "error" in file_data:
280-
raise Exception(file_data["error"])
281-
282-
if file_data["ext"] in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']:
283-
description = await process_image_file(query, file_data["filename"], file_data["content"], tenant_id, language)
284-
truncation_percentage = None
285-
else:
286-
description, truncation_percentage = await process_text_file(query, file_data["filename"], file_data["content"], tenant_id, language)
287-
file_descriptions.append(description)
288-
289-
# Send processing result for each file
290-
file_message_data = {
291-
"type": "file_processed",
292-
"filename": file_data["filename"],
293-
"description": description
294-
}
295-
file_message = json.dumps(
296-
file_message_data, ensure_ascii=False)
297-
yield f"data: {file_message}\n\n"
298-
await asyncio.sleep(0.1)
299-
300-
# Send truncation notice immediately if file was truncated
301-
if truncation_percentage is not None and int(truncation_percentage) < 100:
302-
if int(truncation_percentage) == 0:
303-
truncation_percentage = "< 1"
304-
305-
truncation_message = json.dumps({
306-
"type": "truncation",
307-
"message_data": get_truncation_data(file_data['filename'], truncation_percentage)
308-
}, ensure_ascii=False)
309-
yield f"data: {truncation_message}\n\n"
310-
await asyncio.sleep(0.1)
311-
except Exception as e:
312-
error_description = f"Error parsing file {file_data['filename']}: {str(e)}"
313-
logger.exception(error_description)
314-
file_descriptions.append(error_description)
315-
error_message = json.dumps({
316-
"type": "error",
317-
"filename": file_data["filename"],
318-
"message": error_description
319-
}, ensure_ascii=False)
320-
yield f"data: {error_message}\n\n"
321-
await asyncio.sleep(0.1)
322-
323-
# Send completion message
324-
complete_message = json.dumps({
325-
"type": "complete",
326-
"progress": 100,
327-
"final_query": query
328-
}, ensure_ascii=False)
329-
yield f"data: {complete_message}\n\n"
330-
finally:
331-
preprocess_manager.unregister_preprocess_task(task_id)
332-
333-
334-
async def process_image_file(query: str, filename: str, file_content: bytes, tenant_id: str, language: str = LANGUAGE["ZH"]) -> str:
335-
"""
336-
Process image file, convert to text using external API
337-
"""
338-
# Load messages based on language
339-
messages = get_file_processing_messages_template(language)
340-
341-
try:
342-
image_stream = BytesIO(file_content)
343-
text = convert_image_to_text(query, image_stream, tenant_id, language)
344-
return messages["IMAGE_CONTENT_SUCCESS"].format(filename=filename, content=text)
345-
except Exception as e:
346-
return messages["IMAGE_CONTENT_ERROR"].format(filename=filename, error=str(e))
347-
348-
349-
async def process_text_file(query: str, filename: str, file_content: bytes, tenant_id: str, language: str = LANGUAGE["ZH"]) -> tuple[str, Optional[str]]:
350-
"""
351-
Process text file, convert to text using external API
352-
"""
353-
# Load messages based on language
354-
messages = get_file_processing_messages_template(language)
355-
356-
# file_content is byte data, need to send to API through file upload
357-
data_process_service_url = DATA_PROCESS_SERVICE
358-
api_url = f"{data_process_service_url}/tasks/process_text_file"
359-
logger.info(f"Processing text file {filename} with API: {api_url}")
360-
361-
try:
362-
# Upload byte data as a file
363-
files = {
364-
'file': (filename, file_content, 'application/octet-stream')
365-
}
366-
data = {
367-
'chunking_strategy': 'basic',
368-
'timeout': 60
369-
}
370-
async with httpx.AsyncClient() as client:
371-
response = await client.post(api_url, files=files, data=data, timeout=60)
372-
373-
if response.status_code == 200:
374-
result = response.json()
375-
raw_text = result.get("text", "")
376-
logger.info(
377-
f"File processed successfully: {raw_text[:200]}...{raw_text[-200:]}..., length: {len(raw_text)}")
378-
else:
379-
error_detail = response.json().get('detail', 'unknown error') if response.headers.get(
380-
'content-type', '').startswith('application/json') else response.text
381-
logger.error(
382-
f"File processing failed (status code: {response.status_code}): {error_detail}")
383-
raise Exception(
384-
messages["FILE_PROCESSING_ERROR"].format(status_code=response.status_code, error_detail=error_detail))
385-
386-
except Exception as e:
387-
return messages["FILE_CONTENT_ERROR"].format(filename=filename, error=str(e)), None
388-
389-
try:
390-
text, truncation_percentage = convert_long_text_to_text(
391-
query, raw_text, tenant_id, language)
392-
return messages["FILE_CONTENT_SUCCESS"].format(filename=filename, content=text), truncation_percentage
393-
except Exception as e:
394-
return messages["FILE_CONTENT_ERROR"].format(filename=filename, error=str(e)), None
395-
396-
397-
def get_file_description(files: List[UploadFile]) -> str:
398-
"""
399-
Generate file description text
400-
"""
401-
if not files:
402-
return "User provided some reference files:\nNo files provided"
403-
404-
description = "User provided some reference files:\n"
405-
for file in files:
406-
ext = os.path.splitext(file.filename or "")[1].lower()
407-
if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']:
408-
description += f"- Image file {file.filename or ''}\n"
409-
else:
410-
description += f"- File {file.filename or ''}\n"
411-
return description
412-
413186
def get_llm_model(tenant_id: str):
414187
# Get the tenant config
415188
main_model_config = tenant_config_manager.get_model_config(

frontend/app/[locale]/agents/components/tool/ToolPool.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
import { useState, useEffect, useMemo, useCallback, memo } from "react";
44
import { useTranslation } from "react-i18next";
55

6-
import { Button, App, Tabs, Collapse } from "antd";
6+
import { Button, App, Tabs, Collapse, Tooltip } from "antd";
77
import {
88
SettingOutlined,
99
LoadingOutlined,
1010
ApiOutlined,
1111
ReloadOutlined,
12+
BulbOutlined,
1213
} from "@ant-design/icons";
1314

1415
import { TOOL_SOURCE_TYPES } from "@/const/agentConfig";
@@ -643,6 +644,25 @@ function ToolPool({
643644
<h4 className="text-md font-medium text-gray-700">
644645
{t("toolPool.title")}
645646
</h4>
647+
<Tooltip
648+
title={
649+
<div style={{ whiteSpace: "pre-line" }}>
650+
{t("toolPool.tooltip.functionGuide")}
651+
</div>
652+
}
653+
overlayInnerStyle={{
654+
backgroundColor: "#ffffff",
655+
color: "#374151",
656+
border: "1px solid #e5e7eb",
657+
borderRadius: "6px",
658+
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
659+
padding: "12px",
660+
maxWidth: "600px",
661+
minWidth: "400px",
662+
}}
663+
>
664+
<BulbOutlined className="ml-2 text-yellow-500" />
665+
</Tooltip>
646666
</div>
647667
<div className="flex items-center gap-2">
648668
<Button

0 commit comments

Comments
 (0)