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:
Timebomb2018
2026-03-19 12:31:41 +08:00
parent 33d522b387
commit 8c804a1011
9 changed files with 505 additions and 113 deletions

View File

@@ -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:
"""

View File

@@ -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.

View 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.

View File

@@ -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.