fix(agent features):
1.Voice output is generated in a streaming manner. 2.Multimodal file storage type repair; 3.Adding features to the configuration of the sub-agents in the multi-agent system
This commit is contained in:
@@ -7,7 +7,7 @@ file operations across different storage backends.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
from typing import AsyncIterator, Optional
|
||||
|
||||
|
||||
class StorageBackend(ABC):
|
||||
@@ -42,6 +42,26 @@ class StorageBackend(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upload_stream(
|
||||
self,
|
||||
file_key: str,
|
||||
stream: AsyncIterator[bytes],
|
||||
content_type: Optional[str] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Upload a file from an async byte stream.
|
||||
|
||||
Args:
|
||||
file_key: Unique identifier for the file.
|
||||
stream: Async iterator yielding bytes chunks.
|
||||
content_type: Optional MIME type of the file.
|
||||
|
||||
Returns:
|
||||
Total bytes written.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def download(self, file_key: str) -> bytes:
|
||||
"""
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Optional
|
||||
|
||||
import aiofiles
|
||||
import aiofiles.os
|
||||
from typing import AsyncIterator
|
||||
|
||||
from app.core.storage.base import StorageBackend
|
||||
from app.core.storage_exceptions import (
|
||||
@@ -179,6 +180,36 @@ class LocalStorage(StorageBackend):
|
||||
full_path = self._get_full_path(file_key)
|
||||
return full_path.exists()
|
||||
|
||||
async def upload_stream(
|
||||
self,
|
||||
file_key: str,
|
||||
stream: AsyncIterator[bytes],
|
||||
content_type: Optional[str] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Upload a file from an async byte stream to the local file system.
|
||||
|
||||
Returns:
|
||||
Total bytes written.
|
||||
"""
|
||||
full_path = self._get_full_path(file_key)
|
||||
try:
|
||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
total = 0
|
||||
async with aiofiles.open(full_path, "wb") as f:
|
||||
async for chunk in stream:
|
||||
await f.write(chunk)
|
||||
total += len(chunk)
|
||||
logger.info(f"File stream uploaded successfully: {file_key}")
|
||||
return total
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to stream upload file {file_key}: {e}")
|
||||
raise StorageUploadError(
|
||||
message=f"Failed to stream upload file: {e}",
|
||||
file_key=file_key,
|
||||
cause=e,
|
||||
)
|
||||
|
||||
async def get_url(self, file_key: str, expires: int = 3600) -> str:
|
||||
"""
|
||||
Get an access URL for the file.
|
||||
|
||||
@@ -5,8 +5,9 @@ This module provides a storage backend that stores files on Aliyun Object
|
||||
Storage Service (OSS) using the oss2 SDK.
|
||||
"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import AsyncIterator, Optional
|
||||
|
||||
import oss2
|
||||
from oss2.exceptions import NoSuchKey, OssError
|
||||
@@ -125,10 +126,39 @@ class OSSStorage(StorageBackend):
|
||||
cause=e,
|
||||
)
|
||||
|
||||
async def upload_stream(
|
||||
self,
|
||||
file_key: str,
|
||||
stream: AsyncIterator[bytes],
|
||||
content_type: Optional[str] = None,
|
||||
) -> int:
|
||||
"""Upload from async stream to OSS. Returns total bytes written."""
|
||||
buf = io.BytesIO()
|
||||
try:
|
||||
async for chunk in stream:
|
||||
buf.write(chunk)
|
||||
content = buf.getvalue()
|
||||
headers = {"Content-Type": content_type} if content_type else None
|
||||
self.bucket.put_object(file_key, content, headers=headers)
|
||||
logger.info(f"File stream uploaded to OSS successfully: {file_key}")
|
||||
return len(content)
|
||||
except OssError as e:
|
||||
logger.error(f"OSS error stream uploading file {file_key}: {e}")
|
||||
raise StorageUploadError(
|
||||
message=f"Failed to stream upload file to OSS: {e.message}",
|
||||
file_key=file_key,
|
||||
cause=e,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to stream upload file to OSS {file_key}: {e}")
|
||||
raise StorageUploadError(
|
||||
message=f"Failed to stream upload file to OSS: {e}",
|
||||
file_key=file_key,
|
||||
cause=e,
|
||||
)
|
||||
|
||||
async def download(self, file_key: str) -> bytes:
|
||||
"""
|
||||
Download a file from OSS.
|
||||
|
||||
Args:
|
||||
file_key: Unique identifier for the file in the storage system.
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ This module provides a storage backend that stores files on AWS S3
|
||||
using the boto3 SDK.
|
||||
"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import AsyncIterator, Optional
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError, NoCredentialsError, BotoCoreError
|
||||
@@ -174,6 +175,62 @@ class S3Storage(StorageBackend):
|
||||
cause=e,
|
||||
)
|
||||
|
||||
async def upload_stream(
|
||||
self,
|
||||
file_key: str,
|
||||
stream: AsyncIterator[bytes],
|
||||
content_type: Optional[str] = None,
|
||||
) -> int:
|
||||
"""Upload from async stream to S3 via multipart upload. Returns total bytes written."""
|
||||
extra_args = {"ContentType": content_type} if content_type else {}
|
||||
mpu = self.client.create_multipart_upload(
|
||||
Bucket=self.bucket_name, Key=file_key, **extra_args
|
||||
)
|
||||
upload_id = mpu["UploadId"]
|
||||
parts = []
|
||||
part_number = 1
|
||||
buf = io.BytesIO()
|
||||
total = 0
|
||||
min_part_size = 5 * 1024 * 1024 # S3 最小分片 5MB
|
||||
try:
|
||||
async for chunk in stream:
|
||||
buf.write(chunk)
|
||||
total += len(chunk)
|
||||
if buf.tell() >= min_part_size:
|
||||
buf.seek(0)
|
||||
resp = self.client.upload_part(
|
||||
Bucket=self.bucket_name, Key=file_key,
|
||||
UploadId=upload_id, PartNumber=part_number, Body=buf.read()
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": resp["ETag"]})
|
||||
part_number += 1
|
||||
buf = io.BytesIO()
|
||||
# 上传剩余数据(最后一片可小于 5MB)
|
||||
remaining = buf.getvalue()
|
||||
if remaining:
|
||||
resp = self.client.upload_part(
|
||||
Bucket=self.bucket_name, Key=file_key,
|
||||
UploadId=upload_id, PartNumber=part_number, Body=remaining
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": resp["ETag"]})
|
||||
self.client.complete_multipart_upload(
|
||||
Bucket=self.bucket_name, Key=file_key,
|
||||
UploadId=upload_id,
|
||||
MultipartUpload={"Parts": parts}
|
||||
)
|
||||
logger.info(f"File stream uploaded to S3 successfully: {file_key}")
|
||||
return total
|
||||
except Exception as e:
|
||||
self.client.abort_multipart_upload(
|
||||
Bucket=self.bucket_name, Key=file_key, UploadId=upload_id
|
||||
)
|
||||
logger.error(f"Failed to stream upload file to S3 {file_key}: {e}")
|
||||
raise StorageUploadError(
|
||||
message=f"Failed to stream upload file to S3: {e}",
|
||||
file_key=file_key,
|
||||
cause=e,
|
||||
)
|
||||
|
||||
async def download(self, file_key: str) -> bytes:
|
||||
"""
|
||||
Download a file from S3.
|
||||
|
||||
Reference in New Issue
Block a user